Dear ImGui游戏窗口双光标问题
2024-03-17 17:33:41

双光标问题

向游戏窗口注入 ImGui 窗口后,默认情况下就会出现双光标。

解决

更新光标的地方在imgui_impl_win32.cpp文件中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
static bool ImGui_ImplWin32_UpdateMouseCursor()
{
ImGuiIO& io = ImGui::GetIO();
if (io.ConfigFlags & ImGuiConfigFlags_NoMouseCursorChange)
return false;

ImGuiMouseCursor imgui_cursor = ImGui::GetMouseCursor();
if (imgui_cursor == ImGuiMouseCursor_None || io.MouseDrawCursor)
{
// Hide OS mouse cursor if imgui is drawing it or if it wants no cursor
::SetCursor(NULL);
}
else
{
// Show OS mouse cursor
LPTSTR win32_cursor = IDC_ARROW;
switch (imgui_cursor)
{
case ImGuiMouseCursor_Arrow: win32_cursor = IDC_ARROW; break;
case ImGuiMouseCursor_TextInput: win32_cursor = IDC_IBEAM; break;
case ImGuiMouseCursor_ResizeAll: win32_cursor = IDC_SIZEALL; break;
case ImGuiMouseCursor_ResizeEW: win32_cursor = IDC_SIZEWE; break;
case ImGuiMouseCursor_ResizeNS: win32_cursor = IDC_SIZENS; break;
case ImGuiMouseCursor_ResizeNESW: win32_cursor = IDC_SIZENESW; break;
case ImGuiMouseCursor_ResizeNWSE: win32_cursor = IDC_SIZENWSE; break;
case ImGuiMouseCursor_Hand: win32_cursor = IDC_HAND; break;
case ImGuiMouseCursor_NotAllowed: win32_cursor = IDC_NO; break;
}
::SetCursor(::LoadCursor(NULL, win32_cursor));
}
return true;
}

从代码能看出,如果不希望出现光标,也就是执行::SetCursor(::LoadCursor(NULL, win32_cursor))这句,那么有两个地方可以修改

  1. ConfigFlags加上ImGuiConfigFlags_NoMouseCursorChange参数,函数将完全失效。
  2. io.MouseDrawCursor这个值为true时(默认值为false)。

方式一

在创建 ImGui 时为ConfigFlags加上ImGuiConfigFlags_NoMouseCursorChange参数,使ImGui_ImplWin32_UpdateMouseCursor函数彻底失效

1
2
3
ImGui::CreateContext();
ImGuiIO& io = ImGui::GetIO();
io.ConfigFlags |= ImGuiConfigFlags_NoMouseCursorChange;

接着在窗口消息处理过程中,自行决定是否渲染光标,所以需要处理WM_SETCURSOR消息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
LRESULT WINAPI MyWndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
if (ImGui_ImplWin32_WndProcHandler(hWnd, uMsg, wParam, lParam)) {
return ERROR_SUCCESS;
}

if (uMsg == WM_SETCURSOR) {
// 如果鼠标在 ImGui 窗口内就渲染一个箭头光标
::SetCursor(io.WantCaptureMouse ? ::LoadCursorW(nullptr, IDC_ARROW) : nullptr);
return ERROR_SUCCESS;
}

return ::CallWindowProcW(_oldWndProc, hWnd, uMsg, wParam, lParam);
}

但有瑕疵

当光标在输入框,窗口边框等情况下,它任然会显示箭头,所以还需要对每种情况进行处理,这使得工作变得复杂。
查看源码发现,ImGui 会在ImGui_ImplWin32_WndProcHandler中调用ImGui_ImplWin32_UpdateMouseCursor

1
2
3
4
5
6
7
8
9
IMGUI_IMPL_API LRESULT ImGui_ImplWin32_WndProcHandler(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
...
case WM_SETCURSOR:
if (LOWORD(lParam) == HTCLIENT && ImGui_ImplWin32_UpdateMouseCursor())
return 1;
return 0;
...
}

所以也可以换个思路,去掉ImGuiConfigFlags_NoMouseCursorChange设置,并在消息中拦截处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
LRESULT WINAPI MyWndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
ImGuiIO& io = ImGui::GetIO();
if (LOWORD(lParam) == HTCLIENT && uMsg == WM_SETCURSOR) {
// 鼠标不在外挂窗口内时不要渲染光标
if (!io.WantCaptureMouse) {
::SetCursor(nullptr);
return ERROR_SUCCESS;
}

// 外挂窗口隐藏时就返回,不要交给 ImGui 处理,否则会出现箭头光标
if (!_show) {
return ERROR_SUCCESS;
}
}

if (ImGui_ImplWin32_WndProcHandler(hWnd, uMsg, wParam, lParam)) {
return ERROR_SUCCESS;
}

return ::CallWindowProcW(_oldWndProc, hWnd, uMsg, wParam, lParam);
}

还没完,通常外挂窗口可以通过快捷键隐藏,当通过快捷键隐藏窗口时也要处理光标

1
2
3
4
5
6
7
if (uMsg == WM_KEYUP) {
if (wParam == VK_F4) {
_show = !_show;
::SetCursor(_show ? ::LoadCursorW(nullptr, IDC_ARROW) : nullptr);
return ERROR_SUCCESS;
}
}

方式二

ImGui_ImplWin32_UpdateMouseCursor的逻辑看,当io.MouseDrawCursortrue时,ImGui 就不会使用系统光标,而是自行绘制光标

1
2
3
ImGui::CreateContext();
io.ConfigFlags |= ImGuiConfigFlags_NoMouseCursorChange; // 屏蔽系统光标
io.MouseDrawCursor = true; // ImGui 自行绘制

此时就需要在每一帧创建后指定光标类型

1
2
3
ImGui::NewFrame();
if (!io.WantCaptureMouse)
ImGui::SetMouseCursor(ImGuiMouseCursor_None);

工作一切正常,很完美,实现起来比方式一更简单。

参考

SDL2 hide cursor
Panorama cursor fix (imgui)
ImGUI mouse cursor problem