Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Perform default scrolling behavior, when user drags listview item over its scrolbar

INTRODUCTION:

I am not native English speaker, and I am not very experienced programmer either.

I have met a problem that I have a hard time describing, so please bear this in mind while reading this question.

RELEVANT INFORMATION:

I am working on implementing drag and drop functionality in listview. I just want to be able to rearrange rows inside listview, there will be no dragging items to other windows.

I do not want to use OLE to do this, and I am not satisfied with "default" implementation I found on numerous links.

I have my own idea on how I would like to do this, but my inexperience prevents me from implementing my thoughts.

I am developing in Visual Studio, in C++ and raw WinAPI. I am not using any libraries, nor would I like to start using them now.

PROBLEM:

I wish to implement the following behavior:

User presses left mouse button and starts dragging an item -> user moves mouse over vertical scrollbar -> default scrolling occurs.

Since scrollbar counts as nonclient area, this means that I must somehow perform default behavior for WM_NCMOUSEMOVE and WM_NCLBUTTONDOWN but I do not know how to do it.

Let me try and explain better what I mean:

When you drag item, it is logical that application indicates where it will be dropped when the mouse is over an item ( in the client area of the listview ).

When you drag item over scrollbar, it is obvious that user can not drop item there. Instead of indicating invalid dropping point ( by changing the cursor, for example, like OLE does), I wish to perform the following:

I wish to perform default scrollbar behavior (as if user does not drag item at all ). It would be as if user hovers over scrollbar, presses and holds down left mouse button, and optionally, moves the mouse up or down.

When user moves mouse from scrollbar back into the client area of the listview, drag and drop continues.

SSCCE

My English was not good enough to conduct a proper research ( as I usually do before posting here ), and I do not know of any app that has this type of behavior, so it was really hard for me to try to solve this on my own.

Still, trudging through the Raymond Chen's blog I came to an idea.

The example code below perfectly demonstrates the behavior I talked about above. It is not perfect, but it is the closest to implementing the behavior I want.

Create empty C++ project and simply copy/paste the code below.

Then try to drag item over scrollbar.

IMPORTANT: I haven't implemented rearranging of items, nor have changed cursor shape in order to keep the code minimal. The purpose of this SSCCE is to demonstrate the the behavior I want.

#include <windows.h>
#include <windowsx.h>   // various listview macros etc
#include <CommCtrl.h>
#include <stdio.h>      // swprintf_s()

// enable Visual Styles
#pragma comment( linker, "/manifestdependency:\"type='win32' \
                         name='Microsoft.Windows.Common-Controls' version='6.0.0.0' \
                         processorArchitecture='*' publicKeyToken='6595b64144ccf1df' \
                         language='*'\"")

// link with Common Controls library
#pragma comment( lib, "comctl32.lib")

//global variables
HINSTANCE hInst;
BOOL g_bDrag;

// subclass procedure for listview -> implements drag and drop
LRESULT CALLBACK DragAndDrop(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam,
    UINT_PTR uIdSubclass, DWORD_PTR dwRefData)
{
    switch (message)
    {
    case WM_CAPTURECHANGED:  // in case user ALT+TAB to another window, for example
    {
        g_bDrag = FALSE;
    }
        return DefSubclassProc(hwnd, message, wParam, lParam);

    case WM_LBUTTONUP:      // do the drop ->omitted for brewity
    {
        if (g_bDrag)
        {
            POINT pt = { 0 };
            pt.x = GET_X_LPARAM(lParam);
            pt.y = GET_Y_LPARAM(lParam);

            g_bDrag = FALSE;
            ReleaseCapture();
        }
    }
        return DefSubclassProc(hwnd, message, wParam, lParam);

    case WM_MOUSEMOVE:
    {
        if (g_bDrag)
        {
            POINT pt = { 0 };
            pt.x = GET_X_LPARAM(lParam);
            pt.y = GET_Y_LPARAM(lParam);

            LVHITTESTINFO lvhti = { 0 };
            lvhti.pt = pt;
            ListView_HitTest(hwnd, &lvhti);

            ClientToScreen(hwnd, &pt);  // WM_NCHITTEST takes screen coordinates

            UINT hittest = SendMessage(hwnd, WM_NCHITTEST, 0, MAKELPARAM(pt.x, pt.y));


            if (hittest == HTVSCROLL)  // my try to do the default behavior
            {
                SendMessage(hwnd, WM_NCLBUTTONDOWN, (WPARAM)hittest, (LPARAM)POINTTOPOINTS(pt));
                //SendMessage(hwnd, WM_NCMOUSEMOVE, (WPARAM)hittest, (LPARAM)POINTTOPOINTS(pt));
            }

        }
    }
        return DefSubclassProc(hwnd, message, wParam, lParam);

    case WM_NCDESTROY:
        ::RemoveWindowSubclass(hwnd, DragAndDrop, 0);
        return DefSubclassProc(hwnd, message, wParam, lParam);
    }
    return ::DefSubclassProc(hwnd, message, wParam, lParam);
}

