Simple Drag and Drop for List Boxes in 32-Bit Visual C++ Applications

Nigel Thompson
Microsoft Developer Network Technology Group

Created: October 17, 1994

Click to open or copy the files in the DDLIST sample application for this technical article.

Abstract

This article shows a simple way to add drag-and-drop support to list boxes in 32-bit applications based on the Microsoft® Foundation Class Library (MFC). The sample code included in the DDLIST application supports both Clipboard and drag-and-drop support in a multiple-selection list box. Drag and drop is also supported between different instances of the application. For details on how the Clipboard implementation works, see "MFC Tips for Copying, Pasting, Blting, and Creating Owner-Drawn List Boxes."

Introduction

I have a bit of a thing about list boxes. It seems that every application I want to write for myself involves list boxes. I create all my applications using Visual C++™ on a Microsoft® Windows NT™ machine, and it was because of this need for lists that I found out how to implement Clipboard support in a Microsoft Foundation Class Library (MFC) application. I soon tired of the "Select, CTRL+C, Select, CTRL+V" sequence and decided to implement drag and drop to improve the interface a bit.

Now, I've done some drag-and-drop stuff before. Some of you may have read the "OLE for Idiots" series of articles written by my good friend Herman Rodent. In that series, we used OLE to implement drag and drop. Or, more correctly, in investigating OLE, we played with drag and drop. So my first idea was to implement list box drag-and-drop support using OLE. Then I went and had a brief lie down and decided that I could do the entire thing myself without using OLE and with a lot less pain. The result is a small set of functions and a new list box class, which show how you, too, can implement drag and drop without the need to OLE-enable your application. Having said that, I'm not suggesting this is better than using OLE—just different. So you can choose how you want to implement it.

The DDLIST sample application that accompanies this article is a multiple document interface (MDI) application based on MFC's document/view architecture. Each view is simply a multiple-selection list box. You can move items between views in one application or from a view in one application to a view in another application using either the Clipboard or drag and drop. I've improved the Clipboard support from the code I developed in "MFC Tips for Copying, Pasting, Blting, and Creating Owner-Drawn List Boxes" by creating a few simple helper functions. I've derived a new class from CListBox to implement the drag-and-drop support and again, packaged the grunt work into a few simple helper functions. I won't claim this is the definitive implementation, but it certainly provided the support I needed, which is more than I can say for my office chair.

How to Get There from Here

