Equal Rights for Mousers

 

Steve Hoag
Microsoft Corporation

March 1999

Summary: Discusses how to implement the drag-and-drop feature in Microsoft® Visual Basic®. (9 printed pages) Includes:

Introduction

Users of Microsoft Windows® fall into two general categories: those who prefer to use the keyboard and those who prefer to use the mouse. We've all been taught to look after the needs of keyboard users by providing access keys (the underlined letter in a caption or menu) and shortcuts (the Ctrl + letter combination), but the needs of mouse users have largely been ignored. As programmers we tend to be keyboard users ourselves, so the emphasis on keyboard-oriented features is understandable—but that's no excuse. It's about time that we start providing equal rights for mousers.

One thing that mouse users expect is the ability to drag and drop. If you look at most major applications or at Windows itself, the drag-and-drop feature is everywhere. From dragging and dropping files in Windows Explorer to dragging and dropping text in Microsoft Word, the expectations of users have been set.

Despite these expectations, few Visual Basic programmers provide drag-and-drop capability in their applications—most likely because implementing this feature appears to be much more difficult than it actually is. In this article we'll look at how easy the drag-and-drop operation really is, with examples showing how to move text, pictures, and files within forms, between forms, or even between applications.

Let's Keep It Simple

It's no surprise that the drag-and-drop operation may seem daunting—we have automatic or manual modes, plus a number of properties, methods, and events involved. Making things even more confusing, Visual Basic version 5.0 introduced OLE drag and drop with a whole new set of properties, methods, and events. Where do you start?

First of all, forget that standard drag and drop even exists—it's just there for backward compatibility with existing code. Anything that can be done with standard drag and drop can also be done with OLE drag and drop, and OLE drag and drop provides much richer capabilities.

Second, forget about the automatic OLE drag-and-drop modes. If you set automatic drag-and-drop properties at design time, you're creating potential maintenance problems; if you set them in code, you'll end up writing nearly as much code as you would for manual mode.

Manual OLE drag and drop is all that remains, so from here on out we'll simply refer to it as drag and drop. We've conveniently eliminated more than half of the choices, but there are still a few properties, methods, and events we'll need to know about. The best way to learn them is to inspect the drag-and-drop process.

How Drag and Drop Works

Dragging and dropping is actually the same as cutting and pasting (or copying and pasting) using the mouse instead of the keyboard. In both cases we have a source (where we're cutting or copying from) and a target (where we're pasting to). During either operation, a copy of the data is maintained in memory.

Cut and paste uses the Clipboard; drag and drop uses the DataObject object—which is in essence a private clipboard.

Let's look at the sequence of events in a typical drag-and-drop operation:

  • Dragging is initiated by calling the OLEDrag method in the MouseDown event of the source control. This in turn fires the OLEStartDrag event for the control and instantiates a DataObject.

  • In the OLEStartDrag event, the AllowedEffects argument must be set. This argument is used to tell the target whether the data is being moved or copied.

    In the same event, the SetData method of the DataObject must be called. This loads a copy of the data into memory.

  • The OLEDragDrop event is fired in the target when the user releases the mouse over a target control. The GetFormat method of the DataObject is used to make sure the format of the data is appropriate to the target control and, if it is, the GetData method is used to retrieve the data.

    In order to act as a target, a control must have its OLEDropMode property set to 1—Manual. If you enable OLEDropMode and don't check the format in the OLEDragDrop event, bad things can happen.

  • Once the drop operation is completed, the OLECompleteDrag event is fired. If the data is being moved rather than copied, code can be added here to delete the original data from the source control.

There are three additional events that can be used when you need finer control over the drag-and-drop process:

  • The OLEDragOver event is fired in a target control whenever dragged items pass over the control. This event can be used to display a customized cursor or to modify the AllowedEffects.
  • The OLEGiveFeedback event is fired in the source control whenever dragged items pass over a target. This event can also be used to change the cursor or to modify the AllowedEffects.
  • The OLESetData event is fired in the source control when the OLEDragDrop event occurs in a target. When dragging a large amount of data, the data can be loaded into the DataObject here instead of in the OLEStartDrag event.

That's it in a nutshell. For most simple cases, you can enable dragging with three lines of code, and you can enable dropping with a single line of code.

In the next sections we'll look at some examples of the drag-and-drop operation in action.

Dragging Between List Boxes

One common scenario where drag and drop is expected (and highly appreciated) involves moving items from one ListBox to another. Typically, buttons are also available in this scenario, but they require two mouse clicks (select the item, click the button). The drag-and-drop operation is more efficient here because it only requires a single motion (select and drag).

To re-create this example, add two ListBox controls to a form and set the OLEDropMode property of the second ListBox to 1—Manual. Add the following code:

Private Sub Form_Load()
    ' Populate the list
    List1.AddItem "One"
    List1.AddItem "Two"
    List1.AddItem "Three"
    List1.AddItem "Four"
    List1.AddItem "Five"
