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:
- 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).
- You must track the movement of the mouse as the user drags the item.
- 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:
POINT pt;
int nOffset = 10;
pt.x = nOffset;
pt.y = nOffset;
m_pDragImage = m_listL.CreateDragImage(m_nDragIndex, &pt);
ASSERT(m_pDragImage);
m_pDragImage->BeginDrag(0, CPoint(nOffset, nOffset));
m_pDragImage->DragEnter(GetDesktopWindow(), pNMListView->ptAction);
Second way:
POINT pt;
int nOffset = -10;
if(m_listL.GetSelectedCount() > 1)
pt.x = nOffset;
pt.y = nOffset;
m_pDragImage = m_listL.CreateDragImage(m_nDragIndex, &pt);
ASSERT(m_pDragImage);
CBitmap bitmap;
if(m_listL.GetSelectedCount() > 1)
bitmap.LoadBitmap(IDB_BITMAP_MULTI_BOXES);
else
bitmap.LoadBitmap(IDB_BITMAP_BOX);
m_pDragImage->Replace(0, &bitmap, &bitmap);
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.
m_bDragging = TRUE;
m_nDropIndex = -1;
m_pDragList = &m_listL;
m_pDropWnd = &m_listL;
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.
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.
CPoint pt(point);
ClientToScreen(&pt);
m_pDragImage->DragMove(pt);
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.
CWnd* pDropWnd = WindowFromPoint (pt);
ASSERT(pDropWnd);
This section just deals with the highlighting of items as we drag over the CListCtrl
.
if (pDropWnd != m_pDropWnd)
{
if (m_nDropIndex != -1)
{
TRACE("m_nDropIndex is -1\n");
CListCtrl* pList = (CListCtrl*)m_pDropWnd;
VERIFY (pList->SetItemState (m_nDropIndex, 0,
LVIS_DROPHILITED));
VERIFY (pList->RedrawItems (m_nDropIndex,
m_nDropIndex));
pList->UpdateWindow ();
m_nDropIndex = -1;
else
{
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.
m_pDropWnd = pDropWnd;
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(pDropWnd->IsKindOf(RUNTIME_CLASS (CListCtrl)))
{
UINT uFlags;
CListCtrl* pList = (CListCtrl*)pDropWnd;
pList->SetItemState (m_nDropIndex, 0, LVIS_DROPHILITED);
pList->RedrawItems (m_nDropIndex, m_nDropIndex);
m_nDropIndex = ((CListCtrl*)pDropWnd)->HitTest(pt, &uFlags);
pList->SetItemState(m_nDropIndex, LVIS_DROPHILITED,
LVIS_DROPHILITED);
pList->RedrawItems(m_nDropIndex, m_nDropIndex);
pList->UpdateWindow();
}
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!
ReleaseCapture ();
Since we have dropped the item, we are no longer 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).
m_pDragImage->DragLeave (GetDesktopWindow ());
m_pDragImage->EndDrag ();
delete m_pDragImage;
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);
ClientToScreen (&pt);
CWnd* pDropWnd = WindowFromPoint (pt);
ASSERT (pDropWnd);
if (pDropWnd->IsKindOf (RUNTIME_CLASS (CListCtrl)))
{
m_pDropList = (CListCtrl*)pDropWnd;
DropItemOnList(m_pDragList, m_pDropList);
}
}
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)
{
char szLabel[256];
LVITEM lviT;
LVITEM* plvitem;
lvItem* pItem;
lvItem lvi;
pDropList->SetItemState (m_nDropIndex, 0, LVIS_DROPHILITED);
ZeroMemory(&lviT, sizeof (LVITEM));
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)
{
pDragList->GetItem (lvi.plvi);
lvi.sCol2 = pDragList->GetItemText(lvi.plvi->iItem, 1);
if(pDragList == pDropList)
{
pDragList->DeleteItem (m_nDragIndex);
if(m_nDragIndex < m_nDropIndex) 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);
pDropList->SetItemState (lvi.plvi->iItem, LVIS_SELECTED,
LVIS_SELECTED);
}
else
{
CList<lvItem*, lvItem*> listItems;
POSITION listPos;
POSITION pos = pDragList->GetFirstSelectedItemPosition();
while(pos)
{
plvitem = new LVITEM;
ZeroMemory(plvitem, sizeof(LVITEM));
pItem = new lvItem;
pItem->plvi = plvitem;
pItem->plvi->iItem = m_nDragIndex;
pItem->plvi->mask = LVIF_TEXT;
pItem->plvi->pszText = new char;
pItem->plvi->cchTextMax = 255;
m_nDragIndex = pDragList->GetNextSelectedItem(pos);
pItem->plvi->iItem = m_nDragIndex;
pDragList->GetItem(pItem->plvi);
pItem->sCol2 = pDragList->GetItemText(
pItem->plvi->iItem, 1);
listItems.AddTail(pItem);
}
if(pDragList == pDropList)
{
pos = pDragList->GetFirstSelectedItemPosition();
while(pos)
{
pos = pDragList->GetFirstSelectedItemPosition();
m_nDragIndex = pDragList->GetNextSelectedItem(pos);
pDragList->DeleteItem(m_nDragIndex);
if(m_nDragIndex < m_nDropIndex) m_nDropIndex--;
}
}
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);
pDropList->SetItemText(pItem->plvi->iItem, 1,
pItem->sCol2);
pDropList->SetItemState(pItem->plvi->iItem,
LVIS_SELECTED, LVIS_SELECTED);
m_nDropIndex++;
delete pItem;
}
}
}
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.