Inasmuch as a simple map is often infinitely better than pages of complex driving directions, I thought we'd start with a list of the steps I took to add drag-and-drop support to the list boxes in my application.

  1. Build the basic application without Clipboard or drag-and-drop support.

  2. Add Clipboard support for the list boxes and test it.

  3. Derive a new class from CListBox to which the drag-and-drop support code will be added. The new class has no member functions yet. Build the application and test to see that the Clipboard support still works.

  4. Add handlers to the new list box class for left-mouse-button up and down events and for mouse-movement events.

  5. Reimplement the list box selection logic, and build in a test for the start of a drag operation.

  6. Register some private messages to be used for initiating a drag-and-drop operation, testing a potential drop site, and executing a drop on a drop site.

  7. Implement the QueryDrop functionality to see if a potential drop site will accept a drop. (You don't need any data for this yet.)

  8. Implement the DoDrop functionality, and test, test, test.

I'm going to assume you can manage to get through steps 1 and 2 on your own, so I'll pick up the story with steps 3 and 4.

A New List Box Class: CDDListBox

CDDListBox is derived publicly from CListBox, and its purpose in life is to handle mouse events that would normally be handled directly by CListBox. We need to handle mouse events ourselves in order to know when a drag operation needs to be started. There are many different models we could use for this, and inasmuch as I had no particular desire to reinvent any wheels, I chose to mimic the model that the Microsoft Windows® File Manager uses. I spent a good deal of time playing with the File Manager in order to understand how drag operations are started and, in particular, how they are started when multiple selections are involved. If your list boxes are single-selection, most of the complexity goes away, but because multiple-selection list boxes are what I wanted, I had to go the whole nine yards and implement a reasonable selection model.

CDDListBox begins life with just three main functions to handle mouse events. The following code is taken from DDLSTBOX.H and shows part of the class definition:

class CDDListBox : public CListBox
{
public:
    CDDListBox();
    virtual ~CDDListBox();
   ...
   ...
    // Generated message map functions
protected:
    //{{AFX_MSG(CDDListBox)
    afx_msg void OnLButtonDown(UINT nFlags, CPoint point);
    afx_msg void OnLButtonUp(UINT nFlags, CPoint point);
    afx_msg void OnMouseMove(UINT nFlags, CPoint point);
//}}AFX_MSG
    DECLARE_MESSAGE_MAP()
};

The mouse-event message entries were added with ClassWizard.

Implementing the Selection Logic

The essentials of implementing the selection logic are to detect key down plus movement as the start of a drag operation and to use the key-up event to do some selection steps rather than the key-down event. We can't always change selection state on key-down events because the user might be trying to drag an existing selection set. So here are the handlers for the three relevant events:

void CDDListBox::OnLButtonDown(UINT nFlags, CPoint point)
{
    // Get the index value of the item under the mouse.
    int iSel = IndexFromPoint(point);
    // If no item was hit, there is nothing to do.
    if (iSel == LB_ERR) return;
    SetCapture();
    m_bCaptured = TRUE;
    // Save the position of the mouse.
    m_ptMouseDown = point;
    // If the item is already selected, defer the operation until the 
    // mouse button goes up because this might be the start of a drag operation.
    if (GetSel(iSel)) {
        m_bMouseOpPending = TRUE;
    } else {
        m_bMouseOpPending = FALSE;
        UpdateSelection(iSel, nFlags, point);
    }
}

void CDDListBox::OnLButtonUp(UINT nFlags, CPoint point)
{
    if (m_bCaptured) {
        // See if a mouse operation is pending.
        if (m_bMouseOpPending == TRUE) {
            int iSel = IndexFromPoint(point);
            if (iSel != LB_ERR) {
                UpdateSelection(iSel, nFlags, point);
            }
        }
        m_bMouseOpPending = FALSE;
        ReleaseCapture();
        m_bCaptured = FALSE;
    }
}

void CDDListBox::OnMouseMove(UINT nFlags, CPoint point)
{
    if (m_bCaptured) {
        // See if the mouse has moved far enough to start
        // a drag operation.
        if ((abs(point.x - m_ptMouseDown.x) > 3)
        || (abs(point.y - m_ptMouseDown.y) > 3)) {
            // Release the mouse capture.
            ReleaseCapture();
            m_bCaptured = FALSE;
            // Tell the parent window to begin a drag-and-drop operation.
            GetParent()->PostMessage(ddcMsgBeginDragDrop, 
                                     (WPARAM) GetDlgCtrlID(),
                                     (LPARAM) this);
        }
    }
}

The important thing to look at is how a drag operation is started. I chose to send a message to the list box's parent to inform it that it should begin a drag operation. This makes more sense than having the list box itself control the operation because the list box is a rather generic object and drag and drop is very specific to the application.

The ddcMsgBeginDragDrop message is a Windows message that is registered along with some other messages as the application starts up. The code for this is in DDCLIP.CPP:

// Messages 
UINT ddcMsgQueryDrop = ::RegisterWindowMessage("DDCQUERYDROP");
UINT ddcMsgDoDrop = ::RegisterWindowMessage("DDCDODROP");
UINT ddcMsgBeginDragDrop = ::RegisterWindowMessage("DDCBEGINDRAGDROP");

Note   This code is not part of any function, but simply a set of initialization instructions to be performed before the application starts running.

We'll see shortly what the view window that owns the list box does when it receives the ddcMsgBeginDragDrop message. Just before we do that, it's worth looking at how you add a handler for a registered Windows message because ClassWizard can't do this for you. Here's a section of the definition of the view class in DDLISTVW.H that defines the message handlers:

class CDdlistView : public CView
{
   ...
// Generated message map functions
protected:
    //{{AFX_MSG(CDdlistView)
    ...
    afx_msg LRESULT OnQueryDrop(WPARAM wParam,LPARAM lParam);
    afx_msg LRESULT OnDoDrop(WPARAM wParam,LPARAM lParam);
    afx_msg LRESULT OnBeginDragDrop(WPARAM wParam,LPARAM lParam);
    //}}AFX_MSG
    DECLARE_MESSAGE_MAP()
};

The message handlers are registered as being very generic—they each have a wParam and lParam argument and return an LRESULT.

The implementation of the view class is in DDLISTVW.CPP. Here's the part of the message map that deals with the registered messages:

BEGIN_MESSAGE_MAP(CDdlistView, CView)
    //{{AFX_MSG_MAP(CDdlistView)
    ...
    ON_REGISTERED_MESSAGE(ddcMsgQueryDrop, OnQueryDrop)
    ON_REGISTERED_MESSAGE(ddcMsgDoDrop, OnDoDrop)
    ON_REGISTERED_MESSAGE(ddcMsgBeginDragDrop, OnBeginDragDrop)
    //}}AFX_MSG_MAP
END_MESSAGE_MAP()

So now we can look at the implementation of the OnBeginDragDrop function:

LRESULT CDdlistView::OnBeginDragDrop(WPARAM wParam,LPARAM lParam)
{
    // wParam is child window ID.
    // lParam is pointer to the window object.

    // Construct a drag-and-drop object from our data.
    // Get the number of selected items.
    int iCount = m_wndList.GetSelCount();
    if (iCount <= 0) return 0; // nothing to drag
    // Get the list of selection IDs.
    int* pItems = new int [iCount];
    m_wndList.GetSelItems(iCount, pItems);
    // Create a string list.
    CStrList StrList;
    // Add all the selected items to the list.
    int i;
    for (i=0; i<iCount; i++) {
        CString* pStr = new CString;
        m_wndList.GetText(pItems[i], *pStr);
        StrList.AddTail(pStr);
    }
    // Done with item list.
    delete pItems;
    ::BeginDragDrop((CWnd*)lParam, // Source window
                    theApp.m_uiStrListClipFormat, // Format
                    &StrList); // object
    // Nuke the list.
    StrList.DeleteAll();
    return 0;
}

Most of the code in this function is concerned with creating a list of the selected item IDs and then creating a list object that contains all of the selected objects. Once we have a list object, we can call the BeginDragDrop helper function to start the drag-and-drop operation.

The Drag-and-Drop Operation

I packaged all the actual drag-and-drop code into a single code module, DDCLIP.CPP. The module contains a number of global helper functions and a window class that is used to receive mouse messages during a drag-and-drop operation. Let's go through the drag-and-drop sequence, looking at each function as it's used. We'll begin with the BeginDragDrop function:

void BeginDragDrop(CWnd* pSourceWnd, UINT uiFmt, CObject* pObject)
{
    // Serialize the object to a file.
    // Get the directory for temp files.
    char szPath[_MAX_PATH];
    ::GetTempPath(sizeof(szPath), szPath);
    // Create the full filename.
    strcat(szPath, MAPFILENAME);
    // Create a new file.
    CFile DataFile(szPath, CFile::modeCreate|CFile::modeReadWrite);
    CArchive ar(&DataFile, CArchive::store);  
    pObject->Serialize(ar);
    ar.Close(); // Flush and close.
    DataFile.Close();
    // Capture the mouse so we can control what happens next.
    CDdcWnd* pWnd = new CDdcWnd(pSourceWnd, uiFmt);
    ASSERT(pWnd);
}

We create a file in the machine's temporary files directory and archive the object to the file. This is necessary in order to be able to drag the object into another process address space. We can't simply keep the object in the source process's local address space and send a pointer in the drop message because the pointer would not be valid in any other process.

Once the object has been serialized to the file, the file is closed, and a special window is created to handle mouse events while the drag-and-drop operation is performed. Let's look next at the window's constructor:

CDdcWnd::CDdcWnd(CWnd* pSourceWnd, UINT uiFmt)
{
    // Save the parameters.
    ASSERT(pSourceWnd);
    m_hwndSource = pSourceWnd->m_hWnd; 
    m_uiFmt = uiFmt;
    // Create the window.
    if (!szWndClass) {
        szWndClass = AfxRegisterWndClass(WS_POPUP);
    }
    CreateEx(0, szWndClass, "", WS_POPUP, 0, 0, 0, 0, NULL, NULL);
    m_hwndUnder = NULL;
    // Load the cursors.
    m_hcurNoDrop = ::LoadCursor(AfxGetResourceHandle(),
                                MAKEINTRESOURCE(IDC_DDCNODROP));
    m_hcurDropOK = ::LoadCursor(AfxGetResourceHandle(),
                                MAKEINTRESOURCE(IDC_DDCDROPOK));

    // Save the existing cursor.
    m_hcurOld = ::SetCursor(NULL);
    ::SetCursor(m_hcurOld);
    // Capture the mouse.
    m_hwndOldCapture = ::GetCapture();
    SetCapture();
}

The source window (the place the drag operation was started) is saved, and the mouse handler window is created. The existing cursor is saved, and the mouse is captured so that all further mouse events come to the mouse handler window. Let's see what happens as the mouse is moved:

void CDdcWnd::OnMouseMove(UINT nFlags, CPoint point)
{
    // Convert the mouse coordinates to screen coordinates.
    ClientToScreen(&point);
    // Find the window it's over.
    CWnd* pWnd = WindowFromPoint(point);
    // Note: pWnd is temporary so don't store it!
    if (!pWnd) {
        ::SetCursor(m_hcurNoDrop);
        m_hwndUnder = NULL;
        return;
    }
    HWND hWnd = pWnd->m_hWnd;
    // See if this is a new window.
    if (hWnd == m_hwndUnder) {
        // No, it isn't, so don't do anything.
        return;
    }
    // If this is the source window, show the OK to drop cursor.
    // See if this window will accept a drop.
    // The window will set the cursor if it wants drop, etc.
    if ((hWnd == m_hwndSource)
    || (pWnd->SendMessage(ddcMsgQueryDrop, m_uiFmt, 0))) {
        ::SetCursor(m_hcurDropOK);
    } else {
        ::SetCursor(m_hcurNoDrop);
    }
    m_hwndUnder = hWnd;
}

The window under the mouse position is found, and a test is made to see if this is a new window or one we were over already. This avoids redundantly testing a given window multiple times. If it's a new window, the window is sent a message to see if it would accept a drop operation. If the window responds with TRUE, the cursor is changed to show that a drop can occur here. If the Window responds FALSE (or doesn't process the message), the cursor is changed to show that no drop operation is valid here.

