Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Drag and Drop between and within a CListCtrl

0.00/5 (No votes)
26 Apr 2003 10  
An article showing how to drag and drop items between CListCtrls or reorder items within a CListCtrl

Screenshot - DragTest2.jpg

Introduction:

This article will describe how to implement Drag and Drop to move items from one CListCtrl to another. Also, it shows how to use Drag and Drop within a CListCtrl to allow reordering of the items. This is something I have been curious to learn about for quite some time, and I have also seen several questions here at Code Project about it, especially with regard to reordering items in a CListCtrl. I hope this article answers those questions. I'm sure there are other ways to go about this, probably some that are more sophisticated and more complete, but this works well for me.

First, let me say that there is a nice example project available online from the Microsoft Knowledge Base that helped me a lot. My code very closely mirrors the code in that project. The project at the MSKB addresses dragging and dropping items to and from CListView objects to and from CTreeView objects. There is really very little difference between that and what I describe here. However, the "article" at the MSKB offers only the code and no explanation to go along with it, although the code is pretty well commented.

However, a weakness that the example from MS has, that has been commented on here, is that it only deals with a single column of data in the CListCtrl. I have updated my code to accomodate two columns of information. It should be quite easy for you to scale that up to accomodate as many columns as you need. The concept is the same regardless of how many columns you have.

Also, this article assumes you are at least fairly familiar with using a CListCtrl. There are a couple of truly excellent articles here at the Code Project by Matt Weagle on using CListCtrl and CHeaderCtrl objects. And Chris Maunder wrote a wonderful article on using callbacks in a CListCtrl. If you have not read any or all of these great articles, do so.

Overview:

In a very small nutshell, a Drag and Drop operation involves the following steps:

  1. You handle the notification that a drag operation has begun (i.e. the user is holding down a mouse button - in this example only the left button is dealt with - and dragging).
  2. You must track the movement of the mouse as the user drags the item.
  3. Finally, when the button is released, you perform the actual copy or move of the item.

That's all there is to it, and it really is relatively simple to implement.

Member variables to add to the dialog class:

There are a few pieces of information we need to hang on to in order to complete the drag and drop operation, so we need to add a few member variables to our dialog class (or view if you are using a Doc/View architecture - which my example does not). All of these can be added as "protected" members since they don't need to be accessed outside the dialog.

m_nDragIndex is the index of the item in the CListCtrl we are dragging.

m_nDropIndex is the index over which the mouse is hovering when the drag ends (where the item is dropped).

m_pDragWnd and m_pDropWnd are pointers to CWnd objects representing the windows of the CListCtrl we are dragging from and dropping onto. These will be cast into pointers to CListCtrl objects.

m_bDragging is a flag letting us know when we are in a drag and drop operation. It is used primarily in the MouseMove function so that we know when to be tracking.

m_pDragImage is a pointer to a CImageList object. MFC very helpfully has already created routines that make the creation and management of an image of the object being dragged extremely easy.

Lastly, we need to create a data structure to hold all of the information to move from one list to the other (or from one area of the list to another area in that same list). Here is the structure:

typedef struct {
    LVITEM* plvi;
    CString sCol2;
} lvItem, *plvItem;

What this structure contains is a pointer to an LVITEM and a CString. We will be using the LVITEM pointer to perform operations on the CListCtrl such as GetItem, InsertItem, etc. The CString simply holds the data in the second column of the CListCtrl. If you have more columns than just two, just add more CString members to this structure.

Catching the notification that a drag operation has begun:

Using the ClassWizard, add a function to handle the LVN_BEGINDRAG message in the CListCtrl. BTW, if you want to be able to drag items from multiple CListCtrl objects as in the example here, you have to handle this message for all of the lists.

First, we want to save the index of the item we are dragging, so put that value in a member variable. We use this index later to retrieve the item information.

    m_nDragIndex = pNMListView->iItem;

Next we create the image of the item we are dragging. Here's where MFC helps us a lot.

There are two ways to go about this part. The really easy way is to let MFC do the heavy lifting for you which is what the first snippet of code shows. The second way is only slightly more complex, but it does require that you create resource images to use (which my example project has). The second code snippet shows the way my project does the drag-image.