// main window procedure
LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
    switch (msg)
    {
    case WM_CREATE:
    {
        g_bDrag = FALSE;  // user is not dragging listview item

        //================ create an example listview
        RECT rec = { 0 };
        GetClientRect(hwnd, &rec);

        HWND hwndLV = CreateWindowEx(0, WC_LISTVIEW,
            L"", WS_CHILD | WS_VISIBLE | WS_BORDER | LVS_REPORT,
            50, 50, 250, 200, hwnd, (HMENU)2000, hInst, 0);

        // set extended listview styles
        ListView_SetExtendedListViewStyle(hwndLV, LVS_EX_FULLROWSELECT | LVS_EX_GRIDLINES | LVS_EX_DOUBLEBUFFER);

        // add some columns
        LVCOLUMN lvc = { 0 };

        lvc.mask = LVCF_FMT | LVCF_WIDTH | LVCF_TEXT | LVCF_SUBITEM;
        lvc.fmt = LVCFMT_LEFT;

        for (long nIndex = 0; nIndex < 5; nIndex++)
        {
            wchar_t txt[50];
            swprintf_s(txt, 50, L"Column %d", nIndex);

            lvc.iSubItem = nIndex;
            lvc.cx = 60;
            lvc.pszText = txt;

            ListView_InsertColumn(hwndLV, nIndex, &lvc);
        }

        // add some items
        LVITEM lvi;

        lvi.mask = LVIF_TEXT;

        for (lvi.iItem = 0; lvi.iItem < 10000; lvi.iItem++)
        {
            for (long nIndex = 0; nIndex < 5; nIndex++)
            {
                wchar_t txt[50];
                swprintf_s(txt, 50, L"Item %d%d", lvi.iItem, nIndex);

                lvi.iSubItem = nIndex;
                lvi.pszText = txt;
                if (!nIndex)  // item 
                    SendDlgItemMessage(hwnd, 2000, LVM_INSERTITEM, 0, reinterpret_cast<LPARAM>(&lvi));
                else            // sub-item
                    SendDlgItemMessage(hwnd, 2000, LVM_SETITEM, 0, reinterpret_cast<LPARAM>(&lvi));
            }
        }

        //============================ subclass it
        SetWindowSubclass(hwndLV, DragAndDrop, 0, 0);
    }
        return 0L;
    case WM_NOTIFY:
    {
        switch (((LPNMHDR)lParam)->code)
        {
        case LVN_BEGINDRAG:  // user started dragging listview item
        {
            g_bDrag = TRUE;
            SetCapture(((LPNMHDR)lParam)->hwndFrom);  // listview must capture the mouse
        }
            break;
        default:
            break;
        }
    }
        break;
    case WM_CLOSE:
        ::DestroyWindow(hwnd);
        return 0L;
    case WM_DESTROY:
    {
        ::PostQuitMessage(0);
    }
        return 0L;
    default:
        return ::DefWindowProc(hwnd, msg, wParam, lParam);
    }
    return 0;
}

// WinMain

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine,
    int nCmdShow)
{
    // store hInstance in global variable for later use
    hInst = hInstance;

    WNDCLASSEX wc;
    HWND hwnd;
    MSG Msg;

    // register main window class
    wc.cbSize = sizeof(WNDCLASSEX);
    wc.style = 0;
    wc.lpfnWndProc = WndProc;
    wc.cbClsExtra = 0;
    wc.cbWndExtra = 0;
    wc.hInstance = hInst;
    wc.hIcon = LoadIcon(hInstance, IDI_APPLICATION);
    wc.hCursor = LoadCursor(NULL, IDC_ARROW);
    wc.hbrBackground = GetSysColorBrush(COLOR_WINDOW);
    wc.lpszMenuName = NULL;
    wc.lpszClassName = L"Main_Window";
    wc.hIconSm = LoadIcon(hInstance, IDI_APPLICATION);

    if (!RegisterClassEx(&wc))
    {
        MessageBox(NULL, L"Window Registration Failed!", L"Error!", MB_ICONEXCLAMATION |
            MB_OK);

        return 0;
    }

    // initialize common controls
    INITCOMMONCONTROLSEX iccex;
    iccex.dwSize = sizeof(INITCOMMONCONTROLSEX);
    iccex.dwICC = ICC_LISTVIEW_CLASSES;
    InitCommonControlsEx(&iccex);

    // create main window
    hwnd = CreateWindowEx(0, L"Main_Window", L"Listview Drag and Drop",
        WS_OVERLAPPEDWINDOW,
        50, 50, 400, 400, NULL, NULL, hInstance, 0);

    ShowWindow(hwnd, nCmdShow);
    UpdateWindow(hwnd);

    while (GetMessage(&Msg, NULL, 0, 0) > 0)
    {
        TranslateMessage(&Msg);
        DispatchMessage(&Msg);
    }

    return Msg.wParam;
}

