To make our task easier, we will set the form metric property to 6 (pixels) because we need to know the item height in the ListBox. In order to calculate that height, we could drop a ListBox onto a form in the Form designer, connect it to an array, lengthen the ListBox up to the point it displays exactly ten items, take a screen grab and calculate the average height for each item. If our ListBox's border type is the default one, it is even easier. We just have to take note of the ListBox height, take away four pixels (two for the top and bottom borders), and divide by the number of items displayed in that ListBox.
When the default font is Arial and the default font size is 10 points (and also when you didn't select a large font in your Windows Display settings), the height of an item is 17 pixels.
To implement Drag'nDrop, we need two types of ingredients: events and custom properties. For the latter, each developer has his own programming style. Some people like to create _app.properties, but usually I create custom properties of the form. There are two reasons for that. First, I don't run the risk of having the values of _app. set by a form being modified by another one. Moreover, it is easier to get rid of all these properties when they are not needed any more: as soon as a form is released, dBASE will erase of all its custom properties from memory.
Here, I took a little different approach. From the start, I decided to create custom properties of the ListBox and to count mostly on its events to make it behave as I wanted. Doing it this way allowed me to have about everything encapsulated in that control. If I hadn't have used the form's onMouseMove() event to reset the ListBox when the mouse button is released outside the ListBox, the Drag'nDrop ListBox would have been transformed into a custom class.
The ListBox custom properties
In the ListBox's onOpen() event, I created three ListBox custom properties: mouse_drag (to be set to true as soon as the mouse button is clicked while the curser is over the ListBox), original_value (to keep track of the value of the item that will have to be moved), and original_cursel (to set which item number will be moved). The latter two properties are also used to undo the changes, when needed.
01 Function Listbox1_onLeftMouseDown(flags, col, row)
02 this.colorHighLight := this.colorNormal
03 this.mousePointer := 2
04 this.mouse_drag := true // to ensure we started dragging inside the listbox
05 this.original_cursel := this.cursel
06 this.original_value := this.value
07 this.aChoices.delete( this.aChoices.scan( this.selected()))
08 this.aChoices.size --
09 this.dataSource = "Array this.aChoices"
When the left mouse is clicked while the cursor is over the ListBox, the three custom properties we just spoke about have their values set (lines 04 to 06). The clicked item is deleted (line 07), the array is resized (line 08), the ListBox is refreshed (line 09), and no item gets highlighted (line 02).
Lastly, the mouse pointer is changed to a cross made out of two fine lines (line 03). Initially, I wanted the mouse pointer to be an arrow with a long fine line object as wide as the ListBox, slightly above the tip of the arrow. Unfortunately, dBASE had some problems guessing which object should have focus and the result was unsatisfactory. The cross hair pointer was my best alternative solution.
01 Function Listbox1_onLeftMouseUp(flags, col, row)
02 local nTarget
03 if row < this.aChoices.size * 17 // if dropped among the items
05 nTarget = round( row/17 ) + 1
06 for i = this.aChoices.size to nTarget step - 1
07 if i > 1
08 this.aChoices[i] := this.aChoices[i - 1]
11 this.aChoices[nTarget] = this.original_value
12 else // if dropped at the bottom of the list
14 nTarget = this.aChoices.size
16 this.datasource := "Array this.aChoices"
17 this.cursel = nTarget
18 this.colorHighlight := "HighLightText/HighLight"
19 this.mousePointer := 13
20 this.mouse_drag := false
Line 03 serves to decide if the mouse button is released over the listed items or below them (the list could be smaller than the ListBox). To fully understand that line, we must know that the row parameter is calculated in pixels, starting from the top of the ListBox. As soon as the mouse button is released while the cursor is over the ListBox, dBASE calculates the exact location of the mouse pointer and passes it as a parameter. For example, if row = 34, that means that the mouse pointer is at the bottom of the second item (each item being 17 pixels high). Of course, the row pixel may not be a multiple of 17. That's why line 05 is needed to round off the result of that division and to decide if the moved item should be inserted before or after the item over which the mouse button is released.
When the mouse button is released with the cross hair over any listed item, the item is re-inserted at the position indicated (line 04) and all the items below the mouse pointer are moved down (lines 06 to 10). In order to do that, we have to start at the end of the list. At line 08, the last element takes the value of the previous one, the previous one takes the value of the one before, and so on. Otherwise, we would copy the same value down to the last element. In our For...next loop, the negative step size (step - 1) is needed to decrease the value of “i” instead of increasing it. Lastly, the value of the item over which the mouse button is released is replaced with the value of the item deleted when the mouse button was pressed (line 11). That gives the illusion that the item is inserted. Finally the ListBox is refreshed (line 18).
When the mouse button is released below the listed items, a new element is added to the array and it receives the value of the one deleted by the onLeftMouseDown() event (line 13).
When all that is done, the mouse pointer returns to its original shape (line 19) and the cursel reappears (line 18).
But what if the mouse button is pressed inside the ListBox but is released outside of it? Will the listBox's onLeftMouseUp()ever fire? No, it would never be fired. In that case, won't the item deleted when the mouse button was pressed be lost forever? Yes indeed, it would tragically be lost in Cyberspace. This is why the form's onMouseMove() event is used to undo the changes done by the ListBox's onLeftMouseDown() event. As soon as the mouse quits the ListBox, the mouse_drag custom property serves as a flag to inform the form when some house cleaning has to be done.
We pointed out in our introduction that our code works only when the form's metric is in pixels. If ever someone transforms this ListBox into a custom class (with the need to fiddle with some code in the form's onMouseMove()event), we added the necessary code into the ListBox's onDesignOpen() event to change the form's metric as soon as an instance of that custom ListBox class is dropped on a form in the Form designer.
Lastly, to use that code in your application, you will have to connect the Drag'nDrop ListBox to your array and seek and replace aChoices with your array's name.
The first release of dB2K will add system-wide support for Drag'nDrop. Many controls (browse, container, form, grid, image, listBox, noteBook, paintBox, reportViewer and treeView controls) will be able to serve as Drag'nDrop sources or as targets. Moving items from one ListBox to another one will be easier than ever.But nothing new will be added (for now, at least) to support, for example, Drag'nDrop functionality within a listBox. Meanwhile, that Drag'nDrop functionality can be implemented with a few short lines of code.
At one of the drugstores where I am working as a pharmacist, I showed the Drag'nDrop ListBox in action to clerk who helps me in the prescription department. When I showed her a printout of the code, she was surprised to see that just two and an half pages of code were needed to create the interface and set its behaviour. Indeed, we are so spoiled using dBASE that it takes new eyes to remind us how simple it is to use.
To download the Drag'nDrop
Application, click here
(it's a 2 Kb zipped file)