The Visual Programmer
Joshua Trupin is a software developer specializing in C/C++ and Visual Basic apps for Windows. He can be reached at firstname.lastname@example.org or email@example.com.
Click to open or copy the DRAGDROP project files.
Click to open or copy the MFC40 project files.
Click to open or copy the MSJDRAG project files.
Since I started writing the Visual Programmer column a few months ago, I've received a lot of email. One particular request has appeared more often than any other. Unfortunately, that correspondence always begins "Please provide a valid credit card number, Mr. J. Turpin (sic), or this account will be suspended." I can't write very much on that topic, but a second-favorite has emerged. People are always interested in ways to extend their programming tools, Visual Basic¨ in particular, to take better advantage of their operating system. This month, I'll demonstrate how to enable drag and drop in a Visual Basic project, which sounds easy enough since Visual Basic already provides many drag and drop capabilities. Three mechanisms enable intra-application dragging: the DragMode property, the Drag method, and the DragDrop event. You can enable any control on a Visual Basic form through its DragMode for dragging. Issuing a Drag method starts the operation, and the target control receives a DragDrop event when the operation is complete.
Much as the proverbial hand can't cut the proverbial tomato, the drag and drop mechanisms provided by Visual Basic don't cut it when it comes to inter-application dragging. If you have an OLE container control, you can set its OLEDropAllowed property; this lets you drag a file (from Windows Explorer, for instance), then drop it into the container where it becomes an embedded object. But what happens when you try to drop it in a Picture control or a form? You get the big circle/slash cursor, indicating that you're out of luck.
Enabling an application to accept drag/drop files is relatively simple. An OLE-based call, DragAcceptFiles, lets you indicate a window that will accept files, which simply means that the window will receive a WM_DROPFILES message whenever you drop a file on the window. Using a DragQueryFile call, the window then retrieves the dropped file's full path as a string.
It's easy to call DragAcceptFiles and DragQueryFile in Visual Basic since they can be declared in a module by pasting them from the API Text Viewer that ships with Visual Basic 4.0. However, there's still no way (from Visual Basic) to capture the WM_DROPFILES message sent to an existing Visual Basic form or control. Hmmm. Looks like it's time to turn to that old standby, the OLE control component.
Our OLE control, MSJDrag, lets its container accept files dropped from an outside source. For instance, if you put the control on a form, you can drag files and drop them on the form. If you put it inside a picture control (the picture control acts as a container), you can drop files on the picture control. In either case, MSJDrag generates an event every time a file is dropped on its parent. The control itself is invisible at run time, acting solely as an enabler.
To provide drag and drop functionality, MSJDrag must do three things. First, it needs to find its parent window as soon as it's created. Next, it has to enable drop operations on this parent. Finally, it has to figure out a way to capture WM_DROPFILES messages sent to this parent control. All these items are tricky but doable.
Getting an OLE Control's Parent Window
Since the functionality provided by MSJDrag is event-based, it's assumed that it works at run time only. If your control creates a window, retrieving its parent is no problem-just do a GetParent API call. In this project, we could do something simple like that, but, there might be cases where you want to get a control's parent without creating a window for the control itself. This is where understanding the underlying interfaces that support OLE controls comes in handy. Buried deep within the MFC 4.0 definition of COleControl (the base class that all OLE controls share) is the m_pInPlaceSite member variable. This is a pointer to an IOleInPlaceSite interface defined by the control's parent. IOleInPlaceSite lets you access the "site" information for the control: whether a control can be activated in-place, deactivated, and other placement related issues. IOleInPlaceSite is derived from IOleWindow, whose purpose is to provide an OLE-based way to get a window handle.
Since the control has a pointer to its container's IOleInPlaceSite, which is built on top of IOleWindow, we can walk up the list of interfaces to get the container's window through three steps in the control's C++ code. First, ask IOleInPlaceSite for a pointer to the IOleWindow it's based on. Anytime one interface is based on another, you can get a pointer to the original interface with a QueryInterface call.
m_pInPlaceSite->QueryInterface(IID_IOleWindow, (LPVOID*) &lpOleWnd);
Once you have the IOleWindow pointer, call its GetWindow member function, which returns a window handle for the container.
hwnd now contains the container's window handle, whether or not the control itself has a window. As the third step, free the current instance of IOleWindow. OLE won't free an object correctly unless it's freed each time it's used.
These three steps are put together in a function, called GetContainerHWnd, shown in Figure 1. By calling this as a member function of the MSJDrag control, you can always get the container's window handle. The second phase is setting this window to accept drag and dropped files, which is easy. The control calls DragAcceptFiles with its container's handle, turning the container into a drop client without its knowledge. Step three, somehow making the control capture its host's messages, is more challenging.
More C++, Sorry
The easiest way to grab a window's messages without its knowledge is by replacing the default message processing procedure and passing unused messages back into the original proc. Finding a window's default proc is fairly easy-you can call the GetWindowLong Windows API function, passing it the window handle and the GWL_WNDPROC flag. The function returns the address of the window procedure, which can be called with CallWindowProc when needed.
This used to be a drag to set up. If you use Microsoft Visual C++, you'll find that MFC has simplified this process dramatically. The standard MFC window-wrapping class, CWnd, provides a standard SubclassWindow method. When passed a window handle, SubclassWindow does all the work for you, rerouting all the window's messages through the particular CWnd class you've defined before sending them back to the default proc via CallWindowProc. Through the Visual C++ Class Wizard, you can pick and choose the messages you actually want to deal with.
To take this one step farther, you need to understand that when you base one class on another (known as derivation), the new class has all the functionality of the original, plus anything you add to it. In the code for the MSJDrag control, I've created a CVBFrameWnd class, based on CWnd. It has only one purpose: trapping all WM_DROPFILES messages and returning them to Visual Basic, via the MSJDrag control, as an event. When it's not receiving WM_DROPFILES messages, it's doing all the default CWnd things, such as using CallWindowProc to make sure all other messages are handled properly.
class CVBFrameWnd: public CWnd
In the OLE section of this demonstration, I got the OLE control's parent window. Now I can create an unused instance of CVBFrameWnd and attach the parent window to it.
The declaration of m_framewnd is in the class definition of CMSJDragCtrl. When MSJDrag is first created (in its OnCreate member function), it does all the work in setting up the link to its container window (see Figure 2). At this point, the parent window has been subclassed-all the messages hurtling towards it are tackled by MFC and routed through the m_framewnd object. If you want to see all the messages that go through this container, you could subclass its WindowProc function. Fortunately, you're only concerned with one message: WM_DROPFILES. Everything else is extraneous and is handled by default.
When a WM_DROPFILES message is received, the CVBFrameWnd diverter class goes to work. Its OnDropFiles member function is called, which does the real work of the application. OnDropFiles calls the Windows API function DragQueryFile, asking for file number -1 (0xFFFF). This call returns the total number of files being dragged. For convenience, this control handles the first file dragged only, so this step is actually of little use (it's there for demonstration purposes).
DragQueryFile then retrieves the name of the first file. (Since the file list is zero-based, a value of 0 retrieves the first file.) This one call gives all the information you need to generate the drop message in Visual Basic; at this point, lpszFile contains the full pathname of the dragged file. Call DragFinish to end the drag operation and fire off a FileDrop event in Visual Basic.
void CVBFrameWnd::OnDropFiles(HDROP hDropInfo)
::DragQueryFile(hDropInfo, 0, lpszFile, 127);
When you define an event in an OLE control, a standard mechanism allows you to fire this event in Visual Basic whenever you want. The main OLE control window class has a method called FireXxx, where Xxx is the name of the event you're calling. For example, the MSJDrag control supports an event named FileDrop, which generates an event like this in Visual Basic:
If you call FireFileDrop(hWnd, FName) from the C++ OLE control code, CtrlName_FileDrop will be generated. When I created the CVBFrameWnd, I added a member, named m_ctrlwnd, that is a pointer to the OLE control class. Therefore, the last step of the WM_DROPFILES handler is to call m_ctrlwnd->FireFileDrop(m_hWnd, lpszFile). When this is called, the control's work is finished and the Visual Basic portion of our program begins.
The Visual Basic Code
So we've done a bit of work with the OLE control. The MSJDrag control is a good example of a component that makes life easier by insulating Visual Basic-based code from the Windows API. In fact, the code required to accept a dragged file is reduced to a single line.
The DragDrop Visual Basic project shows how easy MSJDrag is to use. DragDrop consists of a single form, one PictureBox on the form, and an MSJDrag control placed on the picture box. (A PictureBox control can act as a container, which makes this work. An Image control wouldn't let you do this.) When you run the project, the MSJDrag control handles dropped files by setting the PictureBox control's picture to the file that's been dropped on it. This takes one line of code.
Private Sub MSJDrag1_FileDrop(ByVal hWnd As Long,
ByVal FName As String)
Picture1.Picture = LoadPicture(FName)
The LoadPicture function in Visual Basic loads a valid bitmap from a file. The line of code sets the PictureBox's picture to the bitmap referred to by FName. If FName doesn't point to a valid bitmap, the LoadPicture function returns an empty picture. Immediately after a file drag, if all goes well, the program will look like the illustration in Figure 3 (your bitmap may vary).
Figure 3 Using the MSJDrag control in DragDrop
There's a big demand for OLE controls out there. The demand should only increase as they become an integral part of Microsoft's Internet strategy. Internet Explorer 3.0 and Visual Basic Script will support embedded OLE controls; these controls will be most successful when they tend towards smaller size and lighter functionality, making them easily downloadable. MSJDrag is an example of a small control that makes an otherwise difficult task easy.
Have a question about programming in Visual Basic, Visual FoxPro, Access, Office or stuff like that? Mail it directly to The Visual Programmer, Microsoft Systems Journal, 825 Eighth Avenue, 18th Floor, New York, New York 10019, or send it to MSJ (re: Visual Programmer) via:
From the May 1996 issue of Microsoft Systems Journal.