Win32 C++泛型函数,用于从UI线程或任意工作线程设置光标(SetCursor在外部线程中不起作用)

前端开发 2026-07-12

我需要在默认光标和等待光标之间切换。 我在应用中看到如果我这样做

// Start Waiting Cursor
SetCursor(LoadCursor(NULL, IDC_WAIT));
// ... App Logic ...
// Resume Normal Cursor when done
SetCursor(LoadCursor(NULL, IDC_ARROW));

这在直接的UI线程代码中能正确工作,但从分离的工作线程代码中却不起作用。为了在工作线程的某些条件下确定等待光标,我目前处理 WM_SETCURSOR,它检查全局变量。

case WM_SETCURSOR:
     if (LOWORD(lParam) == HTCLIENT /* Client area only*/ && 
         threadCondition1 || threadCondition2 || threadCondition3) {
         SetCursor(LoadCursor(NULL, IDC_WAIT));
     } else {
         // Default Arrow Cursor
         return DefWindowProc(hWnd, message, wParam, lParam); 
     }

据Google说,这是因为

SetCursor 在Win32 API中常常在单独线程中失败,因为光标状态(包括可见性和形状)是线程本地的,而不是全局的。要从工作线程更改光标,必须确保 [主UI线程] 处理 WM_SETCURSOR 消息,或者将线程输入附加以同步光标状态,如由 GitHub用户 描述。

  • 解决方案1(推荐): 使用 PostMessageSendMessage 来通知UI线程改变光标形状,确保更改发生在正确的线程上。
  • 解决方案2(替代): 使用 AttachThreadInput 将工作线程的输入状态与UI线程同步,从而让 SetCursor 能在两者之间工作。

然而,我的目标是拥有一个可以从任何地方调用、无需关心上下文的单一便捷函数。 我不喜欢在不同的地方分别实现。 理想情况下,一个通用的函数,例如

void SetAppCursor(boolean waiting) {
     //... If waiting -> Set Waiting Cursor, else Normal Arrow
}

这是否意味着我应该始终在这个函数的主体中执行

PostMessage(hWndMain, WM_SETCURSOR, ...?, ...?)

,即使是UI线程逻辑,也要完全避免 SetCursor?这样做是否有任何缺点?对于这个通用调用,wParam/lParam 的取值应该是什么?

我找不到替代的 AttachThreadInput 的示例,看起来它的使用似乎被不鼓励。

解决方案

是的,你应该让 SetAppCursor() 向UI线程中的一个窗口发布/发送消息,然后让该窗口的处理程序在合适的时机调用 SetCursor()

不过,我建议使用自定义消息来触发改变,而不是直接使用 WM_SETCURSORWM_SETCURSOR 是在鼠标移到你的窗口上时操作系统发送的通知消息。如果在工作线程想更新光标时鼠标并不在你的窗口上,你不应该立即改变光标,避免打断用户的工作。使用自定义消息可以让你的UI线程对变更请求做出反应,而不强制让改动不必要地可见。

例如:

AppCursor.h

void SetAppCursor(bool waiting);

main.c

...

#define APPWM_SETAPPCURSOR (WM_APP+1)

static UINT uiWaitCount = 0;

void SetAppCursor(bool waiting) {
    PostMessage(hWndMain, APPWM_SETAPPCURSOR, (WPARAM)waiting, 0);
}

static HCURSOR GetWindowClassCursor(HWND hWnd) {
    TCHAR szClassName[256];
    GetClassName(hWnd, szClassName, 256);

    WNDCLASS wc;
    HINSTANCE hInstance = (HINSTANCE)GetWindowLong(hWnd, GWL_HINSTANCE);
    GetClassInfo(hInstance, szClassName, &wc);

    return wc.hCursor;
}

static HCURSOR DetermineCursorForWindow(HWND hWnd) {
    if (uiWaitCount > 0)
        return LoadCursor(NULL, IDC_WAIT);

    HCURSOR hCursor = GetWindowClassCursor(hWnd);
    if (hCursor != NULL)
        return hCursor;

    return LoadCursor(NULL, IDC_ARROW);
}

static void UpdateTheCursorForWindow(HWND hWnd) {
    SetCursor(DetermineCursorForWindow(hWnd));
}

static bool IsMouseInWindow(HWND hWnd) {
    POINT pt;
    GetCursorPos(&pt);
    HWND hWndAtPt = WindowFromPoint(pt);
    while (hWndAtPt) {
        if (hWndAtPt == hWnd) return true;
        hWndAtPt = GetParent(hWndAtPt);
    }
    return false;
}

...

LRESULT WINAPI MainWndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
    switch (uMsg) {
        case APPWM_SETAPPCURSOR:
            if (wParam != 0)
                ++uiWaitCount;
            else
                --uiWaitCount;
            if (IsMouseInWindow(hWnd))
                UpdateTheCursorForWindow(hWnd);
            return 0;

        case WM_SETCURSOR:
            if (LOWORD(lParam) == HTCLIENT) {
                UpdateTheCursorForWindow(hWnd);
                return TRUE;
            }
            break;
        ...
    }
    return DefWindowProc(hWnd, uMsg, wParam, lParam);
}

SomeOtherWorkFile.c

#include "AppCursor.h"

// Start Waiting Cursor
SetAppCursor(true);

// ... App Logic ...

// Resume Normal Cursor when done
SetAppCursor(false);

如果你不想修改现有的窗口处理程序,那么你可以在UI线程中创建一个专用的 消息仅窗口 来让 SetAppCursor() 将消息发布/发送给它。

站内所有文章版权归属LeftHeroAI导航站,无授权禁止任何主体转载、抄袭、复制内容,亦不得私自架设镜像站点。一经侵权,本站将通过法律途径追责。

相关文章