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.
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.
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.
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.
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.
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.
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);
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With