将鼠标光标更改为等待光标,然后启动工作线程,在线程完成后将光标更改回来。

3
在一个较老的MFC应用程序中,我需要向另一台计算机执行网络连接请求,如果计算机名称不正确,则可能需要等待几秒钟才能超时。因此,我启动了一个工作线程来进行初始连接,以便用户界面仍然响应。
当用户选择菜单项并弹出对话框填写目标计算机信息时,将触发网络连接请求。当用户在对话框上单击“确定”按钮时,将使用工作线程处理网络连接请求。
我想要做的是将鼠标光标更改为等待指示器,一旦实际建立连接或尝试超时,就将等待指示器删除。
但我遇到的问题是,鼠标光标仍然是指针,并且鼠标光标未更改为等待指示器。
我最初认为可以使用 BeginWaitCursor() 函数更改鼠标光标。 但是,我发现它没有任何效果。
进一步阅读表明,我还需要在 CScrollView 类的 afx_msg BOOL OnSetCursor(CWnd* pWnd, UINT nHitTest, UINT message) 方法中进行覆盖,但是我似乎找不到任何有用的东西来描述我需要在该方法中执行的操作。 OnSetCursor() 方法似乎会因各种原因而被调用,只是移动鼠标就会触发该方法中的断点。
看起来,在 OnSetCursor() 方法中,我应该检测当前应用程序状态,并基于此使用 SetCursor() 函数设置已先前使用 LoadCursor() 加载的可能鼠标光标样式之一。请参见 Prevent MFC application to change cursor back to default icon 以及 Change cursor for the duration of a thread 但是,我不确定是否确实是这样做,以及提供给 OnSetCursor() 的参数实际上意味着什么以及如何使用它们。
在上面的两个SO帖子中,似乎正在使用全局变量来决定是否调用默认的 CView::OnSetCursor() 方法。

@1201ProgramAlarm 我刚试了一下,发现它确实能够将鼠标光标更改为等待指示。但是有一个局部作用域要求,这意味着一旦菜单处理程序启动工作线程然后返回,CWaitCursor对象就会超出范围。我发现如果我使用 BeginWaitCursor(),然后在启动工作线程后放置一个sleep,冻结UI,我也会看到鼠标光标等待指示。所以除了 BeginWaitCursor()之外,似乎还有其他事情我需要做来保持光标为等待光标。 - Richard Chambers
您可以动态创建CWaitCursor对象,但由于主消息线程仍在运行,可能会替换光标。当您尝试获取网络连接时,如何处理用户选择其他菜单选项(或其他交互)?您可能需要使用“连接到服务器”应用程序模态对话框。 - 1201ProgramAlarm
您想更改整个应用程序的光标还是特定窗口的光标? - Constantine Georgiou
@ConstantineGeorgiou 我计划更改整个应用程序的光标。你为什么问? - Richard Chambers
或许可以在主框架窗口中使用 OnSetCursor() - Constantine Georgiou
显示剩余5条评论
2个回答

4

首先声明以下全局变量:

BOOL bConnecting = FALSE; // TRUE if connecting, set by your application
HCURSOR hOldCursor = NULL; // Cursor backup

当你需要显示"沙漏"光标时,请调用:

bConnecting = TRUE;
hOldCursor = SetCursor(LoadCursor(NULL, IDC_WAIT));

一旦连接建立(或失败),请调用:

bConnecting = FALSE;
SetCursor(hOldCursor);
// Alternatively you can call SetCursor(LoadCursor(NULL, IDC_ARROW)); - no need to backup the cursor then
// Or even not restore the cursor at all, it will be reset on the first WM_MOUSEMOVE message (after bConnecting is set to FALSE)

您还需要重写 OnSetCursor()

BOOL CMainFrame::OnSetCursor(CWnd* pWnd, UINT nHitTest, UINT message)
{
    if (bConnecting) return TRUE; // Prevent MFC changing the cursor

    // else call the default
    return CFrameWndEx::OnSetCursor(pWnd, nHitTest, message);
}

CMainFrame的消息映射中添加ON_WM_SETCURSOR()指令,以启用OnSetCursor()消息处理程序。

"主框架"是MFC应用程序中所有窗口的父级,这就是为什么我们要覆盖它的OnSetCursor()函数。它会影响所有其他窗口。

在MFC环境中,您还可以使用BeginWaitCursor()RestoreWaitCursor()EndWaitCursor()函数。这些都是CCmdTarget方法,可以使用AfxGetApp()以及任何CWnd派生类来访问它们。

请注意,在具有UI线程和工作线程的多线程环境中使用全局变量时,取决于线程如何使用和访问该全局变量,可能会创建竞争条件。


设置等待光标(无论是通过BeginWaitCursor()还是CWaitCursor)不足的原因是您之后要处理消息队列。当消息队列正在运行时,鼠标光标下面的窗口会收到WM_SETCURSOR事件,以便在鼠标移动时控制其上方的光标。如果窗口的线程(通常是主线程)很忙,则一次设置等待光标将使此光标可见,直到重置它或处理WM_SETCURSOR为止。 - Nick
@Nick:确切地说,问题在于MFC确实会处理WM_SETCURSOR消息,因此我们需要提供一个重载。在许多代码示例中,我看到在OnSetCursor()(处理WM_SETCURSOR消息)中调用SetCursor(),即使是鼠标移动也会被调用。相反,在我上面提出的解决方案中,我只设置了一次,然后简单地返回TRUE(防止将其改回),直到它被重置。也就是说,OnSetCursor()可能会被调用,但它不会做任何事情,并且返回TRUE将防止系统将其发送到其他窗口。拥有线程不一定需要忙碌。 - Constantine Georgiou
@Richard Chambers:感谢您编辑我的答案。关于您的消息映射注释,Visual Studio中的属性/事件编辑器通常会为您完成此操作(添加声明和实现并修改消息映射),因此该过程是相当自动化的。这是大多数开发人员使用的方式,这就是为什么我没有特别提到它的原因。至于多线程环境,确实需要通过交错或同步访问来访问/修改不同线程的全局变量。 - Constantine Georgiou
@ConstantineGeorgiou,你发布的答案帮了我很大的忙。当我在使用您提出的解决方案实现我的源代码时,遇到了一些问题,这就是编辑的来源。我将它们添加到您的帖子中,只是为了完整起见。许多其他答案只是半途而废,我想至少在这个主题和领域中有一个相对完整的答案。 - Richard Chambers

2

@Constantine的OnSetCursor实现对我没有起作用(VC++ 2013;Win 10)- 等待光标在启动线程后仍然会回到箭头。但是我通过以下代码解决了我的问题。

BOOL CMainFrame::OnSetCursor(CWnd* pWnd, UINT nHitTest, UINT message)
{
    if (bConnecting) {
          SetCursor(LoadCursor(NULL, IDC_WAIT));
          return TRUE; // Prevent MFC changing the cursor
    }
    // else call the default
    return CFrameWndEx::OnSetCursor(pWnd, nHitTest, message);
}

请注意,如果您想在悬停在视图上时显示等待光标,则所有打开的对话框或视图都应具有OnSetCursor函数。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接