DefWindowProc阻塞问题的解法

关键词: C++, Win32, 阻塞, 卡, DefWindowProc, 标题栏, 移动窗口, 窗口大小

问题简介

win32相关编程时,我们经常遇到程序更新受阻。
在大多数平台,不论是原生Win32还是QTSFML等平台,创建的窗口都会在拖动改变大小时卡住我们自己写的Idle处理逻辑,如:

MSG msg = { 0 };
while (true) {
    if (PeekMessageW(&msg, 0, 0, 0, PM_NOREMOVE)) {
        if (GetMessageW(&msg, 0, 0, 0)) {
            TranslateMessage(&msg);
            DispatchMessageW(&msg);
        }
        else break;
    }
    else {
        IdleProcess(false);
    }
}

其中IdleProcess()移动窗口缩放窗口乃至点击标题栏按钮时不执行,准确的说,上述整个消息循环都不再运行。
这对于一些有渲染任务的窗口,如游戏窗口,是十分不好的表现,因为它会使得下一次更新横跨一个巨大的时间步。当然你也可以手动限制时间步上限,不过联机游戏这样处理又会造成同步问题。

大致原因

最开始探索这个问题时,我认为是移动窗口缩放窗口产生大量WM_MOVINGWM_SIZING导致消息队列爆满,从而分支不能到达IdleProceess()
但是简单地加入断点调试后,我们就能发现循环实际卡死在DispatchMessage()中,因为我们只创建了一个窗口,所以很可能是 窗口消息处理函数WndProc()阻塞了。

WndProc()中,唯一不可控的内容就是我们不希望处理消息时调用的DefWindowProc()
果然,简单调试后我们就能发现WndProc()确实停在调用DefWindowProc()处。

LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam);

DefWindowProc()之前输出消息的参数messagelParamwParam,发现以下消息与阻塞相关,且其实际表现经查询资料后被归结在其后:

  • WM_SYSCOMMAND, SC_MOVE:等待 500 ms 左右 无双击 后,开始移动窗口 并 进入 SIZEMOVE Loop
  • WM_SYSCOMMAND, SC_SIZE:开始改变大小 并 立即进入 SIZEMOVE Loop
  • WM_CONTEXTMENU:打开菜单 并 立即进入 MENU loop
  • WM_NCLBUTTONDOWN
  • WM_NCRBUTTONDOWN

其中 SIZEMOVE Loop 和 MENU Loop 是DefWindowProc()中的消息循环。
WM_NCLBUTTONDOWNWM_NCRBUTTONDOWN比较复杂:

WM_NCLBUTTONDOWN(左键点击非客户区)

  • 点击 标题栏 (HTCAPTION) 时,触发WM_SYSCOMMAND, SC_MOVE
  • 点击 图标 (HTSYSMENU) 时,触发WM_CONTEXTMENU
  • 点击 按钮(HTCLOSE, HTMAXBUTTON, HTMINBUTTON, HTHELP) 时,在松开鼠标前不会返回,松开时执行相应功能(即发送相应消息)

WM_NCRBUTTONDOWN(右键点击非客户区)

  • 在松开鼠标前不会返回,松开鼠标时触发WM_CONTEXTMENU

解决方法

解决思路有参考:

主要围绕上述几个消息专门处理

对于进入 DefWindowProc() 内部消息循环

上述进入 SIZEMOVE Loop 和 MENU Loop,即进入DefWindowProc()内部消息循环。
进入之后,其会发布WM_ENTERSIZEMOVEWM_ENTERMENULOOP消息。

需要注意,并不是DefWindowProc()接收到上述消息才进入循环,而是进入循环后才发布消息来通知我们。

对于这种阻塞,我们不能改变其中的默认消息循环,只能使用SetTimer()注册计时器,让计时器以一定频率调用我们的IdleProcess()
这种方法需要我们对 计时器 和 Windows消息机制 有较好的理解。简要地说,GetMessage()PeekMessage在消息队列为空时会进行检查,注册的计时器就是在此时更新的。于是DefWindowProc()内部的默认消息循环也会处理我们的计时器,就使我们的IdleProcess()可以继续运行了。

我们既可以专门写一个回调函数由计时器调用:

void CALLBACK TimerProc(HWND hWnd, UINT message, UINT_PTR nTimerid, DWORD systemTick) {
	return IdleProcess();
}

且使用

    SetTimer(0, 0, TIMER_ELAPSE, (TIMERPROC)TimerProcess);

也可以不使用回调函数,直接在WndProc()中处理WM_TIMER消息:

    case WM_TIMER:
        IdleProcess();
        break;
对于在非客户区按下鼠标右键

我们可以拦截WM_NCRBUTTONDOWN消息,再在WM_NCRBUTTONUP中发送消息来打开菜单,基本思想如此:

	case WM_NCRBUTTONDOWN:
		break;
	case WM_NCRBUTTONUP:
		PostMessageW(hWnd, WM_CONTEXTMENU, 0, lParam);
		break;

当然,我们需要更多逻辑来避免鼠标按下和松开的位置不同时打开菜单。

对于在非客户区按下鼠标左键

如果按下的是按钮,那么可以简单地拦截按下消息然后在松开时发送相应消息:

	switch(wParam) {
	case HTMAXBUTTON: {
		bool wndMaxed = GetWindowLongPtrW(hWnd, GWL_STYLE) & WS_MAXIMIZE;
		PostMessageW(hWnd, WM_SYSCOMMAND, wndMaxed ? SC_RESTORE : SC_MAXIMIZE, lParam);
		break;
	}
	case HTMINBUTTON:
		PostMessageW(hWnd, WM_SYSCOMMAND, SC_MINIMIZE, lParam);
		break;
	case HTCLOSE:
		PostMessageW(hWnd, WM_SYSCOMMAND, SC_CLOSE, lParam);
		break;
	case HTHELP:
		break;
	default:
		return DefWindowProcW(hWnd, message, wParam, lParam);
	}

而标题栏有两种功能:
一是双击,与最大化按钮一致;
二是拖动,移动窗口。
我们必须拦截WM_NCLBUTTONDOWN消息,所以我们只能自己处理移动:

	static WPARAM l_lastHitNC = HTNOWHERE;
	/* ... */
	case WM_NCMOUSEMOVE:
		if (l_lastHitNC == HTCAPTION) {
			l_lastHitNC = HTNOWHERE;
			PostMessageW(hWnd, WM_SYSCOMMAND, SC_MOVE | HTCAPTION, lParam);
			PostMessageW(hWnd, WM_MOUSEMOVE, MK_LBUTTON, 0);
		}
		else return DefWindowProcW(hWnd, message, wParam, lParam);
		break;
	case WM_NCLBUTTONDOWN:
		if (wParam == HTCAPTION || wParam == HTCLOSE ||
			wParam == HTMAXBUTTON || wParam == HTMINBUTTON || wParam == HTHELP) {
			l_lastHitNC = wParam;
			PostMessageW(hWnd, WM_ACTIVATE, WA_CLICKACTIVE, 0);
		}
		else {
			l_lastHitNC = HTNOWHERE;
			return DefWindowProcW(hWnd, message, wParam, lParam);
		}
		break;
	/* .. */

其中

	PostMessageW(hWnd, WM_MOUSEMOVE, MK_LBUTTON, 0);

的作用是让SC_MOVE触发的移动不再等待 500 ms。

经过测试,这种处理方法:

  • 在 Windows 7 SP1 无效,SC_MOVE仍可能等待 500 ms
  • 在 Windows 11 有效,SC_MOVE确实不会再等待,而是直接开始移动
  • 其他版本未测试

总结起来就得到了全部解决方案,例子如下:

/**
* MIT License
*
* Copyright (c) 2023 Tyler Parret True
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
* 
* Tyler Parret True (OwlHowlinMornSky) <mysteryworldgod@outlook.com>
*/

#include <stdio.h>
#include <Windows.h>


#define IDLE_DEBUG_OUTPUT
#define DEFAULT_ELAPSE 20
#define TIMER_ELAPSE   50


bool    MyRegistClass(HINSTANCE);

LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
void    CALLBACK TimerProc(HWND, UINT, UINT_PTR, DWORD);


#ifdef IDLE_DEBUG_OUTPUT