Obviously, there are all sort of variations you can do here, but this is simple.

The DDLIST sample application simply tests the data type to see if it will accept the drop (DDLISTVW.CPP):

LRESULT CDdlistView::OnQueryDrop(WPARAM wParam,LPARAM lParam)
{
    // wParam has the format.
    if (wParam == theApp.m_uiStrListClipFormat) {
        return TRUE;
    }
    return FALSE;
}

Finally, the user releases the mouse button to end the drag operation:

void CDdcWnd::OnLButtonUp(UINT nFlags, CPoint point)
{
    // Don't drop on the source window.
    if ((m_hwndUnder) 
    && (m_hwndUnder != m_hwndSource)) {

        // See if we can drop here.
        if (::SendMessage(m_hwndUnder, ddcMsgQueryDrop, m_uiFmt, 0)) {
            // Yes
            ::SendMessage(m_hwndUnder,
                          ddcMsgDoDrop,
                          m_uiFmt,
                          (LPARAM)0);
        }
    }
    m_hwndUnder = NULL;
    // Restore the cursor, etc.
    ::SetCursor(m_hcurOld);
    ReleaseCapture();
    if (m_hwndOldCapture) {
        ::SetCapture(m_hwndOldCapture); 
    }   
    DestroyWindow();
    delete this;
}

