Anton Maurovic - anton.maurovic.com

Win32 API approach to Windows Drag-and-Drop

Win32 API approach to Windows Drag-and-Drop

Maybe you can’t implement drag-and-drop in Windows without some COM code, but Anders Karlsson has a straightforward wrapper that will appeal to C/C++ coders that normally prefer the simple flavour of the Windows (Win32) API.

Background

Somehow I’d never developed a Windows project that needed a drag-and-drop implementation, until recently. I was putting together a diagnostic utility that was to inspect the Windows Shortcut of a certain legacy application. Since there was no direct way to find the shortcut I figured it would be easiest for the user to drag-drop it onto my utility’s window.

Surprisingly, besides the WM_DROPFILES legacy from Windows 3.1, formal Windows drag-and-drop support is limited to COM (or higher). However, Anders Karlsson has released a library (as simple C source) that hides the COM ugliness and gives Win32 API C coders something that feels familiar.

A Primer on Karlsson’s Drag-and-Drop C Library

Karlsson’s Drag-and-Drop C Library for Windows is a single .c file that can be added to your project, to implement a COM interface that exposes Windows drag-and-drop functionality to you as a few simple C function calls. This lets your Win32-API-based app written in straight C receive drop events via a callback function (or window message), or initate a drag operation with a few easy steps.

I’m assuming here that you’re already familiar with writing Windows apps using the Win32 API, and hence that you’re familiar with window/dialog “procs” and handling window messages. So, the overall process with Karlsson’s library is as follows:

  • Call MyDragDropInit early on (e.g. in WinMain) to initialise the library and COM/OLE).
  • To then receive drag-drop events:
    1. Use MyRegisterDragDrop with a window handle and a “format” identifier (or a list of them) to register a window that can receive drops and the format(s) it wants.
    2. Via a callback function or a window notification message, handle a drop event based on the type of data being received.
    3. Use MyRevokeDragDrop to de-register a window as a drop target, especially before the window gets destroyed (e.g. during termination of the window).
  • To initiate (i.e. send) drag-drop events:
    1. Decide on a UI event which represents a suitable time to start a drag-drop operation. It’s up to you to initiate the drag-drop in your code, but fortunately this is very simple. A common place is inside a WM_LBUTTONDOWN message handler.
    2. Call CreateMyDropSource to create an object that wraps the source data.
    3. Call MyDragDropSource or MyDragDropSourceEx to initate the drag-drop operation.
    4. Call FreeMyDropSource to release the object you created with CreateMyDropSource.

That’s it, basically, so let’s try some examples.

Simple example code to receive drop events

Start by downloading Karlsson’s Library. You only need DragAndDrop_1_0.zip; it already contains the documentation PDF.

For a very simple example Windows app, download and extract DropTest.zip (9.7 KiB). Create a Windows project and add drop_test.c and resources.rc to it. Finally, add Karlsson’s DragAndDrop.c. You should be able to run the app to display a dialog window that doesn’t yet do anything:

My dialog window. Doesn't do anything yet My dialog window. Doesn't do anything yet

Now, to prepare Karlsson’s library. First, #include it:

#include "DragAndDrop.h"

Then add this statement to your WinMain function:

MyDragDropInit(NULL);

The next objective is to register a window that will receive drop events. Registering the window will yield a handle that is used later for clean-up; so first declare a variable, in the global scope, to hold the handle:

PMYDROPTARGET g_drop_target = NULL;

Now, registering the window is usually done when handling WM_CREATE, or WM_INITDIALOG for a dialog box. Referring to my DropTest.zip example, I’ll stick with dialog boxes, but the code is essentially the same for regular windows: handle the WM_INITDIALOG message with the following code:

case WM_INITDIALOG:
{
        /* We'll register to receive Unicode plaintext only: */
        CLIPFORMAT cf_list[1] = { CF_UNICODETEXT };
        /* Register the main window to be a target for drop events,
        and request notification via a callback function called TheDropProc: */
        g_drop_target = MyRegisterDragDrop(hdialog, cf_list, 1, WM_NULL, TheDropProc, NULL);
        /* Return TRUE: System should select focus */
        return TRUE;
}

In the example above we’ve just registered for our main dialog window to receive the drop events. We’ve specified (with WM_NULL) that no window notification message will be used, and instead that a callback function called TheDropProc will be used. I’ll explain the CLIPFORMAT concept later. For now, just understand that I’ve decided that I want to receive Unicode-formatted plain text (CF_UNICODETEXT) only. For the sake of simplicity, this also assumes that your Windows project is configured to build as a Unicode app. If not, you’ll probably want to use CF_TEXT instead, to get regular old ANSI strings.