HANDLE hStdOut = 0;

void IdleProcess(bool state) {
	// TODO
	static int cnt = 0;
	WCHAR tmp[64] = { 0 };
	swprintf_s(tmp, 64, L"Idle Process. Message Pump State: %s.", state ? L"In System" : L"Normal");
	WriteConsoleW(hStdOut, tmp, static_cast<DWORD>(wcslen(tmp)), 0, 0);
	for (int i = 0; i < cnt; ++i)
		tmp[i] = L'.';
	tmp[cnt] = '\n';
	tmp[cnt + 1] = '\0';
	cnt = (cnt + 1) % 16;
	WriteConsoleW(hStdOut, tmp, static_cast<DWORD>(wcslen(tmp)), 0, 0);
	return;
}

#else

void IdleFunc(bool state) {
	// TODO
	return;
}

#endif // IDLE_DEBUG_OUTPUT


int APIENTRY wWinMain(_In_ HINSTANCE hInstance,
					  _In_opt_ HINSTANCE hPrevInstance,
					  _In_ LPWSTR    lpCmdLine,
					  _In_ int       nCmdShow) {
	UNREFERENCED_PARAMETER(hPrevInstance);
	UNREFERENCED_PARAMETER(lpCmdLine);

#ifdef IDLE_DEBUG_OUTPUT
	AllocConsole();
	hStdOut = GetStdHandle(STD_OUTPUT_HANDLE);
#endif // IDLE_DEBUG_OUTPUT

	if (!MyRegistClass(hInstance)) {
		MessageBoxW(0, L"Regist Class: Failed!", L"Error", MB_ICONERROR);
		return EXIT_FAILURE;
	}

	// TODO

	HWND hWnd0 = CreateWindowW(L"MY_TEST_CLASS_OHMS", L"MY_TEST_WINDOW",
							   WS_OVERLAPPEDWINDOW | WS_CLIPCHILDREN | WS_CLIPSIBLINGS,
							   CW_USEDEFAULT, CW_USEDEFAULT, 640, 480,
							   0, 0, hInstance, 0);
	if (!hWnd0) {
		MessageBoxW(NULL, L"Create Window: Failed!", L"Error", MB_ICONERROR);
		return EXIT_FAILURE;
	}

	ShowWindow(hWnd0, nCmdShow);
	UpdateWindow(hWnd0);

	// Message Pump
	MSG msg = { 0 };
	while (true) {
		if (PeekMessageW(&msg, 0, 0, 0, PM_NOREMOVE)) {
			if (GetMessageW(&msg, 0, 0, 0)) {
				TranslateMessage(&msg);
				DispatchMessageW(&msg);
			}
			else break;
		}
		else {
			IdleProcess(false);
			Sleep(DEFAULT_ELAPSE);
		}
	}
	DestroyWindow(hWnd0);

	// TODO

	return EXIT_SUCCESS;
}


bool MyRegistClass(HINSTANCE hInst) {
	WNDCLASSEXW wcex = { 0 };
	wcex.cbSize = sizeof(WNDCLASSEX);
	wcex.hbrBackground = (HBRUSH)(COLOR_WINDOWFRAME);
	wcex.hCursor = LoadCursorW(nullptr, IDC_ARROW);
	wcex.hInstance = hInst;
	wcex.lpfnWndProc = WndProc;
	wcex.lpszClassName = L"MY_TEST_CLASS_OHMS";
	wcex.style = CS_HREDRAW | CS_VREDRAW;
	return (RegisterClassExW(&wcex) != 0);
}


void CALLBACK TimerProc(HWND hWnd, UINT message, UINT_PTR nTimerid, DWORD systemTick) {
	IdleProcess(true);
	return;
}


LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) {

	static WPARAM l_lastHitNC = HTNOWHERE;
	static UINT_PTR l_timerID = 0;

	switch (message) {

	case WM_CREATE: {
		CreateWindowW(L"BUTTON", L"TEST",
					  WS_TABSTOP | WS_VISIBLE | WS_CHILD | WS_CLIPSIBLINGS | BS_DEFPUSHBUTTON,
					  10, 10, 50, 50,
					  hWnd, 0, 0, 0);
		// TODO
		break;
	}

	case WM_DESTROY: {
		// TODO
		break;
	}

	case WM_PAINT: {
		PAINTSTRUCT ps;
		HDC hdc = BeginPaint(hWnd, &ps);
		// TODO
		EndPaint(hWnd, &ps);
		break;
	}

	case WM_CLOSE:
		PostQuitMessage(0);
		break;

	case WM_NCMOUSEMOVE:
		if (l_lastHitNC == HTCAPTION) {
			l_lastHitNC = HTNOWHERE;
			PostMessageW(hWnd, WM_SYSCOMMAND, SC_MOVE | HTCAPTION, lParam);
			PostMessageW(hWnd, WM_MOUSEMOVE, MK_LBUTTON, 0);
		}
		else return DefWindowProcW(hWnd, message, wParam, lParam);
		break;

	case WM_NCLBUTTONDOWN:
		if (wParam == HTCAPTION || wParam == HTCLOSE ||
			wParam == HTMAXBUTTON || wParam == HTMINBUTTON || wParam == HTHELP) {
			l_lastHitNC = wParam;
			PostMessageW(hWnd, WM_ACTIVATE, WA_CLICKACTIVE, 0);
		}
		else {
			l_lastHitNC = HTNOWHERE;
			return DefWindowProcW(hWnd, message, wParam, lParam);
		}
		break;

	case WM_NCLBUTTONUP:
		if (l_lastHitNC == wParam) {
			l_lastHitNC = HTNOWHERE;
			switch (wParam) {
			case HTMAXBUTTON: {
				bool wndMaxed = GetWindowLongPtrW(hWnd, GWL_STYLE) & WS_MAXIMIZE;
				PostMessageW(hWnd, WM_SYSCOMMAND, wndMaxed ? SC_RESTORE : SC_MAXIMIZE, lParam);
				break;
			}
			case HTMINBUTTON:
				PostMessageW(hWnd, WM_SYSCOMMAND, SC_MINIMIZE, lParam);
				break;
			case HTCLOSE:
				PostMessageW(hWnd, WM_SYSCOMMAND, SC_CLOSE, lParam);
				break;
			case HTHELP:
				break;
			default:
				return DefWindowProcW(hWnd, message, wParam, lParam);
			}
		}
		else {
			l_lastHitNC = HTNOWHERE;
			return DefWindowProcW(hWnd, message, wParam, lParam);
		}
		break;

	case WM_NCRBUTTONDOWN:
		if (l_lastHitNC == HTNOWHERE && (wParam == HTCAPTION || wParam == HTCLOSE ||
			wParam == HTMAXBUTTON || wParam == HTMINBUTTON || wParam == HTHELP)) {
			l_lastHitNC = wParam | 0x8000;
			PostMessageW(hWnd, WM_ACTIVATE, WA_CLICKACTIVE, 0);
		}
		else
			l_lastHitNC = HTNOWHERE;
		break;

	case WM_NCRBUTTONUP:
		if (l_lastHitNC & 0x8000) {
			l_lastHitNC = HTNOWHERE;
			PostMessageW(hWnd, WM_CONTEXTMENU, 0, lParam);
		}
		else
			l_lastHitNC = HTNOWHERE;
		break;

	case WM_ENTERMENULOOP:
		if (l_timerID) {
			KillTimer(0, l_timerID);
		}
		l_timerID = SetTimer(0, 0, TIMER_ELAPSE, (TIMERPROC)TimerProc);
		break;

	case WM_EXITMENULOOP:
		if (l_timerID) {
			KillTimer(0, l_timerID);
		}
		l_timerID = 0;
		break;

	case WM_ENTERSIZEMOVE:
		if (l_timerID) {
			KillTimer(0, l_timerID);
		}
		l_timerID = SetTimer(0, 0, TIMER_ELAPSE, (TIMERPROC)TimerProc);
		break;

	case WM_EXITSIZEMOVE:
		if (l_timerID) {
			KillTimer(0, l_timerID);
		}
		l_timerID = 0;
		break;

	default:
		return DefWindowProcW(hWnd, message, wParam, lParam);
	}
	return 0;
}

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值