Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / MFC

Drag and Drop Listbox Items using OLE

4.58/5 (10 votes)
9 Dec 2005CPOL5 min read 1   2.8K  
Rearrange listbox items using drag and drop using OLE.

Drag and Drop Listbox

Introduction

OLE has made Drag and Drop a piece of cake. It allows two unrelated applications to exchange data in a format that they can both understand, with the help of clipboard formats. Using OLE for drag and drop is a relatively simple task to accomplish. Using MFC, there really isn't much to do but create the appropriate objects and call their appropriate methods. There are three MFC classes that are involved in a Drag and Drop operation. These classes are COleDataSource, COleDropSource, and COleDropTarget.

Background

Here is a quick explanation of what each of these classes do. COleDataSource is the class that holds the data that will be transferred from the source to the destination. COleDropSource is a relatively small class that gives visual feedback during the Drag and Drop operation. And finally, COleDropTarget is the class that handles everything on the destination side of things. In reality, most people will only use COleDataSource and COleDropTarget, since most people don't need to change the behavior of COleDropSource, and it is automatically created by the COleDataSource.

The drag and drop operation consists of the source object creating a COleDataSource, attaching some data to it, and calling the DoDragDrop method of the COleDataSource object. The destination object would have to implement a COleDropTarget class. The COleDropTarget class has a handful of virtual functions that get called during the operation: OnDragEnter, OnDragOver, OnDragLeave, and OnDrop are the most important ones. These methods must be overwritten so that we can tell the system what to do.

Let's take a look at the source for a minute. Here we are going to create a COleDataSource and attach some text to it.

void CMyDragAndDropWnd::StartDrag()
{
   //create the COleDataSource, and attach the data to it
   COleDataSource DataSource;
 
   //create a chunck of memory that will hold "This is a test" 15 chars
   HGLOBAL hData = GlobalAlloc(GMEM_MOVEABLE,15);
   char *pChar = (char *)GlobalLock(hData);
   strcpy(pChar,"This is a test");
   GlobalUnlock(hData);
 
   if (hData)
   {
      //attach the data to the COleDataSource Object
      DataSource.CacheGlobalData(CF_TEXT,hData);
 
      //allow the user to drag it.
      DROPEFFECT DropEffect = 
         DataSource.DoDragDrop(GetDragItemEffects(m_DraggedIndex));
 
      //Once DoDragDrop returns we can check the return value stored in DropEffect
      //to see what kind of dropping happened. Like move, copy or shortcut
   }
}

The 'dropping' side of things is a bit more involved than the drag part. What we have to do is inherit from a COleDropTarget and overwrite some methods. The window that wants to receive drops needs to have an object of the inherited class, and register itself with it using the Register method. Let's take a look at what a simple class inherited from the COleDropTarget would look like.

//this method is called when the Drag operation first enters the window
//attached to this COleDropTarget object
DROPEFFECT CMyDropTarget::OnDragEnter(CWnd* pWnd, 
           COleDataObject* pDataObject, 
           DWORD dwKeyState,CPoint point)
{
   //if the data is the kind we want
   if (pDataObject->IsDataAvailable(CF_TEXT))
   {
      //here we can take bring window to top
      //since what is attached to the COleDropTarget object is
      //most likely a child window, you would probably have to 
      //send it a message so that it can activate it's parent 
      //or something.
      SendMessage(m_hWnd,...,...,...);

      //we can handle copy and move of this data
      return DROPEFFECT_COPY|DROPEFFECT_MOVE;
   }
   //we can't handle this type of data
   return DROPEFFECT_NONE;
}

DROPEFFECT CMyDropTarget::OnDragOver(CWnd* pWnd, 
           COleDataObject* pDataObject, 
           DWORD dwKeyState,CPoint point)
{
   //if the data is the kind we want
   if (pDataObject->IsDataAvailable(CF_TEXT))
   {
      //we can handle copy and move of this data
      return DROPEFFECT_COPY|DROPEFFECT_MOVE;
   }
   
   //we can't handle this type of data
   return DROPEFFECT_NONE;
}