Now start dragging an item and then move the mouse over/below/above scrollbar thumb -> the behavior you observe is the one I seek.

This program has a flaw:

When I try to drag item back into the client area of the listview, instead of my dragging code, scrollbar still gets controlled. This is the default behavior, but I need to change it in such a way so my dragging code executes instead.

This is the best I was able to do on my own. You can now see what I am trying to do.

If further info is required I will update my post. Meanwhile I will keep trying on my own and update this post if I make progress.

Thank you for your time and help. Best regards.

like image 717
AlwaysLearningNewStuff Avatar asked Apr 16 '15 16:04

AlwaysLearningNewStuff


People also ask

How do you make a scrollable screen in Flutter?

How do you make a scrollable screen in flutter? By default, scrollable widgets in Flutter don't show a scrollbar. To add a scrollbar to a ScrollView, wrap the scroll view widget in a Scrollbar widget. Using this widget we can scroll a widget.

How do I enable scrolling in Linux?

And there you have it! Depending on your terminal, one of these methods may work better than the others, but for most instances, you can scroll up in the Linux terminal using either Shift+PageUp or Ctrl+Shift+PageUp.

What is ScrollController in Flutter?

A scroll controller creates a ScrollPosition to manage the state specific to an individual Scrollable widget. To use a custom ScrollPosition, subclass ScrollController and override createScrollPosition. A ScrollController is a Listenable.


1 Answers

The only way for this approach to work is to find a way to get the mouse move messages while we are scrolling. The mouse is "lost" but the capture still remains to listview(scrollbar). So, when the mouse leaves the scrolling area we need to release the capture(from the scrollbar) and set it again to listview. To accomplish this, we will apply a WH_MOUSE_LL hook when we get the LVN_BEGINDRAG notify message and unhook when we finish the dragging (this is for vertical scroll bar. The idea is exactly the same for horizontal):

HHOOK mouseHook = NULL;
unsigned char g_bDrag = false, g_bScroll = false; //if we are scrolling
unsigned char g_bVsrollExist = false;
RECT scrollRect; //the scrollbar rectangle, in screen coordinates
int thumbTop, thumbBottom; //the y in screen coordinates
int arrowHeight; //the height of the scrollbar up-down arrow buttons

LRESULT CALLBACK WindowProcedure(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam){
    NMHDR *nmr;

    switch(message){ //handle the messages

        case WM_NOTIFY:
            nmr = (NMHDR *)lParam;

            if( nmr->code == LVN_BEGINDRAG ){
                //printf("BeginDrag \n");

                g_bDrag = true;
                SetCapture(hwndListView);  // listview must capture the mouse
                if( g_bVsrollExist == true ){
                    mouseHook = SetWindowsHookEx(WH_MOUSE_LL, (HOOKPROC)LowLevelMouseProc, NULL, NULL);
                }
            }

        default:   //for messages that we don't deal with
            return DefWindowProc(hwnd, message, wParam, lParam);
    }

    return DefWindowProc(hwnd, message, wParam, lParam);
}

LRESULT CALLBACK LowLevelMouseProc(int nCode, WPARAM wParam, LPARAM lParam){
    HWND hwnd;
    MSLLHOOKSTRUCT *mslhs;

    if(nCode == HC_ACTION){
        switch( (int)wParam ){ //handle the messages
            case WM_LBUTTONUP:
                //check if we are dragging and release the mouse and unhook
                if( g_bDrag == true ){
                    g_bDrag = false;
                    g_bScroll = false;

                    hwnd = GetCapture();
                    if( hwnd == hwndListView ){
                        ReleaseCapture();
                    }

                    if( mouseHook != NULL ){UnhookWindowsHookEx(mouseHook); mouseHook = NULL;}
                }

                break;               

            case WM_MOUSEMOVE:
                if( g_bDrag == true ){
                    mslhs = (MSLLHOOKSTRUCT *)lParam;

                    // check if we are outside the area which is: scrollbar area minus the arrow buttons
                    if( mslhs->pt.x < scrollRect.left || mslhs->pt.x >= scrollRect.right ||
                    mslhs->pt.y <= scrollRect.top + arrowHeight + 1 || mslhs->pt.y > scrollRect.bottom - arrowHeight - 1 ){

                        if( g_bScroll == true ){
                            //we need to release the capture from scrollbar
                            ReleaseCapture();

                            //set it again to listview
                            SetTimer(hwndListView, 1, 10, NULL);

                            g_bScroll = false;
                        }
                    }
                }

                break;

            default:   //for messages that we don't deal with
                return CallNextHookEx(NULL, nCode, wParam, lParam);
        }
    }

    return CallNextHookEx(NULL, nCode, wParam, lParam);
}

