公司产品主要基于Qt开发,使用过程中遇到了Qt鼠标移动激活窗口问题,本文稍作记录。

问题

根据产品需求,用户鼠标滑过产品指定View组件时,要求组件边框高亮,且可响应键盘鼠标事件。

乍一看,这个需求挺简单的:通过QWidget的enter/leave事件,当鼠标移动过来时,设置边框高亮;要响应键盘事件,将窗口激活,设置widget为focus状态就可以了。代码很简单:

void Xxxx::enterEvent(QEvent *event)
{
    highlightBorder();

    activateWindow();

    setFocus(Qt::MouseFocusReason);

    ...
}

写完代码,在Visual Studio中调试,完美!代码如预期执行。

遗憾的是,代码提交后,测试很快就把bug指过来了。和测试确认,发现当鼠标移出去点击了其他应用的窗口后,移回来边框能高亮,但不响应鼠标输入事件。这是怎么回事呢?

解决办法

看到bug后,第一反应是:为什么开发时测试没问题,打包后他人用就不行呢?

为了确认问题,在VS下启动程序,功能正常;单独启动应用程序,现象和bug描述一样,鼠标移过来时无法响应键盘事件。这说明在VS调试模式下,应用程序的一些属性和单独使用并不一致,因此才有这个bug。

不响应鼠标事件,说明组件不是focus状态,或者应用程序窗口并未激活。根据现象,判断窗口未激活的可能性更大,于是仔细查看Qt文档关于 activateWindow 函数的说明:

This function performs the same operation as clicking the mouse on the title bar of a top-level window. On X11, the result depends on the Window Manager. If you want to ensure that the window is stacked on top as well you should also call raise(). Note that the window must be visible, otherwise activateWindow() has no effect.

On Windows, if you are calling this when the application is not currently the active one then it will not make it the active window. It will change the color of the taskbar entry to indicate that the window has changed in some way. This is because Microsoft does not allow an application to interrupt what the user is currently doing in another application.

注意到第二段话:On Windows, if you are calling this when the application is not currently the active one then it will not make it the active window. 原来这是Windows平台限制。再检查鼠标点了其他程序窗口后移回来的现象,确实是仅任务栏图标高亮,但窗口并未激活。

Qt选择了尊重Windows系统的限制,但也给出了解决办法,那就是使用QWindowsWindowFunctions::setWindowActivationBehavior函数。针对我们的需求,初始化主窗口时,需做如下设置:

QWindowsWindowFunctions::setWindowActivationBehavior(QWindowsWindowFunctions::AlwaysActivateWindow)

公司使用的是Qt 5.12版本,此版本有个小坑:文档中说用该函数需引入头文件 #include <QWindowsWindowFunctions>,但直接这样写会发现找不到头文件;正确的头文件应该是 #include <QtPlatformHeaders/QWindowsWindowFunctions>,这个小问题在后续版本的文档中已经被修复,但5.12版的文档一直都没改正!

引入辅助函数后,激活窗口的行为按预期执行,问题也就解决了。

参考

1. QWidget Class

2. CMake简明实用教程