void CMyDropTarget::OnDragLeave(CWnd* pWnd)
{
   //we can use this method to do any kind of clean up
   //in this case we don't have anything
}

BOOL CMyDropTarget::OnDrop(CWnd* pWnd, 
                    COleDataObject* pDataObject, 
                    DROPEFFECT dropEffect,CPoint point)
{
   BOOL Ret = FALSE;
   
   //if it is a format that we handle
   if (pDataObject->IsDataAvailable(CF_TEXT))
   {
      //grab the data 
      HGLOBAL hGlobal = pDataObject->GetGlobalData(m_cfFormat);
   
      //Do something with the data
      //here you would normally send a message back to the 
      //window registered to the COleDropTarget to tell it
      //to do something with the data
      SendMessage(m_hWnd,WM_DOSOMETHING,hGlobal,0);
   
      Ret = TRUE;
   }
   
   return Ret;
}

That’s all there is to it. Now, let’s take a look at my Drag and Drop ListBox example.

How it works

This listbox class demonstrates how to rearrange listbox items, and also drag and drop items between two listboxes utilizing OLE's Drag and Drop functionality. The concept is very simple: get the item that the user wants to drag, create a COleDataSource object, and attach data to it and call COleDataSource.DoDragDrop(). On the receiving end of things, indicate where the drop will be when the user is dragging the item around, by overriding the OnDragOver method of COleDropTarget, and finally insert the item in its new location once the user releases the mouse button which calls the OnDrop method of the COleDropTarget.

To accomplish this task, I created a new class COleDragAndDropListBox which inherits from CListBox and COleDropTarget. I like this method better than the containment method, since this way, the COleDropTarget derived class doesn't have to send messages back to the CListBox derived class to get information and notify of the drop operation. (The multi-inheritance part is not allowed in VC++ 6.0 and older, so this will only work when compiled using the VC++ 7.0 compiler.)

Let's start with the dragging part of the task. We need to catch three messages for your listbox window to do this, WM_LBUTTONDOWN, WM_MOUSEMOVE, and WM_LBUTTONUP.

The WM_LBUTTONDOWN handler method simply determines which item the user has selected.

void COLEDragAndDropListBox::OnLButtonDown(UINT nFlags, CPoint point)
{
   __super::OnLButtonDown(nFlags, point);

   //keep track of the item that was clicked on
   //WM_MOUSEMOVE message is going to use that to 
   //create the COleDataSource
   m_Interval = 0;
   m_DropIndex = LB_ERR;
   m_DraggedIndex = LB_ERR;

   BOOL Outside;
   int Index = ItemFromPoint(point,Outside);
   if (Index != LB_ERR && !Outside)
   {
      m_DraggedIndex = Index;
      SetCurSel(Index);
   }
}

The WM_WMMOUSEMOVE handler's only job is to create a COleDataSource, attach data to it, and call DoDragDrop. Since not everyone would simply transfer text from one listbox to another, the CDragAndDropListBox class has defined some virtual functions for getting the available drop modes, getting data, and dropping items. When an event happens that needs something, one of these virtual methods is used to get the information it needs. The OnMouseMove is the first one to use these virtuals. It calls GetData() to get the data that should be attached to the COleDataSource, and later after the the call to DoDragDrop(), it calls RemoveItem() to remove the item if the operation was a move operation.