In the subclassed listview:

LRESULT CALLBACK ListViewWndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam, UINT_PTR uIdSubclass, DWORD_PTR dwrefData){
    POINT pnt;
    SCROLLBARINFO sf;    

    //UNREFERENCED_PARAMETER(uIdSubclass)
    //UNREFERENCED_PARAMETER(dwrefData)

    switch(message){ //handle the messages
        case WM_MOUSEMOVE:
            if( g_bDrag == true && g_bScroll == false && g_bVsrollExist == true ){
                sf.cbSize = sizeof(SCROLLBARINFO);

                GetScrollBarInfo(hwndListView, OBJID_VSCROLL, &sf);

                //in client coordinates
                thumbTop = sf.xyThumbTop;
                thumbBottom = sf.xyThumbBottom;

                //in screen coordinates
                thumbTop += scrollRect.top + 1;
                thumbBottom += scrollRect.top - 2;

                pnt.x = GET_X_LPARAM(lParam);
                pnt.y = GET_Y_LPARAM(lParam);

                ClientToScreen(hwnd, &pnt);

                //we check if we enter the thumb area
                if( pnt.x >= scrollRect.left && pnt.x <= scrollRect.right && 
                    pnt.y > thumbTop + 1 && pnt.y <= thumbBottom - 1 ){
                    g_bScroll = true;
                    SendMessage(hwnd, WM_NCLBUTTONDOWN, (WPARAM)HTVSCROLL, (LPARAM)POINTTOPOINTS(pnt));
                }
            }

            break;

        case WM_TIMER:
            //set the capture to listview to continue getting mouse move messages
            if( (int)wParam == 1 ){
                UpdateWindow(hwndListView);

                SetCapture(hwndListView);

                KillTimer(hwndListView, 1);
            }

            break;

        case WM_LBUTTONDOWN:
            sf.cbSize = sizeof(SCROLLBARINFO);

            GetScrollBarInfo(hwndListView, OBJID_VSCROLL, &sf);

            //check if vertical scrolbar exist
            if( sf.rgstate[0] == STATE_SYSTEM_INVISIBLE ){
                g_bVsrollExist = false;

                break;
            }
            else{g_bVsrollExist = true;}

            arrowHeight = sf.dxyLineButton;
            scrollRect = sf.rcScrollBar;

            //in client coordinates
            thumbTop = sf.xyThumbTop;
            thumbBottom = sf.xyThumbBottom;

            //in screen coordinates
            thumbTop += scrollRect.top + 1;
            thumbBottom += scrollRect.top - 2;

            break;
        case WM_LBUTTONUP:
            if(g_bDrag == true){
                pnt.x = GET_X_LPARAM(lParam);
                pnt.y = GET_Y_LPARAM(lParam);

                g_bDrag = false;
                ReleaseCapture();
            }

            break;

        default:   //for messages that we don't deal with
            return DefSubclassProc(hwnd, message, wParam, lParam);
    }

    return DefSubclassProc(hwnd, message, wParam, lParam);
}

EDIT (default scrolling)

unsigned char scrollUp = false, scrollDown = false, scrollLeft = false, 
    scrollRight = false, scrolling = false, vertScrollIsVisible = false, 
    horzScrollIsVisible = false;
int top, down, left, right; //client window in screen coordinates

