Click here to Skip to main content
15,867,750 members
Articles / GUI
Tip/Trick

Using SetCapture() and ReleaseCapture() correctly (usually during a drag n' drop operation).

Rate me:
Please Sign up or sign in to vote.
4.97/5 (21 votes)
14 Jan 2011CPOL4 min read 79K   20   9
This tip describes the correct use of the mouse capturing in custom controls and custom windows.

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 HWNDs 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:



  1. WM_LBUTTONDOWN starts the dragging operation and captures the mouse
  2. WM_MOUSEMOVE moves the window along with the cursor
  3. WM_LBUTTONUP does nothing but releases the capture
  4. WM_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:
		// here you can add extra check and decide whether to start
		// the window move or not
		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;

	// THE RIGHT WAY OF DOING IT:
	//*  <- Remove a slash to comment out the good version!
	case WM_LBUTTONUP:
		ReleaseCapture();
		return 0;
	case WM_CAPTURECHANGED:
		g_MovingMainWnd = (HWND)lParam == hWnd;
		return 0;
	/**/

	// THE WRONG WAY OF DOING IT:
	/*  <- Prefix this with a slash to uncomment the bad version!
	case WM_LBUTTONUP:
		g_MovingMainWnd = false;
		ReleaseCapture();
		return 0;
	// buggy programs usually do not handle WM_CAPTURECHANGED at all
	case WM_CAPTURECHANGED:
		break;
	/**/

	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;
}

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
United Kingdom United Kingdom
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
Praisesetcapture-and-releasecapture-correctly Pin
Member 1507861927-Feb-21 5:20
Member 1507861927-Feb-21 5:20 
GeneralMy vote of 5 Pin
Avtem8-Dec-20 4:07
Avtem8-Dec-20 4:07 
QuestionGetCapture() are not applicable? Pin
Vadim SG22-Jan-15 3:24
Vadim SG22-Jan-15 3:24 
AnswerRe: GetCapture() are not applicable? Pin
pasztorpisti23-Jan-15 10:55
pasztorpisti23-Jan-15 10:55 
QuestionExcellent Pin
bramoin2-May-12 10:07
bramoin2-May-12 10:07 
GeneralMy vote of 5 Pin
Member 366954922-Apr-12 16:26
Member 366954922-Apr-12 16:26 
GeneralExcellent Pin
gregthecanuck17-Dec-10 0:15
gregthecanuck17-Dec-10 0:15 
GeneralMy vote of 4 Pin
S.H.Bouwhuis22-Nov-10 1:34
S.H.Bouwhuis22-Nov-10 1:34 
Good stuff. I even went into some of my own code for customers to check whether I had the problem. Turns out I didn't, but it never hurts to make sure.

Thanks for the tip.
GeneralRe: My vote of 4 Pin
VBAdvisor6-Apr-12 17:30
VBAdvisor6-Apr-12 17:30 

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.