First way:

    
    //// Create a drag image
    POINT pt;
    int nOffset = 10; //offset in pixels for drag image (up and 
                          //to the left)
    pt.x = nOffset;
    pt.y = nOffset;

    m_pDragImage = m_listL.CreateDragImage(m_nDragIndex, &pt);
    ASSERT(m_pDragImage); //make sure it was created
    //We will call delete later (in LButtonUp) to clean this up

    //// Change the cursor to the drag image
    ////    (still must perform DragMove() in OnMouseMove() to show it moving)
    m_pDragImage->BeginDrag(0, CPoint(nOffset, nOffset));
    m_pDragImage->DragEnter(GetDesktopWindow(), pNMListView->ptAction);

Second way:

    //// Create a drag image
    POINT pt;
    int nOffset = -10; //offset in pixels for drag image 
                           //(positive is up and to the left; 
                           //neg is down and to the right)
    if(m_listL.GetSelectedCount() > 1) //more than one item is selected
    pt.x = nOffset;
    pt.y = nOffset;

    m_pDragImage = m_listL.CreateDragImage(m_nDragIndex, &pt);
    ASSERT(m_pDragImage); //make sure it was created
    //We will call delete later (in LButtonUp) to clean this up

    CBitmap bitmap;
    if(m_listL.GetSelectedCount() > 1) //more than 1 item in list is selected
        //bitmap.LoadBitmap(IDB_BITMAP_MULTI);
        bitmap.LoadBitmap(IDB_BITMAP_MULTI_BOXES);
    else
        bitmap.LoadBitmap(IDB_BITMAP_BOX);
    m_pDragImage->Replace(0, &bitmap, &bitmap);

    //// Change the cursor to the drag image
    ////    (still must perform DragMove() in OnMouseMove() to show it moving)
    m_pDragImage->BeginDrag(0, CPoint(nOffset, nOffset - 4));
    m_pDragImage->DragEnter(GetDesktopWindow(), pNMListView->ptAction);

Then we set some of our member variable values. Most importantly, we are setting m_bDragging to TRUE so that we know we are in a drag and drop operation.

    //// Set dragging flag and others
    m_bDragging = TRUE;    //we are in a drag and drop operation
    m_nDropIndex = -1;    //we don't have a drop index yet
    m_pDragList = &m_listL; //make note of which list we are dragging from
    m_pDropWnd = &m_listL;    //at present the drag list is the drop list

Finally, we call SetCapture. By doing this, we are ensuring that our active control (in this case the CListCtrl) will receive all mouse messages. Even if the user drags the item off our CListCtrl or even off the dialog altogether, this control will still be getting notification messages - that is, until we release the capture (which we do once the button is released and the drag ends) or until another window captures the mouse.

    //// Capture all mouse messages
    SetCapture ();

Tracking the drag:

Using ClassWizard, add a function to handle the WM_MOUSEMOVE message in the dialog class.