void COLEDragAndDropListBox::OnMouseMove(UINT nFlags, CPoint point)
{
   if (m_DraggedIndex != LB_ERR && (nFlags & MK_LBUTTON))
   {
      //create the COleDataSource, and attach the data to it
      COleDataSource DataSource;
      HGLOBAL hData = GetData(m_DraggedIndex);
      if (hData)
      {
         //attach the data
         DataSource.CacheGlobalData(m_cfFormat,hData);
   
         //allow the user to drag it.
         DROPEFFECT DropEffect = 
                DataSource.DoDragDrop(GetDragItemEffects(m_DraggedIndex));
   
         //if the user wanted to move the item then delete it
         //Only do this if it was dragged to another window
         //OnDrop handles deleting a moved item within the same window
         if (DropEffect & DROPEFFECT_MOVE && m_DraggedIndex != LB_ERR)
         {
            RemoveItem(m_DraggedIndex);
         }
         m_DraggedIndex = LB_ERR;
         GetParent()->SendMessage(WM_COMMAND, MAKEWPARAM(GetDlgCtrlID(), 
                                  LBN_SELCHANGE),(LPARAM)CListBox::m_hWnd);
      }
   }
   
   __super::OnMouseMove(nFlags, point);
}

The WM_LBUTTONUP message handler's job is very simple. It simply sets all the variables to their initial state for the next drag operation.

void COLEDragAndDropListBox::OnLButtonUp(UINT nFlags, CPoint point)
{
   //cancel everything
   KillTimer(TID_SCROLLDOWN);
   KillTimer(TID_SCROLLUP);
   m_Interval = 0;
   m_DropIndex = LB_ERR;
   Invalidate();

   __super::OnLButtonUp(nFlags, point);
}

The COleDropTarget class that COLEDragAndDropListBox inherits from helps us handle the dropping side of things. This class has four virtual functions that we would be interested in: OnDragEnter, OnDragOver, OnDragLeave, and OnDrop. Our implementation of OnDragEnter and OnDragOver are almost identical. The only difference is that the OnDragEnter gives the class an opportunity to activate a window by calling the ActivateWindow virtual method.

DROPEFFECT COLEDragAndDropListBox::OnDragEnter(CWnd* pWnd, 
                COleDataObject* pDataObject,DWORD dwKeyState, CPoint point)
{
   if (DragOriginateInSameWindow() && !GetCanInternalDrop())
   {
      return DROPEFFECT_NONE;
   }
   //if the data is the kind we want
   if (pDataObject->IsDataAvailable(m_cfFormat))
   {
      //bring window to top
      ActivateWindow();
      //draw the line where it would be inserted
      DrawTheLines(GetItemAt(point));
      //scroll if needed
      DoTheScrolling(point);
      //return how the user can drop the item
      return GetDropItemEffects(pDataObject,dwKeyState);
   }
   return DROPEFFECT_NONE;
}

DROPEFFECT COLEDragAndDropListBox::OnDragOver(CWnd* pWnd, 
           COleDataObject* pDataObject,DWORD dwKeyState,CPoint point)
{
   if (DragOriginateInSameWindow() && !GetCanInternalDrop())
   {
      return DROPEFFECT_NONE;
   }
   //if the data is the kind we want
   if (pDataObject->IsDataAvailable(m_cfFormat))
   {
      //draw the line where it would be inserted
      DrawTheLines(GetItemAt(point));
      //scroll if needed
      DoTheScrolling(point);
      //return how the user can drop the item
      return GetDropItemEffects(pDataObject,dwKeyState);
   }
   return DROPEFFECT_NONE;
}

The OnDragLeave method is even simpler. All it does is a bit of clean up. Since the OnDragOver method drew a line on the listbox indicating the insertion point, the OnDragLeave will handle erasing this line in the event that the mouse leaves the window during the drag operation.

void COLEDragAndDropListBox::OnDragLeave(CWnd* pWnd)
{
   //cancel everything
   KillTimer(TID_SCROLLDOWN);
   KillTimer(TID_SCROLLUP);
   //Clear The line that was drawn
   CDC *pDC = GetDC(); 
   ClearOldLine(pDC,m_DropIndex);
   ReleaseDC(pDC);
   m_Interval = 0;
   m_DropIndex = LB_ERR;
}