Now let’s put in the code to release the drop target when the dialog box is being closed:

case WM_CLOSE:
{
        /* Before destroying the dialog, de-register IDC_DROPTARGET as a drop target: */
        MyRevokeDragDrop(g_drop_target);
        /* Close request: Terminate the dialog box */
        EndDialog(hdialog, 0);
        return TRUE;
}

The only thing we need now is to implement the callback function:

/* Callback that receives drag-and-drop "drop" events */
static DWORD TheDropProc(CLIPFORMAT cf, HGLOBAL hdata, HWND hwnd, DWORD key_state, POINTL pt, void* param)
{
        if (CF_UNICODETEXT == cf)
        {
                /* We're receiving dropped text */
                /* Lock a pointer to the raw data: */
                void* data = GlobalLock(hdata);
                /* In CF_UNICODETEXT format, we know it's a Unicode C string: */
                MessageBox(hwnd, (LPCTSTR)data, __TEXT("Dropped Text"), MB_OK);
                /* Release the pointer: */
                GlobalUnlock(hdata);
                /* Indicate that our reaction was to copy the data: */
                return DROPEFFECT_COPY;
        }
        /* Default reaction is to do nothing: */
        return DROPEFFECT_NONE;
}

Again, replace CF_UNICODETEXT with CF_TEXT if required. Now, run the app, select some text in another application, and drag it anywhere onto the dialog window. For example, select some text from this web page and drop it onto the dialog window, and it should display that text in a standard Windows message box:

Demonstration of plain text dropped onto our window Demonstration of plain text dropped onto our window

So far we’re using the whole dialog window area as the drop target. I was caught out the first time I tried to use the white rectangle area instead: It seems the creation order of the dialog controls is important. Without going into why, open resources.rc, find the IDD_DROPTEST section, and swap around these two lines:

GROUPBOX        "Drag text, and drop it into this region:",IDC_DROPHEADING,12,12,216,96
CONTROL         "",IDC_DROPTARGET,"Static",SS_WHITERECT,18,24,204,78

If the IDC_DROPTARGET control doesn’t come first, it seems to get lost “behind” the IDC_DROPHEADING control when we try to register it as a drop target. With that now done, we can modify the WM_INITDIALOG handler code to use IDC_DROPTARGET instead of the dialog window:

case WM_INITDIALOG:
{
        /* Get the window handle of a given dialog box control. In this case,
           IDC_DROPTARGET refers to a white panel on the dialog box. */
        HWND htarget = GetDlgItem(hdialog, IDC_DROPTARGET);
        /* We'll register to receive plaintext only: */
        CLIPFORMAT cf[1] = { CF_UNICODETEXT };
        /* Register the IDC_DROPTARGET window to be a target for drop events,
           and request notification via a callback function called TheDropProc: */
        g_drop_target = MyRegisterDragDrop(htarget, cf, 1, WM_NULL, TheDropProc, NULL);
        /* Return TRUE: System should select focus */
        return TRUE;
}

Try running the app again and you’ll find that you can only drop text onto the white rectangle area.

Simple example to initiate drag events

I originally assumed that implementing the “drag” action involved registering a window as a drag source, and then leaving the user interaction part up to Windows, but this doesn’t make sense when you think about it. Based on where the user clicks (amongst other factors), they might be targeting any number of different things that are represented on the canvas of that one window. The app needs to decide for itself when to begin the drag-drop action (if at all), and what data should be included.

A simple drag action would usually begin with the user clicking the left mouse button, so we’ll do something inside a WM_LBUTTONDOWN handler. Karlsson’s library provides convenient helper functions when doing this with text, but they fail in Unicode mode because of a bug, so my example here will show you the lower level calls.

case WM_LBUTTONDOWN:
{
        /* User clicked inside our window, so initate a drag operation... */
        /* Prepare object to hold the [string] data we're going to drag: */
        char* text = "Hello, World! This is the drop source.";
        HANDLE text_on_heap;
        PMYDROPSOURCE text_drop_source;
        CLIPFORMAT cf[1] = { CF_TEXT };
        /* Allocate a heap buffer for the string (inc. null terminator). */
        text_on_heap = GlobalAlloc(GMEM_FIXED, strlen(text) + 1);
        if (NULL == text_on_heap) break;
        /* Copy source string into the heap buffer. */
        strcpy(text_on_heap, text);
        /* Wrap the heap buffer in a drop source object. */
        text_drop_source = CreateMyDropSource(FALSE, cf, &text_on_heap, 1);
        if (NULL != text_drop_source)
        {
                /* Drop source created, so begin the drag. This will block. */
                MyDragDropSource(text_drop_source);
                /* Drag-drop is done so destroy the drop source. */
                FreeMyDropSource(text_drop_source);
        }
        /* Free up the buffer; drag-drop is finished. */
        GlobalFree(text_on_heap);
        return TRUE;
}