End Sub 
Private Sub List1_MouseDown(Button As Integer, Shift As Integer, X As Single, Y As Single)
    List1.OLEDrag    ' Begin dragging
End Sub
Private Sub List1_OLEStartDrag(Data As DataObject, AllowedEffects As Long)
    ' Only allow moves
    AllowedEffects = vbDropEffectMove
    ' Assign the ListBox selection to the DataObject
    Data.SetData List1
End Sub
Private Sub List2_OLEDragDrop(Data As DataObject, Effect As Long, Button As Integer, Shift As Integer, X As Single, Y As Single)
    Dim strList As String
    ' Check the format of the DataObject
    If Not Data.GetFormat(vbCFText) Then Exit Sub
    ' Retrieve the text from the DataObject
    strList = Data.GetData(vbCFText)
    List2.AddItem strList
End Sub
Private Sub List1_OLECompleteDrag(Effect As Long)
    ' Remove the item from the ListBox
    List1.RemoveItem List1.ListIndex
End Sub

In the OLEStartDrag event we are assigning the selected item in the source ListBox to the DataObject. We didn't need to specify the format; the DataObject recognizes it as text. In the OLEDragDrop event of the target ListBox we use the GetFormat method of the DataObject to make sure that it contains text—if we inadvertently dropped another type of data it would cause a run-time error. The vbCFText argument is a constant that comes from the ClipBoardConstants enumeration.

You may have noticed that we assigned the output of the GetData method to a string variable in the OLEDragDrop event. In this case it's necessary only because the ListBox requires the AddItem method; for a TextBox control we could assign the output directly:

Text1.Text = Data.GetData(vbCFText)

The techniques used in this example will work in any situation where you want to drag or drop text, whether between controls on a form, between forms, or even between your application and another application, such as Microsoft Word, that supports the drag-and-drop feature.

Dragging a Picture

Another common drag-and-drop scenario involves moving or copying a picture between two PictureBox controls. To do this via the keyboard you would need to add routines to cut and paste, resulting in more code than is necessary for drag and drop.

To re-create this example, create two forms with a PictureBox control on each. Add any bitmap to the Picture property of the PictureBox on the first form. Set the OLEDropMode property of the PictureBox on the second form to 1—Manual. Add the following code to Form1:

Private Sub Form_Load()
    Form2.Show
End Sub
Private Sub Picture1_MouseDown(Button As Integer, Shift As Integer, X As Single, Y As Single)
    Picture1.OLEDrag    ' Begin dragging
End Sub
Private Sub Picture1_OLEStartDrag(Data As DataObject, AllowedEffects As Long)
    AllowedEffects = vbDropEffectCopy Or vbDropEffectMove
    ' Assign the Picture selection to the DataObject
    Data.SetData Picture1.Picture
End Sub
Private Sub Picture1_OLECompleteDrag(Effect As Long)
    ' If the Picture was moved, remove it
    If Effect = vbDropEffectMove Then
        Picture1.Picture = LoadPicture("")
    End If
End Sub

Add the following code to Form2:

Private Sub Picture1_OLEDragOver(Data As DataObject, Effect As Long, Button As Integer, Shift As Integer, X As Single, Y As Single, State As Integer)
    ' If it isn't the right format, don't allow dropping
    If Not Data.GetFormat(vbCFDIB) Then
        Effect = vbDropEffectNone
    End If
End Sub
Private Sub Picture1_OLEDragDrop(Data As DataObject, Effect As Long, Button As Integer, Shift As Integer, X As Single, Y As Single)
    ' The Ctrl key is used to copy while dragging
    If Shift = vbCtrlMask Then
        Effect = vbDropEffectCopy
    Else
        Effect = vbDropEffectMove
    End If
    Picture1.Picture = Data.GetData(vbCFDIB)    ' Assign the picture
End Sub

The code for Form1 is similar to the ListBox example, except this time we assign the Picture property of the PictureBox control to the DataObject, and this time we'll allow either a move or copy operation.

In the code for Form2 we use the OLEDragOver event to test for the format. If it doesn't match we prevent the drop by changing the Effect argument to vbDropEffectNone. This also changes the cursor to a No Drop cursor, providing feedback to the user.

Notice that the format argument here is vbCFDIB, the Clipboard format for device-independent bitmap (DIB). Even though we are using a bitmap, internally Visual Basic converts it to a .dib. You can test this by assigning a different picture type (.gif or .jpg) to the PictureBox—the OLEDragOver event still sees the format as being vbCFDIB.

Because of this internal conversion, the techniques used in this example work fine for dragging or dropping between PictureBox controls, whether on the same form, separate forms, or separate VB applications. If you want to drag and drop images to someone else's application, you'll want to treat them as files.

Dragging Files

Perhaps the most prevalent use for the drag-and-drop feature is the ability to drag and drop files between Windows Explorer and an application. One example of an application where this capability is useful would be image viewers, where the user can drag picture files onto the application and open them.