The OnDrop method is the most extensive method in this class. As its name implies, it gets called when the user releases the mouse button over the window. In general, this would be very simple, but since the user can drop within the same window that originated the drag and drop operation, we have to handle some special case. The special case is if the user wants to move an item from one index to another. This can be tricky because the index will change when we add or delete items out of a listbox and we can easily lose the index of where we need to insert or delete items.

BOOL COLEDragAndDropListBox::OnDrop(CWnd* pWnd, 
          COleDataObject* pDataObject,DROPEFFECT dropEffect,CPoint point)
{
   BOOL Ret = FALSE;
   if (DragOriginateInSameWindow() && !GetCanInternalDrop())
   {
      return FALSE;
   }
   //if it is a format that we handle
   if (pDataObject->IsDataAvailable(m_cfFormat))
   {
      //get the index of where the user want's to insert item
      int Index = GetItemAt(point);
      HGLOBAL hGlobal = pDataObject->GetGlobalData(m_cfFormat);

      //if the Drag was initiated in the same window as the drop and
      //user wants to move the item then we have to handle the delete here
      //because if the original item was at a higher index than the dropped index
      //we would have to delete the dragged item first before we
      //insert the new item
      if (DragOriginateInSameWindow() && dropEffect == DROPEFFECT_MOVE)
      {
         if (m_DraggedIndex < Index)
         {
            Ret = DroppedAt(Index,hGlobal);
            RemoveItem(m_DraggedIndex);
            m_DraggedIndex = LB_ERR;
            SetCurSel(Index-1);
         }
         else if (m_DraggedIndex > Index)
         {
            RemoveItem(m_DraggedIndex);
            m_DraggedIndex = LB_ERR;
            Ret = DroppedAt(Index,hGlobal);
         }
      }
      else //simply drop the item in the desired index
      {
         Ret = DroppedAt(Index,hGlobal);
         GetParent()->SendMessage(WM_COMMAND, 
               MAKEWPARAM(GetDlgCtrlID(),LBN_SELCHANGE),
               (LPARAM)CListBox::m_hWnd);
      }
   }
   m_DropIndex = LB_ERR;
   return Ret;
}

Using the code

The COLEDragAndDropListBox class can be used in two different ways. If you simply want to move the text from one listbox to another, then change the type of your variable from CListBox to COLEDragAndDropListBox.

// CDragDropListBoxSampleDlg dialog
class CDragDropListBoxSampleDlg : public CDialog
{
    ......

    // Implementation
    protected:
       COLEDragAndDropListBox  m_ListBox;
    
    ......
    };

But if you want to send more info than just simple text, then you will need to create a class that inherits from COLEDragAndDropListBox and override the few virtual methods to supply it data and to handle the insertion and deletion of items.

// CDragDropListBoxSampleDlg dialog
class CMyDragAndDropListBox : public COLEDragAndDropListBox
{
   ......
protected: //overridables

   //override to supply data to attach to COleDataSource
   virtual HGLOBAL GetData(int ForIndex);

   //override to handle the item insertion
   virtual BOOL DroppedAt(int InsertBefore,HGLOBAL hGlobal);

   //override to so that you can specify 
   //if the DROPPED item can be moved or copied or both
   virtual DROPEFFECT GetDropItemEffects(COleDataObject* 
                          pDataObject,DWORD dwKeyState);

   //override to so that you can specify 
   //if the DRAGGED item can be moved or copied or both
   virtual DROPEFFECT GetDragItemEffects(int Index);

   //override to handle item deleting when an item is moved
   virtual void RemoveItem(int Index);

   //you can override this method to return 
   //the index of the item that is under the point
   //the default implementation depends 
   //on the Outside variable passed to ItemFromPoint
   //but that does not work in all cases
   virtual int GetItemAt(CPoint Point);

   //this function gets called from the OnDragOver method.
   //the default implementation tries to active the parent frame first,
   //if there is no parent frame then it activates the parent window
   virtual void ActivateWindow();

   ......
};

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)