Note that I’ve opted to use an ANSI string here, yet this example will compile and run fine in both ANSI and Unicode modes. As per the drop example above you can interchangeably use CF_TEXT and CF_UNICODETEXT, so long as your string data agrees with the specified format. That is, don’t supply an ANSI string if your CLIPFORMAT is CF_UNICODETEXT.

When you run this code, you should find that you can left-click anywhere in the window and drag to an application that receives text (which excludes “Notepad”).

Demonstration of dragging text to another window Demonstration of dragging text to another window

Now, because we control when to call MyDragDropSource, we can decide to handle WM_LBUTTONDOWN selectively. Say we want to only start a drag from within the region of the little panel that says “(This panel doesn’t do anything yet)”. The proper way might be to subclass that window and handle its own WM_LBUTTONDOWN, but for simplicity you can instead just add this code to the top of our existing WM_LBUTTONDOWN handler:

/* Get the X and Y coordinates of the point the user clicked. */
POINT pt = { LOWORD(lparam), HIWORD(lparam) };
/* Get the extents of the rectangular space occupied
   by the IDC_DROPSOURCE control: */
WINDOWPLACEMENT wp = {sizeof(wp)};
GetWindowPlacement(GetDlgItem(hdialog, IDC_DROPSOURCE), &wp);
/* Bail out if the cursor was not clicked inside that region. */
if (!PtInRect(&wp.rcNormalPosition, pt)) break;

When you run this code and click anywhere outside the region of the IDC_DROPSOURCE control, nothing will happen. Meanwhile, you can start a normal drag operation from within the region of that panel.

Note that in this example you can’t drag from the IDC_DROPSOURCE control to the IDC_DROPTARGET control. That’s where it would be better to subclass the source window, so that the whole client area doesn’t get locked out.

Multiple data types via Clipboard Formats

So far we’ve looked at how to drag-drop simple text. Many other formats are possible, and a drag source usually offers its information in multiple formats simultaneously – a concept originally from the Windows Clipboard. Dragging data from one place to another is like a more-direct copy/paste action. Providing multiple formats is important because it allows two programs to share information, to the maximum extent of their respective capabilities, without having to support all of the elaborate formats out there.

An example of multiple formats in action is when you copy richly-formatted text from (say) Wordpad: Paste into Wordpad and formatting is preserved. Paste into Notepad and you get just raw text. Notepad isn’t responsible for the conversion, and neither is Windows. Instead, Wordpad is offering its content in at least two different formats: Rich Text Format for WYSIWYG-style editors; and stripped-down plaintext (CF_TEXT) for basic programs that can’t handle anything else.

A drag source should offer its information in as many different formats as possible so that a receiving program can choose the best format out of those that it knows how to support. To see multiple formats in effect, check out ClipSpy (alternate download link).

ClipSpy reveals CLIPFORMATs from the Clipboard and Drag-and-Drop ClipSpy reveals CLIPFORMATs from the Clipboard and Drag-and-Drop

Here we see a rich text snippet that has been dragged onto ClipSpy. Several different formats, as offered by Wordpad, are detected in the one drag-drop. Besides the familiar CF_TEXT format and its Unicode counterpart, there’s also the Rich Text Format and a variety of other representations and metadata formats.

The CLIPFORMAT constants define built-in Windows formats that are very often supported. It is also possible to “register” other formats for common use: The RegisterClipboardFormat Win32 API call takes the name of a format (as a string) and assigns it a unique CLIPFORMAT number, such that two programs that agree on a common name will be able to share data in that agreed format. The names don’t imply any meaning to Windows, but there are many that are widely supported as standard:

  • Rich Text Format, as we have seen, is very common. It refers to visually-styled text that is represented using the RTF standard.
  • HTML Format is specifically the HTML Clipboard Format; a snippet of HTML, presented as a complete webpage, and including a special header that describes the original text selection.
  • FileName is a path to a file, and often included when dragging a file from Windows Explorer. It’s an ANSI string in 8.3 Format. By contrast, FileNameW (which is usually provided at the same time) is the “wide” (Unicode) equivalent, in Long filename format.
  • A number of other named formats are offered by Windows Explorer, as documented on Microsoft’s Shell Clipboard Formats page.

