概述
提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
- 前言
- 一、说明
- 1、QT版本以及编译器
- 2、主要参考文章以及代码参考
- 3、声明
- 二、基本实现思路
- 三、方案优化
- 1、样式表为状态与Qt事件
- 2、确定需求
- 1.功能需求
- 2.样式需求
- 3、具体实现代码
- 总结
前言
提示:这里可以添加本文要记录的大概内容:
贴靠布局是 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源码代码仅供参考学习。
最后
以上就是淡然西装为你收集整理的Qt 基于win11无边框界面的实现(最大化按钮悬浮弹出snap layout)前言一、说明二、基本实现思路三、方案优化总结的全部内容,希望文章能够帮你解决Qt 基于win11无边框界面的实现(最大化按钮悬浮弹出snap layout)前言一、说明二、基本实现思路三、方案优化总结所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复