LRESULT CALLBACK ListViewWndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam, UINT_PTR uIdSubclass, DWORD_PTR dwrefData){
    POINT pnt;
    SCROLLBARINFO sbiVert, sbiHorz;    

    //UNREFERENCED_PARAMETER(uIdSubclass)
    //UNREFERENCED_PARAMETER(dwrefData)

    switch(message){ //handle the messages
        case WM_MOUSEMOVE:
            pnt.x = GET_X_LPARAM(lParam);
            pnt.y = GET_Y_LPARAM(lParam);

            ClientToScreen(hwnd, &pnt);

            if( g_bDrag == true && (horzScrollIsVisible == true || vertScrollIsVisible == true) ){
                CheckMouse(pnt);
            }

            break;

        case WM_LBUTTONDOWN:
            sbiVert.cbSize = sizeof(SCROLLBARINFO);
            sbiHorz.cbSize = sizeof(SCROLLBARINFO);

            GetScrollBarInfo(hwndListView, OBJID_VSCROLL, &sbiVert);
            GetScrollBarInfo(hwndListView, OBJID_HSCROLL, &sbiHorz);

            if( sbiVert.rgstate[0] == STATE_SYSTEM_INVISIBLE ){
                vertScrollIsVisible = false;
            }
            else{
                vertScrollIsVisible = true;
            }

            if( sbiHorz.rgstate[0] == STATE_SYSTEM_INVISIBLE ){
                horzScrollIsVisible = false;
            }
            else{
                horzScrollIsVisible = true;
            }

            if( vertScrollIsVisible == true ){
                //you can get the header handle with hwndHeader = ListView_GetHeader(hwndListView);
                GetWindowRect(hwndHeader, &rt);

                top = rt.bottom;

                GetWindowRect(hwndListView, &rt);

                if( horzScrollIsVisible == true ){
                    bottom = rt.bottom - sbiHorz.dxyLineButton;
                }
                else{
                    bottom = rt.bottom;
                }
            }

            if( horzScrollIsVisible == true ){
                GetWindowRect(hwndListView, &rt);

                left = rt.left;

                if( vertScrollIsVisible == true ){
                    right = rt.right - sbiVert.dxyLineButton;
                }
                else{
                    right = rt.right;
                }
            }

            break;
        case WM_LBUTTONUP:
            if(g_bDrag == true){
                KillTimer(hwndWin, 1); //hwndWin is your main window
                g_bDrag = false;
                ReleaseCapture();
            }

            break;

        default:   //for messages that we don't deal with
            return DefSubclassProc(hwnd, message, wParam, lParam);
    }

    return DefSubclassProc(hwnd, message, wParam, lParam);
}

void CheckMouse(POINT pnt){
    if( pnt.y < top ){
        scrollUp = true;
        scrollDown = false;
    }
    else if( pnt.y >= bottom ){
        scrollDown = true;
        scrollUp = false;
    }
    else{
        scrollUp = false;
        scrollDown = false;
    }

    if( pnt.x >= right ){
        scrollRight = true;
        scrollLeft = false;
    }
    else if( pnt.x < left ){
        scrollLeft = true;
        scrollRight = false;
    }
    else{
        scrollRight = false;
        scrollLeft = false;
    }

    if( scrollUp == true || scrollDown == true || scrollLeft == true || scrollRight == true ){
        if( scrolling == false ){
            scrolling = true;

            SetTimer(hwndWin, 1, 20, NULL);
        }
    }
    else{
        if( scrolling == true ){
            scrolling = false;

            KillTimer(hwndWin, 1);
        }
    }

    return;
}

LRESULT CALLBACK WindowProcedure(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam){
    NMHDR *nmr;

    switch(message){ //handle the messages

        case WM_NOTIFY:
            nmr = (NMHDR *)lParam;

            if( nmr->code == LVN_BEGINDRAG ){
                //printf("BeginDrag \n");

                g_bDrag = true;
                SetCapture(hwndListView);  // listview must capture the mouse
            }

            break;

        case WM_TIMER:
            if( (int)wParam == 1 ){
                if( scrollUp == true && vertScrollIsVisible == true ){
                    SendMessage(hwndListView, WM_VSCROLL, (WPARAM)0x00000000, (LPARAM)0x0); //up
                }

                if( scrollDown == true && vertScrollIsVisible ){
                    SendMessage(hwndListView, WM_VSCROLL, (WPARAM)0x00000001, (LPARAM)0x0); //down
                }

                if( scrollRight == true && horzScrollIsVisible ){
                    SendMessage(hwndListView, WM_HSCROLL, (WPARAM)0x00000001, (LPARAM)0x0); //right
                }

                if( scrollLeft == true && horzScrollIsVisible  ){
                    SendMessage(hwndListView, WM_HSCROLL, (WPARAM)0x00000000, (LPARAM)0x0); //left
                }
            }

            break;

        default:   //for messages that we don't deal with
            return DefWindowProc(hwnd, message, wParam, lParam);
    }

    return DefWindowProc(hwnd, message, wParam, lParam);
}