That's a drag






4.57/5 (12 votes)
Mar 20, 2006
2 min read

58860

1142
Dragging items around in a list view.
Introduction
Doing some rewriting on my program NewsReactor, I developed this queue that you can rearrange using your mouse. I wanted the possibility of dragging a multiple selection freely around the list. Since I'm using a virtual listview, I needed some simple functions for my backend that enables moving items. The simplest function to make this possible is a swap function. It's almost never hard to swap two items (or records) from a list. Therefore, this method enables "dragging by swapping". As an example, I've made a little card game. It's your job to order the deck and win ever lasting honor and admiration.
Let's get this show on the road
This example uses a MFC document/view application. But the essential code could be easily ported to WTL or plain APIs.
The main attention goes to the CDraggerView
class. This is where the show gets on the road. We need two extra member variables:
BOOL m_Dragging
UINT m_DragItem
Let's clear them in the constructor:
CDraggerView::CDraggerView() { // Member variable to check if we are dragging m_Dragging = FALSE; // Member variable to store the selected // item when starting a drag m_DragItem = 0; }
The most important for implementing the effective dragging is the extended listview style LVS_EX_FULLROWSELECT
. In the CDraggerView::OnInitialUpdate()
override, we can set this:
void CDraggerView::OnInitialUpdate()
{
...
CListCtrl &list = GetListCtrl();
list.SetExtendedStyle(list.GetExtendedStyle()|
LVS_EX_FULLROWSELECT );
...
}
Drag Race
Dragging starts when the mouse button goes down on an item and the moving starts. There's a nice message for this: ON_NOTIFY_REFLECT(LVN_BEGINDRAG, OnLvnBegindrag)
. We need a handler for it:
void CDraggerView::OnLvnBegindrag(NMHDR *pNMHDR, LRESULT *pResult) { LPNMLISTVIEW pNMLV = reinterpret_cast<LPNMLISTVIEW>(pNMHDR); // Set dragging to ON m_Dragging = TRUE; // Store item where dragging begins m_DragItem = pNMLV->iItem; // Get the mouse capture on list control GetListCtrl().SetCapture(); *pResult = 0; }
On the move
Now to make dragging happen in real time, we are going to get this done in the ON_WM_MOUSEMOVE()
message handler. The main tricky stuff is in here:
void CDraggerView::OnMouseMove(UINT nFlags, CPoint point) { CListCtrl& list = GetListCtrl(); // Are we dragging? if(m_Dragging) { // Disable list drawing for less flickering list.SetRedraw(FALSE); // Now find the item where the mouse cursor is UINT flags=0; UINT index = list.HitTest( point, &flags ); // No valid item found? Perhaps // the mouse is outside the list if(index==-1) { int top = list.GetTopIndex(); int last = top + list.GetCountPerPage(); // Mouse is under the listview, // so pretend it's over the last item // in view if(flags & LVHT_BELOW) index=last; else // Mouse is above the listview, // so pretend it's over the top item in // view - 1 if(flags & LVHT_ABOVE) index=top-1; } // Do we have a valid item now? if(index!=-1) { // calculate the offset between the two items int offset = index-m_DragItem; // Is it not the same item? if(offset != 0) { // Do we have a multiple selection? UINT selectedcount = list.GetSelectedCount(); // Create an array of selected // items (could use CArray here) UINT *selected = new UINT[selectedcount]; UINT i = 0; // Add all selected items to this array POSITION pos = list.GetFirstSelectedItemPosition(); while(pos) selected[i++]=list.GetNextSelectedItem(pos); // Now we are going to move the selected items for(i=0;i < selectedcount;i++){ // If we are moving the selection downward, we'll start // with the last one and iterate up. Else start with // the first one and iterate down. int iterator = (offset>0) ? selectedcount-i-1 : i; // Now get the position of the first selected item int oldpos = selected[iterator]; // Calculate the new position int newpos = oldpos+offset; // Is the new position outsize the list's boundaries? break if(newpos<0 || newpos>=list.GetItemCount()) break; // Unselect the item list.SetItemState(oldpos, 0, LVIS_SELECTED); // No we keep swapping items until the selected // item reaches the new position if(offset>0) { // Going down for(int j=oldpos;j < newpos;j++) SwapRows(j,j+1); }else { // Going up for(int j=oldpos;j > newpos;j--) SwapRows(j,j-1); } // Make sure the newposition is in view list.EnsureVisible(newpos,TRUE); // Select it again list.SetItemState(newpos, LVIS_SELECTED, LVIS_SELECTED); } // Free the array delete [] selected; // Set the dragging item to the current index position, // so we can start over again m_DragItem=index; } } // Enable drawing in the listview again list.SetRedraw(TRUE); } CListView::OnMouseMove(nFlags, point); }
You might ask: What's with all the for
loops and swapping? Why not a simple MoveTo()
function? Well, it all depends on what you are using as a dataset. In a simple listview with items maintained by the listview itself, it's quite simple to remove an item and insert it at another position, automatically shifting all others. When using a virtual listview with a database, for example, this is not always so simple. Swapping is almost always quite simple as said. Swapping an item multiple times happens only when the mouse is moved very fast, so: offset > 1 or offset < -1.
Hot swapping
Swapping rows is quite simple with most datasets, mostly this boils down to temp=row1;row1=row2;row2=temp
:
BOOL CDraggerView::SwapRows(UINT row1,UINT row2) { // In this function we need to swap two rows, // Here it does some mangling with the listview's item texts/image/userdata // If you have a virtual list view you can swap it's items here //... code to swap rows ... }
Clean up your act
When finished dragging, the mouse button is released and we need to clean up:
void CDraggerView::OnLButtonUp(UINT nFlags, CPoint point) { // Were we dragging? if(m_Dragging) { m_Dragging = FALSE; // Release mouse capture ReleaseCapture(); // Check puzzle state CheckPuzzle(); } CListView::OnLButtonUp(nFlags, point); }
Well now, isn't that a drag?