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()
{
m_Dragging = FALSE;
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);
m_Dragging = TRUE;
m_DragItem = pNMLV->iItem;
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();
if(m_Dragging)
{
list.SetRedraw(FALSE);
UINT flags=0;
UINT index = list.HitTest( point, &flags );
if(index==-1)
{
int top = list.GetTopIndex();
int last = top + list.GetCountPerPage();
if(flags & LVHT_BELOW) index=last;
else
if(flags & LVHT_ABOVE) index=top-1;
if(index!=-1) {
int offset = index-m_DragItem;
if(offset != 0) {
UINT selectedcount = list.GetSelectedCount();
UINT *selected = new UINT[selectedcount];
UINT i = 0;
POSITION pos = list.GetFirstSelectedItemPosition();
while(pos)
selected[i++]=list.GetNextSelectedItem(pos);
for(i=0;i < selectedcount;i++){
int iterator = (offset>0) ? selectedcount-i-1 : i;
int oldpos = selected[iterator];
int newpos = oldpos+offset;
if(newpos<0 || newpos>=list.GetItemCount()) break;
list.SetItemState(oldpos, 0, LVIS_SELECTED);
if(offset>0) {
for(int j=oldpos;j < newpos;j++)
SwapRows(j,j+1);
}else {
for(int j=oldpos;j > newpos;j--)
SwapRows(j,j-1);
}
list.EnsureVisible(newpos,TRUE);
list.SetItemState(newpos, LVIS_SELECTED, LVIS_SELECTED);
}
delete [] selected;
m_DragItem=index;
}
}
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)
{
}
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)
{
if(m_Dragging) {
m_Dragging = FALSE;
ReleaseCapture();
CheckPuzzle();
}
CListView::OnLButtonUp(nFlags, point);
}
Well now, isn't that a drag?