Offering multiple formats with Karlsson’s library

Let’s modify the “Simple example to initiate drag events” so that it will offer not only CF_TEXT, but also Rich Text Format. While we’re at it, the words in each format will be different, to prove that we’re in control.

Start by deleting the guts of the WM_LBUTTONDOWN handler, and we’ll replace it bit by bit. First, we define our raw source data and other assorted variables:

char* plaintext = "Hello there World";
char* rtf = "{\\rtf1\\i\\fs24 Hello\\i0\\fs20  \\b\\fs18 again\\b0  World}";
HANDLE heap_handles[2] = { NULL, NULL };
PMYDROPSOURCE drop_source;

Now we define the CLIPFORMATs that we’re going to offer:

CLIPFORMAT cf[2] = { CF_TEXT, RegisterClipboardFormat(__TEXT("Rich Text Format")) };

Then store our source data on the heap:

heap_handles[0] = GlobalAlloc(GMEM_FIXED, strlen(plaintext)+1);
if (NULL == heap_handles[0]) break;
strcpy(heap_handles[0], plaintext);

heap_handles[1] = GlobalAlloc(GMEM_FIXED, strlen(rtf)+1);
if (NULL == heap_handles[1])
{
        /* 2nd GlobalAlloc failed, but not the first,
           so release the first buffer then bail out. */
        GlobalFree(heap_handles[0]);
        break;
}
strcpy(heap_handles[1], rtf);

Then create the drag-drop source (this time specifying that there are 2 formats) and initate the drag:

drop_source = CreateMyDropSource(FALSE, cf, heap_handles, 2);
if (NULL != drop_source)
{
        /* Drop source created, so begin the drag. This will block. */
        MyDragDropSource(drop_source);
        /* Drag-drop is done so destroy the drop source. */
        FreeMyDropSource(drop_source);
}

Finally, clean up the global heap handles and exit the handler:

GlobalFree(heap_handles[1]);
GlobalFree(heap_handles[0]);
return TRUE;

The complete WM_LBUTTONDOWN handler should now look like this:

case WM_LBUTTONDOWN:
{
        char* plaintext = "Hello there World";
        char* rtf = "{\\rtf1\\i\\fs24 Hello\\i0\\fs20  \\b\\fs18 again\\b0  World}";
        HANDLE heap_handles[2] = { NULL, NULL };
        PMYDROPSOURCE drop_source;

        CLIPFORMAT cf[2] = { CF_TEXT, RegisterClipboardFormat(__TEXT("Rich Text Format")) };

        heap_handles[0] = GlobalAlloc(GMEM_FIXED, strlen(plaintext)+1);
        if (NULL == heap_handles[0]) break;
        strcpy(heap_handles[0], plaintext);

        heap_handles[1] = GlobalAlloc(GMEM_FIXED, strlen(rtf)+1);
        if (NULL == heap_handles[1])
        {
                /* 2nd GlobalAlloc failed, but not the first,
                   so release the first buffer then bail out. */
                GlobalFree(heap_handles[0]);
                break;
        }
        strcpy(heap_handles[1], rtf);

        drop_source = CreateMyDropSource(FALSE, cf, heap_handles, 2);
        if (NULL != drop_source)
        {
                /* Drop source created, so begin the drag. This will block. */
                MyDragDropSource(drop_source);
                /* Drag-drop is done so destroy the drop source. */
                FreeMyDropSource(drop_source);
        }

        GlobalFree(heap_handles[1]);
        GlobalFree(heap_handles[0]);
        return TRUE;
}

When you compile and run this new app, you should get different results depending on whether the drop target app supports RTF, or just plaintext:

Different text strings demonstrating multiple CLIPFORMATs Different text strings demonstrating multiple CLIPFORMATs

Note that I’ve used ANSI here (i.e. not Unicode), to keep it simple. You could offer CF_UNICODETEXT instead of CF_TEXT or better yet, offer both.

Note also that Rich Text Format uses ASCII as its base, so don’t supply it as a Unicode string.

Multiple format support when receiving a drop event

To support more than one format when receiving a drop event, we need to make two simple changes:

  • Modify the callback function to test for the received CLIPFORMAT, and handle it appropriately.
  • Modify the call to MyRegisterDragDrop to give an array of different CLIPFORMATs that we support, in order of preference.

With Karlsson’s implementation, the callback function is only ever called once for a drop event. The format supplied to the callback is whichever available format had the highest preference, as registered with MyRegisterDragDrop. So, we can change the “Simple example code to receive drop events” as follows…