For this example we'll use the same forms we created for the previous example—we'll just add some code to allow the PictureBox on the first form to accept dropped files. Set the OLEDropMode property of the PictureBox on Form1 to 1—Manual and add the following code to Form1:

Private Sub Picture1_OLEDragOver(Data As DataObject, Effect As Long, Button As Integer, Shift As Integer, X As Single, Y As Single, State As Integer)
    If Not Data.GetFormat(vbCFFiles) Then
        Effect = vbDropEffectNone    ' Don't allow dropping
    End If
End Sub 
Private Sub Picture1_OLEDragDrop(Data As DataObject, Effect As Long, Button As Integer, Shift As Integer, X As Single, Y As Single)
    Dim strFilename As String
    ' Get the filename from the Files collection 
    strFilename = Data.Files(1)
    ' Load the picture
    Picture1.Picture = LoadPicture(strFilename)
End Sub

The code used to drop files isn't much different from the previous examples. In this case we check for a Clipboard format of vbCFFiles. The DataObject contains a Files collection that can contain one or more items, with each item containing a string that represents a file name and path. As in the ListBox example, we need to assign the file to a string variable because we can't assign a file directly to a PictureBox.

If you wanted to allow dropping multiple files, you could add code to loop through the Files collection. Of course, you should also add error-handling code to prevent disaster should an unsupported file be dropped on the PictureBox.

It might also be useful to allow image files to be dragged from our application to Windows Explorer or an image editing application. To demonstrate this, we'll modify some of the code for Form1. Because the PictureBox doesn't store the file name, we'll need to store it ourselves in a form-level variable:

Private strFile As String

In the Form_Load event, we'll need to assign the initial file name (substitute an actual path and file name for path) and load the picture:

strFile = "path"
Picture1.Picture = LoadPicture(strFile))

We'll also need code in the OLEDragDrop event to update the file name:

strFile = strFileName

Finally, we'll need to modify the OLEStartDrag event to drag files instead of images. In this case we'll just assign the format to the SetData method, and then we'll add code to the OLESetData event to load the file:

Data.SetData , vbCFFiles
Private Sub Picture1_OLESetData(Data As DataObject, DataFormat As Integer)
    ' Add the file to the Files collection
    Data.Files.Add strFile
End Sub

The OLESetData event is only fired when a drop actually occurs, so if the drop is cancelled we've saved time and memory by not actually loading the file in the OLEStartDrag event. Note that in the call to the SetData method, a comma is used as a placeholder for the omitted argument.

If you wanted to drag multiple files you could store file names in a ListBox and loop through the list to add them to the Files collection. Of course, the techniques in this example apply to any type of files—you can even create your own file format and use the drag-and-drop feature to move or copy custom data.

Emulating Standard Drag and Drop

From the beginning Visual Basic has supported dragging and dropping controls within a single form. One of the simplest uses for drag and drop is to give the user the ability to rearrange controls on a form at run time. As mentioned earlier, this can also be done using OLE drag and drop, but it isn't obvious.

Standard drag and drop used a Source argument in the Drag method to determine which control was being dragged; the OLEStartDrag method has no such argument. We can, however, emulate the behavior by creating a form-level variable to track the control.

To re-create this example, add a CommandButton to a form, set the OLEDropMode property of the form to 1—Manual, and add the following code:

Private Source As Control
Private Sub Command1_MouseDown(Button As Integer, Shift As Integer, X As Single, Y As Single)
    ' Assign the CommandButton to the Source object variable
    Set Source = Command1
    Command1.OLEDrag
End Sub
Private Sub Command1_OLEStartDrag(Data As DataObject, AllowedEffects As Long)
    AllowedEffects = vbDropEffectMove
    Data.SetData ""    ' Assign a zero-length string
End Sub
Private Sub Form_OLEDragDrop(Data As DataObject, Effect As Long, Button As Integer, Shift As Integer, X As Single, Y As Single)
    Dim ctl As Control
    Set ctl = Source    ' Assign the Source object
    ctl.Move X, Y    ' Call the Move method of the Source
End Sub

In the MouseDown event the control is assigned to the Source object variable. The values in the OLEStartDrag event aren't important, but both lines are necessary in order to start dragging. In this case, we're just assigning an empty string to the DataObject (if we drop it somewhere else it can't cause any damage) and arbitrarily setting the AllowedEffects property to allow a move.

Notice that in the OLEDragDrop event there are no references to the DataObject—we're using the event for our own purposes. By assigning the Source variable to a local object variable, we get access to all of the CommandButton's properties and methods.

Because there is no direct reference to the CommandButton control, this same routine will work for any control on the form. This same technique can be used in the OLEDragDrop event of any control as well, providing a simple means of passing data between controls.

Thanks for Dropping In

As you can see from the examples, adding drag-and-drop capabilities isn't difficult at all. In fact, most of it is generic enough that you could add it to your form templates and user controls, making it even easier.

Most developers have standards for such things as access and shortcut keys—why not add the drag-and-drop feature to those standards? At the very least, you'll make your mousers happy.