Click here to Skip to main content
15,881,248 members
Articles / Desktop Programming / MFC
Article

That's a drag

Rate me:
Please Sign up or sign in to vote.
4.57/5 (14 votes)
20 Mar 20062 min read 58.1K   1.1K   32   6
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


Written By
Web Developer
Netherlands Netherlands
Niek is the founder and programmer of DaanSystems.com and is working on many projects all the time. He makes a living by doing contractwork for others.

Comments and Discussions

 
QuestionDatagridView C# Pin
Bahaatya8-Jun-17 9:08
Bahaatya8-Jun-17 9:08 
GeneralWow Pin
futurejo19-Jan-10 7:05
futurejo19-Jan-10 7:05 
GeneralMy vote of 2 Pin
Alexandre GRANVAUD4-Feb-09 20:46
Alexandre GRANVAUD4-Feb-09 20:46 
GeneralRe: My vote of 2 Pin
Stephan Poirier3-Sep-09 5:10
Stephan Poirier3-Sep-09 5:10 
Generalgreat article Pin
Bartosz Wójcik14-Oct-07 9:32
Bartosz Wójcik14-Oct-07 9:32 
GeneralCreateDragImage() bug with XP styles enabled Pin
Richard GILL29-Mar-07 5:06
Richard GILL29-Mar-07 5:06 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.