Only if the m_bDragging flag is set to TRUE will we do anything here.

    if (m_bDragging)
    {

This next section is where we actually move the drag-image on the screen.

The DragShowNolock(false) call allows windows to display the drag-image smoothly on screen. If we don't make this call, then the drag-image gets cropped sometimes depending on what it is overlapping. For example, if you disable that line and run the program, you will see that as you drag the item over the other items in the list, the drag-image gets cropped by those items.

        //// Move the drag image
        CPoint pt(point);    //get our current mouse coordinates
        ClientToScreen(&pt); //convert to screen coordinates
        m_pDragImage->DragMove(pt); //move the drag image to those coordinates
        // Unlock window updates (this allows the dragging image to be shown 
                // smoothly)
        m_pDragImage->DragShowNolock(false);

WindowFromPoint(pt) is a very handy function that returns a pointer to which ever window the cursor is hovering over. This is how we know where the mouse is pointing, and whether it is pointing to CListCtrl window.

        // Get the CWnd pointer of the window that is under the 
                // mouse cursor
        CWnd* pDropWnd = WindowFromPoint (pt);
        ASSERT(pDropWnd); //make sure we have a window

This section just deals with the highlighting of items as we drag over the CListCtrl.

        //// If we drag outside current window we need 
                //   to adjust the highlights displayed
        if (pDropWnd != m_pDropWnd)
        {
                        //If we drag over the CListCtrl header, turn off the 
                        // hover highlight
            if (m_nDropIndex != -1) 
            {
                TRACE("m_nDropIndex is -1\n");
                CListCtrl* pList = (CListCtrl*)m_pDropWnd;
                VERIFY (pList->SetItemState (m_nDropIndex, 0, 
                                                             LVIS_DROPHILITED));
                // redraw item
                VERIFY (pList->RedrawItems (m_nDropIndex, 
                                                           m_nDropIndex));
                pList->UpdateWindow ();
                m_nDropIndex = -1;
            else //If we drag out of the CListCtrl altogether
            {
                TRACE("m_nDropIndex is not -1\n");
                CListCtrl* pList = (CListCtrl*)m_pDropWnd;
                int i = 0;
                int nCount = pList->GetItemCount();
                for(i = 0; i < nCount; i++)
                {
                    pList->SetItemState(i, 0, LVIS_DROPHILITED);
                }
                pList->RedrawItems(0, nCount);
                pList->UpdateWindow();
            }
        }

As we move the mouse, we need to keep track of what window we are dragging over so that we know into which control we are to drop the item when the button is released. So we save that in our member variable.

        // Save current window pointer as the CListCtrl we are dropping onto
        m_pDropWnd = pDropWnd;

        // Convert from screen coordinates to drop target client coordinates
        pDropWnd->ScreenToClient(&pt);

IsKindOf(RUNTIME_CLASS(CListCtrl)) is another handy function that allows us to find out if the window the mouse is hovering over is actually a CListCtrl or not. This section simply deals with highlighting the item the mouse is hovering over.

        //If we are hovering over a CListCtrl we need to adjust
        //the highlights
        if(pDropWnd->IsKindOf(RUNTIME_CLASS (CListCtrl)))
        {            
            UINT uFlags;
            CListCtrl* pList = (CListCtrl*)pDropWnd;

            // Turn off hilight for previous drop target
            pList->SetItemState (m_nDropIndex, 0, LVIS_DROPHILITED);
            // Redraw previous item
            pList->RedrawItems (m_nDropIndex, m_nDropIndex);
            
            // Get the item that is below cursor
            m_nDropIndex = ((CListCtrl*)pDropWnd)->HitTest(pt, &uFlags);
            // Highlight it
            pList->SetItemState(m_nDropIndex, LVIS_DROPHILITED, 
                                           LVIS_DROPHILITED);
            // Redraw item
            pList->RedrawItems(m_nDropIndex, m_nDropIndex);
            pList->UpdateWindow();
        }
        // Lock window updates
        m_pDragImage->DragShowNolock(true);
    }

Ending the drag - i.e., doing the drop:

We're almost done here. Using ClassWizard again, add a function to handle the WM_LBUTTONUP message. This part is pretty short and pretty straightforward.

Just like in the OnMouseMove function above, we only do stuff if we are currently performing a drag and drop operation.

    if (m_bDragging)
    {

Don't forget to release the mouse capture!

        // Release mouse capture, so that other controls 
                // can get control/messages
        ReleaseCapture ();

Since we have dropped the item, we are no longer in a drag operation.

        // Note that we are NOT in a drag operation
        m_bDragging = FALSE;

MFC once again offers us functions to deal with the drag-image. Don't forget to "delete" the image since it was "created" above (when the drag operation began).

        // End dragging image
        m_pDragImage->DragLeave (GetDesktopWindow ());
        m_pDragImage->EndDrag ();
        delete m_pDragImage; 
        //must delete it because it was created at the beginning of the drag

Just like we did in the OnMouseMove function, we find out where the mouse is and over which window it is hovering. Then we check to see if that window is a CListCtrl. If it is, then we perform the actual drop. To make things easier (and easier to read), I have put the code that performs the actual copy/move of the item into a separate function called DropItemOnList (below).

        CPoint pt (point); //Get current mouse coordinates
        ClientToScreen (&pt); //Convert to screen coordinates
        // Get the CWnd pointer of the window that is under
        //the mouse cursor
        CWnd* pDropWnd = WindowFromPoint (pt);
        ASSERT (pDropWnd); //make sure we have a window pointer
        // If window is CListCtrl, we perform the drop
        if (pDropWnd->IsKindOf (RUNTIME_CLASS (CListCtrl)))
        {
            m_pDropList = (CListCtrl*)pDropWnd; 
                        //Set pointer to the list we are dropping on
            DropItemOnList(m_pDragList, m_pDropList); 
                        //Call routine to perform the actual drop
        }
    }

Which brings us to the final step: the actual copy or move of the item from one place to another.

The actual copy/move:

You need to manually add a new function to your dialog class to handle the copy/move of the item from one place to another. This function can also be a "protected" member like the member variables we added earlier. Here's the line to add to the header file:

void DropItemOnList(CListCtrl* pDragList, CListCtrl* pDropList);

Here's the actual function to add to the .cpp file:

void CDragTestDlg::DropItemOnList(CListCtrl* pDragList,
     CListCtrl* pDropList)
{
    //This routine performs the actual drop of the item dragged.
    //It simply grabs the info from the Drag list (pDragList)
    // and puts that info into the list dropped on (pDropList).
    //Send:    pDragList = pointer to CListCtrl we dragged from,
    //        pDropList = pointer to CListCtrl we are dropping on.
    //Return: nothing.

    ////Variables
    char szLabel[256];
    LVITEM lviT;
    LVITEM* plvitem;
    lvItem* pItem;
    lvItem lvi;
    
    // Unhilight the drop target
    pDropList->SetItemState (m_nDropIndex, 0, LVIS_DROPHILITED);

    //Set up the LV_ITEM for retrieving item from pDragList and adding the 
        //new item to the pDropList
    ZeroMemory(&lviT, sizeof (LVITEM)); //allocate and clear memory space 
                                            // for LV_ITEM
    lviT.iItem        = m_nDragIndex;
    lviT.mask        = LVIF_TEXT;
    lviT.pszText        = szLabel;
    lviT.cchTextMax    = 255;

    lvi.plvi = &lviT;
    lvi.plvi->iItem = m_nDragIndex;
    lvi.plvi->mask = LVIF_TEXT;
    lvi.plvi->pszText = szLabel;
    lvi.plvi->cchTextMax = 255;

    if(pDragList->GetSelectedCount() == 1)
    {
        // Get item that was dragged
        pDragList->GetItem (lvi.plvi);
        lvi.sCol2 = pDragList->GetItemText(lvi.plvi->iItem, 1);

        // Delete the original item (for Move operation)
        // This is optional. If you want to implement a Copy 
        // operation, don't delete. This works very well though 
        // for re-arranging items within a CListCtrl. It is written 
        // at present such that when dragging from one list to the 
                // other the item is copied, but if dragging within one list,
                // the item is moved.
        if(pDragList == pDropList)
        {
            pDragList->DeleteItem (m_nDragIndex);
            if(m_nDragIndex < m_nDropIndex) m_nDropIndex--; 
                        //decrement drop index to account for item
                        //being deleted above it
        }

        // Insert item into pDropList
        // if m_nDropIndex == -1, iItem = GetItemCount() 
                //   (inserts at end of list), else iItem = m_nDropIndex
        lvi.plvi->iItem = (m_nDropIndex == -1) ? 
                                   pDropList->GetItemCount () : m_nDropIndex;
        pDropList->InsertItem (lvi.plvi);
        pDropList->SetItemText(lvi.plvi->iItem, 1, (LPCTSTR)lvi.sCol2);

        // Select the new item we just inserted
        pDropList->SetItemState (lvi.plvi->iItem, LVIS_SELECTED, 
                                         LVIS_SELECTED);
    }
    else //more than 1 item is being dropped
    {
        //We have to parse through all of the selected items from the 
                // DragList
        //1) Retrieve the info for the items and store them in memory
        //2) If we are reordering, delete the items from the list
        //3) Insert the items into the list (either same list or 
                //   different list)

        CList<lvItem*, lvItem*> listItems;
        POSITION listPos;
            
        //Retrieve the selected items
        POSITION pos = pDragList->GetFirstSelectedItemPosition(); 
                                                 //iterator for the CListCtrl
        while(pos) //so long as we have a valid POSITION, we keep 
                           // iterating
        {
            plvitem = new LVITEM;
            ZeroMemory(plvitem, sizeof(LVITEM));
            pItem = new lvItem;
            //ZeroMemory(pItem, sizeof(lvItem)); 
                        //If you use ZeroMemory on the lvItem struct, 
                        //it creates an error when you try to set sCol2
            pItem->plvi = plvitem;
            pItem->plvi->iItem = m_nDragIndex;
            pItem->plvi->mask = LVIF_TEXT;
            pItem->plvi->pszText = new char; 
                        //since this is a pointer to the string, we need a 
                        //new pointer to a new string on the heap
            pItem->plvi->cchTextMax = 255;

            m_nDragIndex = pDragList->GetNextSelectedItem(pos);

            //Get the item
            pItem->plvi->iItem = m_nDragIndex; //set the index in 
                                           //the drag list to the selected item
            pDragList->GetItem(pItem->plvi); //retrieve the 
                                                         // information
            pItem->sCol2 = pDragList->GetItemText(
                                                       pItem->plvi->iItem, 1);

            //Save the pointer to the new item in our CList
            listItems.AddTail(pItem);
        } //EO while(pos) -- at this point we have deleted the moving 
                  // items and stored them in memory

        if(pDragList == pDropList) //we are reordering the list (moving)
        {
            //Delete the selected items
            pos = pDragList->GetFirstSelectedItemPosition();
            while(pos)
            {
                pos = pDragList->GetFirstSelectedItemPosition();
                m_nDragIndex = pDragList->GetNextSelectedItem(pos);

                pDragList->DeleteItem(m_nDragIndex); 
                                //since we are MOVING, delete the item
                if(m_nDragIndex < m_nDropIndex) m_nDropIndex--; 
                                //must decrement the drop index to account
                                //for the deleted items
            } //EO while(pos)
        } //EO if(pDragList...

        //Iterate through the items stored in memory and add them 
                //back into the CListCtrl at the drop index
        listPos = listItems.GetHeadPosition();
        while(listPos)
        {
            pItem = listItems.GetNext(listPos);

            m_nDropIndex = (m_nDropIndex == -1) ? 
                                    pDropList->GetItemCount() : m_nDropIndex;
            pItem->plvi->iItem = m_nDropIndex;
            pDropList->InsertItem(pItem->plvi); //add the item
            pDropList->SetItemText(pItem->plvi->iItem, 1, 
                                               pItem->sCol2);

            pDropList->SetItemState(pItem->plvi->iItem, 
                                                LVIS_SELECTED, LVIS_SELECTED); 
                        //highlight/select the item we just added

            m_nDropIndex++; 
                        //increment the index we are dropping at to keep the 
                        //dropped items in the same order they were in in the 
            //Drag List. If we dont' increment this, the items are  
                        //added in reverse order

            //Lastly, we need to clean up by deleting our "new" 
                        //variables
            delete pItem;

        } //EO while(listPos)

    }
}

We simply set up an LVITEM structure that will be used to both retrieve the information from the CListCtrl we dragged from and to add that information into the CListCtrl we dropped on. If your CListCtrl includes icons, don't forget to change the "lvi.mask=LVIF_TEXT" line.

So we set up the LVITEM and then use GetItem(&lvi) to retrieve the information from the list we dragged the item from. But you will note that this will NOT get the data from the second (or any subsequent) column. That's why we need the lvItem structure. We use pItem->sCol2 = pDragList->GetItemText(pItem->plvi->iItem, 1); to retrieve that info.

When we are dealing with multiple items being dragged, we set up a CList of the lvItem objects. We parse through the CListCtrl we are dragging from and retrieve all of the information on all of the selected items. Then, if we are performing a "move" operation, we delete those selected items from the list. Then we parse through the CList of objects and add each item to the CListCtrl.

As I have my example program implemented, I check to see if the user has dragged the item onto the other CListCtrl or still on the same CListCtrl. If the user dragged an item to the other CListCtrl, then we will COPY the item to that control. If, however, the user simply dragged the item to a different spot on the same CListCtrl, we will MOVE the item to that new index (this is how to implement user sorting of a list by drag and drop). Basically, if the user is dropping on the same list they dragged the item from, we delete that item (after retrieving the item information) before adding it back into the list. We have to delete the item first, because if we added the item back into the list first the indexes of the items would not be the same, and in many instances we end up writing the dragged item information over another existing item. Try it to see what I mean. Move the part where we delete the item to AFTER we have added the item to the list. Drag items around in the list and see what happens.

Finally, we simply add the item to the CListCtrl and set the highlight to that item.

Tada! It's pretty easy, actually.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here