前言
Qt中自定义无边框对话框 很少有人这么做, 但是项目上遇到这种需求,对话框使用上存在的问题就是需要实现模态对话框非激活/激活切换时候出现需要出现闪烁效果,增加用户体验,网上没有很明确的解决办法,今天花点时间研究了一下,把实现方式的关键写出来给大家分享。
首先,Qt 没有支持具体平台的类,但是都留有平台相关的类,这里虽然以windows为例,但是linux/mac下类似,都会有平台相关的系统库支持,下面会看到Qt的支持。
Qt 相关类和接口
对话框想要实现边框闪烁, 关键是获取系统的消息,Qt提供两种方式接收处理处理系统消息:
1. QCoreApplication安装NativeEventFilter全局处理的方式
通过void QCoreApplication::installNativeEventFilter(QAbstractNativeEventFilter *filterObj) 函数
安装一个自定义本地事件过滤器QAbstractNativeEventFilter ,
QAbstractNativeEventFilter 过滤处理事件接口:
virtual bool nativeEventFilter(const QByteArray &eventType, void *message, long *result) = 0
2. 实现QWidget 类的bool nativeEvent(const QByteArray &eventType, void *message, long *result) 函数,里面直接处理系统消息
Windows 平台下各个参数的含义:
MSG 即是Windows 下消息的结构体,其他平台从Qt帮助文档也能看到对应的消息类型:
接下来以Windows为例,如何处理我们想要的消息类型,由于我们只想在对画框处理系统消息,所以我使用第二种方式。
处理我们想要的消息
需要头文件:<dwmapi.h>
、<windowsx.h>
bool nativeEvent(const QByteArray &eventType, void *message, long *result)
这里eventType 这里为window 消息类型 "windows_generic_MSG"
不同系统上eventType不同
X11 为 "xcb_generic_event_t"
macOS 为 "mac_generic_NSEvent"
windows 为 "windows_generic_MSG" 和 "windows_dispatcher_MSG"
message windows 下为MSG 的结构体
result为处理后的结果,可以修改
以下为MSG结构体的介绍:
在Windows程序中,消息是由MSG结构体来表示的。MSG结构体的定义如下(参见MSDN):
typedef struct tagMSG {
HWND hwnd;
UINT message;
WPARAM wParam;
LPARAM lParam;
DWORD time;
POINT pt;
} MSG;
第一个成员变量hwnd表示消息所属的窗口。我们通常开发的程序都是窗口应用程序,一个消息一般都是与某个窗口相关联的。例如,在某个活动窗口中按下鼠标左键,产生的按键消息就是发给该窗口的。在Windows程序中,用HWND类型的变量来标识窗口。
第二个成员变量message指定了消息的标识符。在Windows中,消息是由一个数值来表示的,不同的消息对应不同的数值。但是由于数值不便于记忆,所以Windows将消息对应的数值定义为WM_XXX宏(WM是Window Message的缩写)的形式,XXX对应某种消息的英文拼写的大写形式。例如,鼠标左键按下消息是WM_LBUTTONDOWN,键盘按下消息是WM_KEYDOWN,字符消息是WM_CHAR,等等。在程序中我们通常都是以WM_XXX宏的形式来使用消息的。
提示:如果想知道WM_XXX消息对应的具体数值,可以在Visual C++开发环境中选中WM_XXX,然后单击鼠标右键,在弹出菜单中选择goto definition,即可看到该宏的具体定义。跟踪或查看某个变量的定义,都可以使用这个方法。
第三、第四个成员变量wParam和lParam,用于指定消息的附加信息。例如,当我们收到一个字符消息的时候,message成员变量的值就是WM_CHAR,但用户到底输入的是什么字符,那么就由wParam和lParam来说明。wParam、lParam表示的信息随消息的不同而不同。如果想知道这两个成员变量具体表示的信息,可以在MSDN中关于某个具体消息的说明文档查看到。读者可以在VC++的开发环境中通过goto definition查看一下WPARAM和LPARAM这两种类型的定义,可以发现这两种类型实际上就是unsigned int和long。
最后两个变量分别表示消息投递到消息队列中的时间和鼠标的当前位置。
这么多消息类型,那个是和对话框激活状态切换有关的呢?
通过测试和查阅资料,窗口闪烁两个状态分别对应对话框的激活和非激活,以下打印代码:
#ifdef Q_OS_WIN
MSG* msg = (MSG*)message;
msgTimes ++;
qDebug()<<QString(eventType)<<"msg Type:"<<"0x"+QString::number(msg->message,16)<<" times:"<<msgTimes
<<"winId:"<<msg->hwnd<<"Qt winId:"<<"0x"+QString::number(this->winId(),16)
<<"isActive:"<< this->isActiveWindow()<<msg->wParam<<msg->lParam;
#endif
我发现对话框闪烁的时候打印这一行:
"windows_generic_MSG" msg Type: "0x86" times: 2 winId: 0x350b58 Qt winId: "0x350b58" isActive: true 0 0
"windows_generic_MSG" msg Type: "0x86" times: 3 winId: 0x350b58 Qt winId: "0x350b58" isActive: true 1 0
到这里相信大家明白了,就是0x86 这个消息类型,通过查询头文件
发现即是 WM_NCACTIVATE 这个消息类型,MSG所有消息类型的介绍,请参考:
https://blog.youkuaiyun.com/wanghaofeng/article/details/6632165
通过上面对MSG结构体的介绍,我们知道消息带有两个参数 wParam和lParam ,结合上面的程序的输出,我们马上就能猜想到 WParam = true/false 就是代表当前窗口是否激活状态,注意这里的激活和激活不是一般说的激活意思(模态窗口一般都是激活的),我们就表示闪烁的两种状态-明色和暗色吧,网上找到这么一段解释 WM_NCACTIVATE 的wParam参数:
到这里我们就知道怎么办了,通过过滤我们处理 WM_NCACTIVATE 消息,通过其 wParam 参数,我们在 paintEvent里面绘制明暗两种颜色状态,就实现闪烁了-快速多次闪烁时,WM_NCACTIVATE 消息会连续发送多次,wParam 1/0切换。简单贴一下我的代码:
FrameLessDialog::FrameLessDialog(QWidget *parent)
:QDialog(parent),m_actived(false)
{
this->setWindowFlags(Qt::FramelessWindowHint| this->windowFlags());
}
void FrameLessDialog::paintEvent(QPaintEvent *e)
{
QDialog::paintEvent(e);
QColor bkColor = Qt::darkGray;
if(m_actived)
bkColor = Qt::lightGray;
QPainter pt(this);
pt.fillRect(this->rect(),bkColor);
}
bool FrameLessDialog::nativeEvent(const QByteArray &eventType, void *message, long *result)
{
#ifdef Q_OS_WIN
MSG* msg = (MSG*)message;
if(msg->message == WM_NCACTIVATE && (HWND)this->winId() == msg->hwnd ){
m_actived = msg->wParam;
this->update();
}
#endif
return QDialog::nativeEvent(eventType,message,result);
}
注意
if(msg->message == WM_NCACTIVATE && (HWND)this->winId() == msg->hwnd ) 这两个判断条件顺序不能改变,否则有可能会出现段错误