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

That's a drag

0.00/5 (No votes)
20 Mar 2006 3  
Dragging items around in a list view.

Screenshot

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?

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