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.
LVN_BEGINDRAG
handler, please see the SSCCE above. At the moment I can not conclude more from your comment. Please be patient, as I am preparing important edit as I type this comment. Thank you for trying to help. Best regards. – LowrieWM_NCLBUTTONDOWN
message you lose the mouse. The cursor is captured by the scrollbar. You don't get any messages to check if the cursor has left the scrollbar area any more. Another problem with this method is when you move the mouse not on the thumb but on the up-down arrows of the scrollbar. – Evocative