First, replace the WM_INITDIALOG handler with this:

case WM_INITDIALOG:
{
        /* Get the window handle of a given dialog box control. In this case,
           IDC_DROPTARGET refers to a white panel on the dialog box. */
        HWND htarget = GetDlgItem(hdialog, IDC_DROPTARGET);
        /* We'll register to PREFER Rich Text Format, 
           but we can fall back to CF_UNICODETEXT: */
        CLIPFORMAT cf[2] = {
                RegisterClipboardFormat(__TEXT("Rich Text Format")),
                CF_UNICODETEXT
        };
        /* Register to receive whichever format is first available, out of our
           list of supported formats (in this case, there are 2 of them): */
        g_drop_target = MyRegisterDragDrop(htarget, cf, 2, WM_NULL, TheDropProc, NULL);
        /* Return TRUE: System should select focus */
        return TRUE;
}

Here we see that MyRegisterDragDrop has been called with an array of two (2) formats, with Rich Text Format as first preferece. That is, no matter what, if a drop event includes Rich Text Format then we’ll get it as our first preference. If RTF content is not included, but Unicode content is, then we’ll get that instead. If we prefer Unicode content over RTF, then we’d list CF_UNICODETEXT first in the array.

The last thing we need to do is change the callback function (TheDropProc) so that it can handle both formats:

/* Callback that receives drag-and-drop "drop" events */
static DWORD TheDropProc(CLIPFORMAT cf, HGLOBAL hdata, HWND hwnd, DWORD key_state, POINTL pt, void* param)
{
        CLIPFORMAT cf_rtf = RegisterClipboardFormat(__TEXT("Rich Text Format"));
        void* data;
        if (CF_UNICODETEXT == cf)
        {
                /* We're receiving dropped text */
                /* Lock a pointer to the raw data: */
                data = GlobalLock(hdata);
                /* In CF_UNICODETEXT format, we know it's a Unicode C string: */
                MessageBox(hwnd, (LPCTSTR)data, __TEXT("Dropped CF_UNICODETEXT"), MB_OK);
                /* Release the pointer: */
                GlobalUnlock(hdata);
                /* Indicate that our reaction was to copy the data: */
                return DROPEFFECT_COPY;
        }
        else if (cf_rtf == cf)
        {
                /* We got RTF instead. */
                data = GlobalLock(hdata);
                /* Use ANSI version of MessageBox to display the raw RTF */
                MessageBoxA(hwnd, (LPCSTR)data, "Dropped RTF", MB_OK);
                GlobalUnlock(hdata);
                return DROPEFFECT_COPY;
        }
        /* Default reaction is to do nothing: */
        return DROPEFFECT_NONE;
}

As mentioned in the previous section, Rich Text Format strings must be ASCII (i.e. not Unicode), which is why I’m cheating and using the explicit MessageBoxA (ASCII) call to display the RTF string. Of course, in a serious app you’d probably use WideCharToMultiByte instead. You’d probably also call RegisterClipboardFormat just once and store the returned CLIPFORMAT value as a global.

Unicode Bug in Karlsson’s library

Karlsson’s library includes a function called CreateMyDropSourceText. This is used by the MyDragDropText function, and is meant for creating a drop source that wraps simple text data. I would have used the MyDragDropText function in my “Simple example to initiate drag events” to make it very compact, but CreateMyDropSourceText contains a bug that manifests in Unicode mode.

The library works fine when compiled in non-Unicode mode. In Unicode mode, a buffer overrun occurs. To understand why, you can read my comment on the issue.

If you just want to patch the code to eliminate the issue, I suggest doing the following as per my comment:

  1. Change LPCTSTR to LPCSTR (i.e. remove the middle T), in both MyDragDropText and CreateMyDropSourceText. This forces the caller to supply only with ANSI (8-bit) strings, which are compatible with the CF_TEXT format used by the CreateMyDropSourceText function.
  2. Modify the prototypes for those functions accordingly, in draganddrop.h.
  3. In the CreateMyDropSourceText function, replace _tcslen with strlen, and replace _tcscpy with strcpy.

Conclusion

Karlsson’s library is quite simple but very useful, especially for those that like coding with the Win32 API in C. I’ve introduced the basics of doing drag-and-drop using this library, and hopefully here you can easily figure out a lot more. Furthermore, the code in the library is easy to understand and will help you to figure out the guts of the COM implementation, if you decide you want to.

When you download the library, it also includes a PDF with good instructions for all of the library functions. There is also an example project included which demonstrates a few more things you can do.

 

Comments

blog comments powered by Disqus