The window is again tested to see if a drop is valid, and if so, the target window is sent a ddcMsgDoDrop message. Notice that there is no data pointer in the message. Here's how DDLIST handles the message in DDLSTVW.CPP:

LRESULT CDdlistView::OnDoDrop(WPARAM wParam, LPARAM lParam)
{
    // wParam has the format.
    // We only accept one format, so check that's what we have.
    ASSERT(wParam == theApp.m_uiStrListClipFormat);
    // Create an object to receive the data.
    CStrList PasteList;
    ::GetDropData(&PasteList);

    // Add all the strings to the doc.
    CDdlistDoc* pDoc = GetDocument();
    ASSERT(pDoc);
    CStrList* pStrList = &pDoc->m_StrList;
    ASSERT(pStrList);
    POSITION pos = NULL;
    // Use the data in the list to update the current info.
    while (! PasteList.IsEmpty()) {
        CString* pStr = PasteList.RemoveHead();
        ASSERT(pStr);
        pStrList->AddTail(pStr);
    }
    GetParentFrame()->SetActiveView(this); // Make this the active child.
    pDoc->SetModifiedFlag();
    pDoc->UpdateAllViews(NULL);
    return 0;
}

This is almost a complete mirror of the steps used to start the drag operation: The GetDropData helper function is used to retrieve the object ,and the object's contents are then added to the document. Finally, the view is repainted to show the new information. Let's see how GetDropData works:

BOOL GetDropData(CObject* pObject)
{
    ASSERT(pObject);
    // Open the temp file.
    char szPath[_MAX_PATH];
    ::GetTempPath(sizeof(szPath), szPath);
    // Create the full filename.
    strcat(szPath, MAPFILENAME);
    CFile DataFile(szPath, CFile::modeRead);
    // Create the archive and get the data.
    CArchive ar(&DataFile, CArchive::load);  
    pObject->Serialize(ar);
    ar.Close();
    DataFile.Close();
    return TRUE;
}

The temporary file is opened, and the object is serialized from the file. The temporary file is then closed. I didn't bother to delete the file because it gets reused a lot in my own application, and it's always truncated to zero length when it's opened anyway.

Summary

If you want some simple drag-and-drop functionality without having to resort to adding OLE support to your application, you should be able to use this example as a good starting point.