The mouse capture is a windows feature that can direct all mouse messages to a single window or control (HWND
) even if the cursor is outside of the specified window. I assume that you are familiar with this feature and the related API (SetCapture(HWND)
, ReleaseCapture()
, and WM_CAPTURECHANGED
) and I want to show you a common mistake that developers often make. There are some custom controls and custom windows that sometimes need to capture the mouse, usually for the time of completing an operation. The window chooser (crosshair) control of spy++ is a good example, it allows you to drag a crosshair over a window on your desktop to select a HWND
. Spy++ needs to capture the mouse because during mouse capture the HWND
s under the cursor never receive WM_SETCURSOR
messages, so windows of other programs don't have the chance to change the cursor (crosshair) that is defined by spy++. After establishing the mouse capture for example for your file list dragging/copying operation, you can call SetCursor()
to setup a dragging cursor and it won't change until you release the capture even if the user drags the cursor over the window of other applications. Don't be afraid of calling SetCursor()
, WM_SETCURSOR
does this millions of times on nearly all mouse messages, after releasing the capture someone will soon receive a mouse message and a WM_SETCURSOR
in response to this restoring the original cursor. If you want to be extra safe, then you can call SetCursor()
also at the end of the operation. My example program creates a window that you can move by dragging its client area. This behavior could be achieved just by returning HTCAPTION
when the DefWindowProc()
would return HTCLIENT
in response to the WM_NCHITTEST
window message, but with that solution, your main loop isn't running, as if you were dragging the window by its title bar (because windows runs its own message loop to do the job). Some programs (media players, games) usually make the whole window ownerdraw and provide the window-dragging feature with a codepiece found in my example program. This way, your main message loop keeps running. This window move feature needs the capture because if you press down the left mouse button and then pull out the cursor from the client area outside the window crazily the window mover code still wants to receive the WM_MOUSEMOVE
message.
Let's see how is this "moving by grabbing the client area" feature works:
WM_LBUTTONDOWN
starts the dragging operation and captures the mouseWM_MOUSEMOVE
moves the window along with the cursorWM_LBUTTONUP
does nothing but releases the captureWM_CAPTURECHANGED
ends the dragging operation
The main point of this article is the thing that you should do inside your WM_LBUTTONUP
and WM_CAPTURECHANGED
handler. It is important that WM_LBUTTONUP
just releases the capture and only WM_CAPTURECHANGED
ends the dragging operation!!! The reason for this is that the dragging operation can be ended for example by pressing ALT+TAB. In this case, you do not receive a WM_LBUTTONUP
message, but you still lose your capture and WM_CAPTURECHANGED
is received ending the drag operation. If you put the "drag operation ending" code to your WM_LBUTTONUP
then your program still loses the capture on ALT+TAB but another big window may come to the forground masking your whole window that logically still thinks that it is being dragged (so it is in inconsistent state).
So the conclusion is, always put the ending of the operation to your WM_CAPTURECHANGED
handler, and call ReleaseCapture()
from other places where you want to end the operation!!! These places can be anywhere, in the WM_LBUTTONUP
, WM_MOUSEMOVE
handlers...
Compile my example code, and try to bring a big window to the foreground with ALT+TAB so that it covers the whole window of my example program while you are dragging it by its client area. Everything will work fine even after releasing the mouse button and switching back to the example program. Hovering over the example window is OK. Then comment out the WM_LBUTTONUP
and WM_CAPTURECHANGED
handlers and uncomment the buggy versions of these! Try the ALT+TAB again! After ALT+TAB, release the left mouse button and switch back to our window with ALT+TAB and try to hover with the cursor over our window, and try extreme cursor speeds as well! The window behaves crazy until you click on it with the left button to serve it a WM_LBUTTONUP
message! :laugh:
#include <windows.h>
HINSTANCE g_hInstance = (HINSTANCE)GetModuleHandle(NULL);
HWND g_hMainWnd = NULL;
bool g_MovingMainWnd = false;
POINT g_OrigCursorPos;
POINT g_OrigWndPos;
LRESULT CALLBACK MainWndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
switch (uMsg)
{
case WM_LBUTTONDOWN:
if (GetCursorPos(&g_OrigCursorPos))
{
RECT rt;
GetWindowRect(hWnd, &rt);
g_OrigWndPos.x = rt.left;
g_OrigWndPos.y = rt.top;
g_MovingMainWnd = true;
SetCapture(hWnd);
SetCursor(LoadCursor(NULL, IDC_SIZEALL));
}
return 0;
case WM_LBUTTONUP:
ReleaseCapture();
return 0;
case WM_CAPTURECHANGED:
g_MovingMainWnd = (HWND)lParam == hWnd;
return 0;
case WM_MOUSEMOVE:
if (g_MovingMainWnd)
{
POINT pt;
if (GetCursorPos(&pt))
{
int wnd_x = g_OrigWndPos.x +
(pt.x - g_OrigCursorPos.x);
int wnd_y = g_OrigWndPos.y +
(pt.y - g_OrigCursorPos.y);
SetWindowPos(hWnd, NULL, wnd_x,
wnd_y, 0, 0, SWP_NOACTIVATE|
SWP_NOOWNERZORDER|SWP_NOZORDER|
SWP_NOSIZE);
}
}
return 0;
case WM_DESTROY:
PostQuitMessage(0);
return 0;
}
return DefWindowProc(hWnd, uMsg, wParam, lParam);
}
bool CreateMainWnd()
{
static const char CLASS_NAME[] = "MainWndClass";
WNDCLASS wc;
wc.cbClsExtra = 0;
wc.cbWndExtra = 0;
wc.hbrBackground = (HBRUSH)(COLOR_WINDOW+1);
wc.hCursor = LoadCursor(NULL, IDC_ARROW);
wc.hIcon = LoadIcon(NULL, IDI_APPLICATION);
wc.hInstance = g_hInstance;
wc.lpfnWndProc = &MainWndProc;
wc.lpszClassName = CLASS_NAME;
wc.lpszMenuName = NULL;
wc.style = CS_HREDRAW | CS_VREDRAW | CS_DBLCLKS;
if (!RegisterClass(&wc))
return false;
g_hMainWnd = CreateWindowEx(
0,
CLASS_NAME,
"Main Window",
WS_OVERLAPPED | WS_SYSMENU | WS_MINIMIZEBOX | WS_MAXIMIZEBOX,
CW_USEDEFAULT, CW_USEDEFAULT, 300, 200,
NULL,
NULL,
g_hInstance,
NULL
);
return true;
}
int main()
{
if (!CreateMainWnd())
return -1;
ShowWindow(g_hMainWnd, SW_SHOW);
UpdateWindow(g_hMainWnd);
MSG msg;
while (GetMessage(&msg, NULL, 0, 0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
return (int)msg.wParam;
}