提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
前言
提示:这里可以添加本文要记录的大概内容:
贴靠布局是 Windows 11 中的一项新功能,用户可通过该布局了解强大的窗口贴靠功能。 通过将鼠标悬停在窗口的最大化按钮上或按 Win + Z,可以轻松访问对齐布局。
在Qt界面开发中,为了使UI界面更加协调,或者说标题栏需要添加自定义的一些功能或者控件时,常常使用无边框窗口进行设计符合自己需求的标题栏,然而当去掉系统边框之后,在普通按钮上悬浮又不会弹出snap layout布局,强烈的好奇心下,就产生一个可以支持win11 snap layout 的无边框窗口。
本文主要目的在于如何使Qt的无边框窗口支持win11 snap layout ,先来放一下最终的实现效果图。
提示:以下是本篇文章正文内容,下面案例可供参考
一、说明
1、QT版本以及编译器
Qt版本: Qt5.9.9
编译器:MSVC2015 64bit
2、主要参考文章以及代码参考
Windows平台Qt无边款窗口技术细节
这里还要感谢该篇文章大佬提供的思路以及帮助,很有耐心的解决我的疑问。
windows系统实现无边框,同时支持Aero效果
代码是在这个开源项目的基础之上进行修改的,是一个很完美的Qt无边框解决方案,具体无边框的细节可以参考这个,下面是一个翻译的文章
基于QMainWindow 实现的效果很好的 Qt 无边框窗口
3、声明
由于本人知识有限,如有什么错误或者不足的地方,还请各位大佬帮忙指出。
二、基本实现思路
由于window目前并没有提供一个API接口来调用snap layout,所以就想通过消息欺骗的方式使系统自己调用snap layout,通俗说,我提供给系统一个假消息,某某按钮就是最大化按钮,然后交由系统处理。
其中
WM_NCHITTEST消息
就是用来发送到窗口以确定窗口的哪个部分对应于特定的屏幕坐标。 例如,当光标移动、按下或释放鼠标按钮或响应对 WindowFromPoint 等函数的调用时,可能会发生这种情况。
如果未捕获鼠标,则会将消息发送到光标下方的窗口。 否则,消息将发送到已捕获鼠标的窗口。
那我们需要做的是WM_NCHITTEST消息的返回值置为最大化按钮,即命中测试返回HTMAXBUTTON。
这里MSDN提供的一个方案。
支持为 Windows 11 上的桌面应用使用贴靠布局
。
LRESULT CALLBACK TestWndProc(HWND window, UINT msg, WPARAM wParam, LPARAM lParam)
{
switch (msg)
{
case WM_NCHITTEST:
{
// Get the point in screen coordinates.
// GET_X_LPARAM and GET_Y_LPARAM are defined in windowsx.h
POINT point = { GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam) };
// Map the point to client coordinates.
::MapWindowPoints(nullptr, window, &point, 1);
// If the point is in your maximize button then return HTMAXBUTTON
if (::PtInRect(&m_maximizeButtonRect, point))
{
return HTMAXBUTTON;
}
}
break;
}
return ::DefWindowProcW(window, msg, wParam, lParam);
}
上述方案在Qt中的代码实现
case WM_NCHITTEST:
{
*result = 0;
const LONG border_width = m_borderWidth;
RECT winrect;
GetWindowRect(HWND(winId()), &winrect);
//x,y 为鼠标在屏幕的坐标
long x = GET_X_LPARAM(msg->lParam);
long y = GET_Y_LPARAM(msg->lParam);
//...
//...
if (0!=*result) return true;
//*result still equals 0, that means the cursor locate OUTSIDE the frame area
//but it may locate in titlebar area
if (!m_titlebar) return false;
//support highdpi
double dpr = this->devicePixelRatioF();
QPoint pos = m_titlebar->mapFromGlobal(QPoint(x/dpr,y/dpr));
if (!m_titlebar->rect().contains(pos)) return false;
QWidget* child = m_titlebar->childAt(pos);
if (!child)
{
*result = HTCAPTION;
return true;
}else{
if (m_whiteList.contains(child))
{
*result = HTCAPTION;
return true;
}
if(mMaxBtnHelper->isValid()){ //鼠标位于标题栏中最大化
if(mMaxBtnHelper->AbstractButton() == child){
mMaxBtnHelper->setInRectBtnFlag(true);
*result = HTMAXBUTTON; //最大化
return true; //返回为true,截获信息,并提供一个虚假的信息,告诉系统这个区域为最大化区域,即可实现下消息欺骗。
}
}
}
return false;
} //end case WM_NCHITTEST
三、方案优化
当然上面这个方法是有副作用的,即当我们截获鼠标信息以后
最大化区域内无法响应鼠标事件,不能点击,相对对应的悬停、按下等效果都失效。所以需要在
WM_NCLBUTTONDOWN、WM_NCLBUTTONUP、WM_NCMOUSEHOVER、WM_NCMOUSEMOVE等消息中,转换成WM_MOUSEMOVE等相关鼠标消息发送给按钮。
Windows平台Qt无边款窗口技术细节
,接下来详细实现win对应鼠标消息发送相应的事件交由QT的事件处理机制,来恢复该区域的相对应的事件,以及悬浮、按下样式。
1、样式表为状态与Qt事件
QPushButton:{ background-color:rgb(180 , 200, 255); }
QPushButton:hover{ background-color:rgb(220 , 200 , 255); }
QPushButton:pressed{ background-color:rgb(200 , 250 , 0); }";
上面 :hover、pressed 为样式表中常见
伪状态
,
用户在操作时,可以根据不同的交互状态展示不同的用户样式,界面能够识别用户操作,不需要代码控制即可响应不同状态下的样式。
其中我们所用到的伪状态hover和press与Qt对应的事件如下。
normal -> :hover ---- QEvent::Enter
:hover -> normal ---- QEvent::Leave
normal -> :pressed ---- QEvent::MouseButtonPress
:pressed -> normal ---- QEvent::MouseButtonRelease
清楚了这些,样式的恢复思路就清晰了。如果我们想达到hover样式,只需要发送QEvent::Enter就可以了
(注意:发送QEvent::Ente与QEvent::Leave需要发送update事件重绘一下按钮,不然-样式不会生效)。
2、确定需求
以最大化按钮为例,可具体为功能需求和样式需求。
1.功能需求
如图,正常情况下,鼠标释放,窗口响应最大化,非正常情况下,窗口维持原状。
对于正常2和非正常2情况,
当鼠标从按钮左键按下不松开移动到窗口客户区以后,鼠标将不在进入WM_NCHITTEST命中测试,此时发送鼠标按钮外释放事件,在此之后所有的鼠标释放均为WM_LBUTTONUP,此时WM_MOUSEMOVE中判断鼠标释放时刻的位置。
如果鼠标再次回到按钮区域内,发送QEvent::MouseButtonPress与QEvent::MouseButtonRelease模拟鼠标点击事件,完成最大化功能。
2.样式需求
这里需要特殊说明的是,当鼠标 进入->按下->按钮内释放(释放后不移动鼠标),按钮将会处于hover状态,所以在鼠标在按钮内释放时,发送QEvent::MouseButtonRelease的同时,发送QEvent::Leave。
3、具体实现代码
case WM_LBUTTONUP:
// qDebug() << " ========== WM_LBUTTONUP ========== " << countAll++;
mMinBtnHelper->mouseRealseDeal(result, false);
mMaxBtnHelper->mouseRealseDeal(result, false);
mCloseBtnHelper->mouseRealseDeal(result, false);
return false;
//鼠标在客户区移动
case WM_MOUSEMOVE:
{
//qDebug() << " ========== WM_MOUSEMOVE ========== " << countAll++;
*result = 0;
// 鼠标按下的情况下,第一次从按钮移入客户区,发送鼠标释放的事件
if(mMinBtnHelper->isFirstMove()){
mMinBtnHelper->setMoveInClientFirst(false);
mMinBtnHelper->sendMouseRelease(false);
//qDebug() << " ========== MOVE mMinBtnHelper MOVE ========== " << countAll++;
}
if(mMaxBtnHelper->isFirstMove()){
mMaxBtnHelper->setMoveInClientFirst(false);
mMaxBtnHelper->sendMouseRelease(false);
//qDebug() << " ========== MOVE mMaxBtnHelper MOVE ========== " << countAll++;
}
if(mCloseBtnHelper->isFirstMove()){
mCloseBtnHelper->setMoveInClientFirst(false);
mCloseBtnHelper->sendMouseRelease(false);
//qDebug() << " ========== MOVE mCloseBtnHelper MOVE ========== " << countAll++;
}
mMinBtnHelper->mouseEnterLeaveDeal();
mMaxBtnHelper->mouseEnterLeaveDeal();
mCloseBtnHelper->mouseEnterLeaveDeal();
//x,y 为鼠标在窗口客户区的坐标
long x = GET_X_LPARAM(msg->lParam);
long y = GET_Y_LPARAM(msg->lParam);
//support highdpi
double dpr = this->devicePixelRatioF();
QPoint pos = QPoint(x/dpr,y/dpr);
if (!m_titlebar->rect().contains(pos)) return false;
QWidget* child = m_titlebar->childAt(pos);
if (child){
if (m_whiteList.contains(child))
{
*result = HTCAPTION;
return true;
}
if(mMinBtnHelper->isValid()){ //鼠标位于标题栏中最小化按钮
if(mMinBtnHelper->AbstractButton() == child){
mMinBtnHelper->setInRectBtnFlag(true);
}
}
if(mMaxBtnHelper->isValid()){ //鼠标位于标题栏中最大化
if(mMaxBtnHelper->AbstractButton() == child){
mMaxBtnHelper->setInRectBtnFlag(true);
}
}
if(mCloseBtnHelper->isValid()){ //鼠标位于标题栏中关闭按钮
if(mCloseBtnHelper->AbstractButton() == child){
mCloseBtnHelper->setInRectBtnFlag(true);
}
}
}
return false;
}
//非客户端区域鼠标左键按下
case WM_NCLBUTTONDOWN:
{
// qDebug() << "********** WM_NCLBUTTONDOWN ****************" << countAll++;
// 处理鼠标事件
mMinBtnHelper->mouseEnterLeaveDeal();
mMaxBtnHelper->mouseEnterLeaveDeal();
mCloseBtnHelper->mouseEnterLeaveDeal();
if(msg->wParam == HTMINBUTTON){ //最小化按钮
if(mMinBtnHelper->mousePressDeal(result))
return true;
}
else if(msg->wParam == HTMAXBUTTON){ //最大化按钮
if(mMaxBtnHelper->mousePressDeal(result))
return true;
}
else if(msg->wParam == HTCLOSE){ //关闭按钮
if(mCloseBtnHelper->mousePressDeal(result))
return true;
}
return false; //
}
//非客户端区域鼠标左键释放
case WM_NCLBUTTONUP:
{
// qDebug() << "========= WM_NCLBUTTONUP ****************" << countAll++;
if(msg->wParam == HTMINBUTTON){ //最小化按钮
if(mMinBtnHelper->mouseRealseDeal(result))
return true;
}
else if(msg->wParam == HTMAXBUTTON){ //最大化按钮
if(mMaxBtnHelper->mouseRealseDeal(result))
return true;
}
else if(msg->wParam == HTCLOSE){ //关闭按钮
if(mCloseBtnHelper->mouseRealseDeal(result))
return true;
}
mMinBtnHelper->releaseFlag();
mMaxBtnHelper->releaseFlag();
mCloseBtnHelper->releaseFlag();
return false;
}
case WM_NCHITTEST:
{
//qDebug() << "********** WM_NCHITTEST ****************" << countAll++;
mMinBtnHelper->mouseEnterLeaveDeal();
mMaxBtnHelper->mouseEnterLeaveDeal();
mCloseBtnHelper->mouseEnterLeaveDeal();
*result = 0;
const LONG border_width = m_borderWidth;
RECT winrect;
GetWindowRect(HWND(winId()), &winrect);
//x,y 为鼠标在屏幕的坐标
long x = GET_X_LPARAM(msg->lParam);
long y = GET_Y_LPARAM(msg->lParam);
if(m_bResizeable)
{
bool resizeWidth = minimumWidth() != maximumWidth();
bool resizeHeight = minimumHeight() != maximumHeight();
if(resizeWidth)
{
//left border
if (x >= winrect.left && x < winrect.left + border_width)
{
*result = HTLEFT;
}
//right border
if (x < winrect.right && x >= winrect.right - border_width)
{
*result = HTRIGHT;
}
}
if(resizeHeight)
{
//bottom border
if (y < winrect.bottom && y >= winrect.bottom - border_width)
{
*result = HTBOTTOM;
}
//top border
if (y >= winrect.top && y < winrect.top + border_width)
{
*result = HTTOP;
}
}
if(resizeWidth && resizeHeight)
{
//bottom left corner
if (x >= winrect.left && x < winrect.left + border_width &&
y < winrect.bottom && y >= winrect.bottom - border_width)
{
*result = HTBOTTOMLEFT;
}
//bottom right corner
if (x < winrect.right && x >= winrect.right - border_width &&
y < winrect.bottom && y >= winrect.bottom - border_width)
{
*result = HTBOTTOMRIGHT;
}
//top left corner
if (x >= winrect.left && x < winrect.left + border_width &&
y >= winrect.top && y < winrect.top + border_width)
{
*result = HTTOPLEFT;
}
//top right corner
if (x < winrect.right && x >= winrect.right - border_width &&
y >= winrect.top && y < winrect.top + border_width)
{
*result = HTTOPRIGHT;
}
}
}
if (0!=*result) return true;
//*result still equals 0, that means the cursor locate OUTSIDE the frame area
//but it may locate in titlebar area
if (!m_titlebar) return false;
//support highdpi
double dpr = this->devicePixelRatioF();
QPoint pos = m_titlebar->mapFromGlobal(QPoint(x/dpr,y/dpr));
if (!m_titlebar->rect().contains(pos)) return false;
QWidget* child = m_titlebar->childAt(pos);
if (!child)
{
*result = HTCAPTION;
return true;
}else{
if (m_whiteList.contains(child))
{
*result = HTCAPTION;
return true;
}
if(mMinBtnHelper->isValid()){ //鼠标位于标题栏中最小化按钮
if(mMinBtnHelper->AbstractButton() == child){
mMinBtnHelper->setInRectBtnFlag(true);
*result = HTMINBUTTON; //最小化
return true;
}
}
if(mMaxBtnHelper->isValid()){ //鼠标位于标题栏中最大化
if(mMaxBtnHelper->AbstractButton() == child){
mMaxBtnHelper->setInRectBtnFlag(true);
*result = HTMAXBUTTON; //最大化
return true;
}
}
if(mCloseBtnHelper->isValid()){ //鼠标位于标题栏中关闭按钮
if(mCloseBtnHelper->AbstractButton() == child){
mCloseBtnHelper->setInRectBtnFlag(true);
*result = HTCLOSE; //关闭
return true;
}
}
}
return false;
} //end case WM_NCHITTEST
总结
以上就是本文要讲的内容,有些地方可能描述的不太清楚,具体可以看源码实现。
CSDN源码
积分多的大佬,可以采用此方式下载
GitCode源码
代码仅供参考学习。