如何在pyqt中在实现无边框窗口的同时保留Windows窗口动画效果(一)
无边框窗体的实现思路
在pyqt中只要 self.setWindowFlags(Qt.FramelessWindowHint) 就可以实现边框的去除,但是没了标题栏也意味着窗口大小无法改变、窗口无法拖拽和窗口阴影的消失。网上有很多介绍pyqt自定义标题栏的方法,几乎都是通过处理 mousePressEvent 、 mouseReleaseEvent 以及 mouseMoveEvent 来实现的,在移动的过程中是可以看到窗口的内容的。在没有给窗口打开Windows的亚克力效果时这种方法还能凑合着用,如果给窗口加上了亚克力效果,移动窗口就会非常卡。
在《PYQT5 实现 无frame窗口的拖动和放缩》中,作者重写了nativeEvent,将qt的消息转换为Windows的 MSG,确实可以直接还原去除边框前的移动窗口和窗口放缩效果,但还是无法还原Windows的窗口最大化和最小化动画。然后这篇博客《Qt无边框窗体(Windows)》中,作者很详细地介绍了C++的实现方法,看完这篇博客之后开始着手用 ctypes 和 pywin32 模块来翻译作者提供的那些代码。从代码中可以看到作者把 msg->lParam 强转为各种结构体,里面存着窗口的信息。有些结构体用继承 ctypes.Structure 之后再加上 ctype.wintypes的一些数据类型是可以实现的,但是有的变量类型比如 PWINDOWPOS 在 ctype.wintypes 里面找不到。对于这种情况就用VS2019把C++代码转成dll来直接调用。
更完整的且不依赖于 C++ dll 的无边框解决方案请移步《如何在pyqt中自定义无边框窗口》。
效果
- 窗口拖动和贴边最大化

- 窗口拉伸

具体实现过程
自定义标题栏
对于窗口移动,只要在自定义标题栏的 mousePressEvent 中调用 win32gui.ReleaseCapture() 和 win32api.SendMessage(hWnd,message,wParam,lParam)就可以实现,具体代码如下(用到的资源文件和其他按钮的代码放在了文末的链接中,可以自取):
import sys
from ctypes.wintypes import HWND
from win32.lib import win32con
from win32.win32api import SendMessage
from win32.win32gui import ReleaseCapture
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QIcon, QPixmap, QResizeEvent
from PyQt5.QtWidgets import QApplication, QHBoxLayout, QLabel, QWidget
from effects.window_effect import WindowEffect
from .title_bar_buttons import BasicButton, MaximizeButton
class TitleBar(QWidget):
""" 定义标题栏 """
def __init__(self, parent):
super().__init__(parent)
self.resize(1360, 40)
# self.win = parent
# 实例化无边框窗口函数类
self.windowEffect = WindowEffect()
self.setAttribute(Qt.WA_TranslucentBackground)
# 实例化小部件
self.title = QLabel('Groove 音乐', self)
self.createButtons()
# 初始化界面
self.initWidget()
self.adjustButtonPos()
def createButtons(self):
""" 创建各按钮 """
self.minBt = BasicButton([
{'normal': r'resource\images\titleBar\透明黑色最小化按钮_57_40.png',
'hover': r'resource\images\titleBar\绿色最小化按钮_hover_57_40.png',
'pressed': r'resource\images\titleBar\黑色最小化按钮_pressed_57_40.png'},
{'normal': r'resource\images\titleBar\白色最小化按钮_57_40.png',
'hover': r'resource\images\titleBar\绿色最小化按钮_hover_57_40.png',
'pressed': r'resource\images\titleBar\黑色最小化按钮_pressed_57_40.png'}], self)
self.closeBt = BasicButton([
{'normal': r'resource\images\titleBar\透明黑色关闭按钮_57_40.png',
'hover': r'resource\images\titleBar\关闭按钮_hover_57_40.png',
'pressed': r'resource\images\titleBar\关闭按钮_pressed_57_40.png'},
{'normal': r'resource\images\titleBar\透明白色关闭按钮_57_40.png',
'hover': r'resource\images\titleBar\关闭按钮_hover_57_40.png',
'pressed': r'resource\images\titleBar\关闭按钮_pressed_57_40.png'}], self)
self.returnBt = BasicButton([
{'normal': r'resource\images\titleBar\黑色返回按钮_60_40.png',
'hover': r'resource\images\titleBar\黑色返回按钮_hover_60_40.png',
'pressed': r'resource\images\titleBar\黑色返回按钮_pressed_60_40.png'},
{'normal': r'resource\images\titleBar\白色返回按钮_60_40.png',
'hover': r'resource\images\titleBar\白色返回按钮_hover_60_40.png',
'pressed': r'resource\images\titleBar\白色返回按钮_pressed_60_40.png'}], self, iconSize_tuple=(60, 40))
self.maxBt = MaximizeButton(self)
self.button_list = [self.minBt, self.maxBt,
self.closeBt, self.returnBt]
def initWidget(self):
""" 初始化小部件 """
self.setFixedHeight(40)
self.setStyleSheet("QWidget{background-color:transparent}\
QLabel{font:14px 'Microsoft YaHei Light'; padding:10px 15px 10px 15px;}")
# 隐藏抬头
self.title.hide()
# 将按钮的点击信号连接到槽函数
self.minBt.clicked.connect(self.window().showMinimized)
self.maxBt.clicked.connect(self.showRestoreWindow)
self.closeBt.clicked.connect(self.window().close)
def adjustButtonPos(self):
""" 初始化小部件位置 """
self.title.move(self.returnBt.width(), 0)
self.closeBt.move(self.width() - 57, 0)
self.maxBt.move(self.width() - 2 * 57, 0)
self.minBt.move(self.width() - 3 * 57, 0)
def resizeEvent(self, e: QResizeEvent):
""" 尺寸改变时移动按钮 """
self.adjustButtonPos()
def mouseDoubleClickEvent(self, event):
""" 双击最大化窗口 """
self.showRestoreWindow()
def mousePressEvent(self, event):
""" 移动窗口 """
# 判断鼠标点击位置是否允许拖动
if self.isPointInDragRegion(event.pos()):
ReleaseCapture()
SendMessage(self.window().winId(), win32con.WM_SYSCOMMAND,
win32con.SC_MOVE + win32con.HTCAPTION, 0)
event.ignore()
# 也可以通过调用windowEffect.dll的接口函数来实现窗口拖动
# self.windowEffect.moveWindow(HWND(int(self.parent().winId())))
def showRestoreWindow(self):
""" 复原窗口并更换最大化按钮的图标 """
if self.window().isMaximized():
self.window().showNormal()
# 更新标志位用于更换图标
self.maxBt.setMaxState(False)
else:
self.window().showMaximized()
self.maxBt.setMaxState(True)
def isPointInDragRegion(self, pos) -> bool:
""" 检查鼠标按下的点是否属于允许拖动的区域 """
x = pos.x()
condX = (60 < x < self.width() - 57 * 3)
return condX
def setWhiteIcon(self, isWhiteIcon):
""" 设置图标颜色 """
for button in self.button_list:
button.setWhiteIcon(isWhiteIcon)
WindowEffect 类
对于还原窗口动画,调用 win32gui.GetWindowLong()和win32gui.SetWindowLong 重新设置一下窗口动画即可,这个函数定义在了WindowEffect 中,这个类可以用来实现窗口的各种效果,包括 Win7 的 AERO 效果、Win10的亚克力效果、DWM绘制的窗口阴影和移动窗口等,不过要想成功使用这个类必须在Visual Studio里面装好C++,不然调用windowEffect.dll 时候会报错找不到相关的依赖项(免安装MSVC的无边框窗口解决方案见《如何在pyqt中自定义无边框窗口》)。WindowEffect 的代码如下所示,其中 setWindowAnimation(hWnd) 用来还原了窗口动画:
# coding:utf-8
from ctypes import c_bool, cdll
from ctypes.wintypes import DWORD, HWND,LPARAM
from win32 import win32gui
from win32.lib import win32con
class WindowEffect():
""" 调用windowEffect.dll来设置窗口外观 """
dll = cdll.LoadLibrary('dll\\windowEffect.dll')
def setAcrylicEffect(self,
hWnd: HWND,
gradientColor: str = 'FF000066',
accentFlags: bool = False,
animationId: int = 0):
""" 开启亚克力效果,gradientColor对应16进制的rgba四个分量 """
# 设置阴影
if accentFlags:
accentFlags = DWORD(0x20 | 0x40 | 0x80 | 0x100)
else:
accentFlags = DWORD(0)
# 设置和亚克力效果相叠加的背景颜色
gradientColor = gradientColor[6:] + gradientColor[4:6] + \
gradientColor[2:4] + gradientColor[:2]
gradientColor = DWORD(int(gradientColor, base=16))
animationId = DWORD(animationId)
self.dll.setAcrylicEffect(hWnd, accentFlags, gradientColor,
animationId)
def setAeroEffect(self, hWnd: HWND):
""" 开启Aero效果 """
self.dll.setAeroEffect(hWnd)
def setShadowEffect(self,
class_amended: c_bool,
hWnd: HWND,
newShadow=True):
""" 去除窗口自带阴影并决定是否添加新阴影 """
class_amended = c_bool(
self.dll.setShadowEffect(class_amended, hWnd, c_bool(newShadow)))
return class_amended
def addShadowEffect(self, shadowEnable: bool, hWnd: HWND):
""" 直接添加新阴影 """
self.dll.addShadowEffect(c_bool(shadowEnable), hWnd)
def setWindowFrame(self, hWnd: HWND, left: int, top, right, buttom):
""" 设置客户区的边框大小 """
self.dll.setWindowFrame(hWnd, left, top, right, buttom)
def setWindowAnimation(self, hWnd):
""" 打开窗口动画效果 """
style = win32gui.GetWindowLong(hWnd, win32con.GWL_STYLE)
win32gui.SetWindowLong(
hWnd, win32con.GWL_STYLE, style | win32con.WS_MAXIMIZEBOX
| win32con.WS_CAPTION
| win32con.CS_DBLCLKS
| win32con.WS_THICKFRAME)
def adjustMaximizedClientRect(self, hWnd: HWND, lParam: int):
""" 窗口最大化时调整大小 """
self.dll.adjustMaximizedClientRect(hWnd, LPARAM(lParam))
def moveWindow(self,hWnd:HWND):
""" 移动窗口 """
self.dll.moveWindow(hWnd)
无边框窗口
正如第二篇博客所说的那样,如果还原窗口动画,就会导致窗口最大化时尺寸超过正确的显示器尺寸,甚至最大化之后又会有新的标题栏跑出来,要解决这个问题必须在 nativeEvent 中处理两个消息,一个是WM_NCCALCSIZE,另一个则是 WM_GETMINMAXINFO。处理WM_GETMINMAXINFO时用到的结构体 MINMAXINFO 如下所示:
class MINMAXINFO(Structure):
_fields_ = [
("ptReserved", POINT),
("ptMaxSize", POINT),
("ptMaxPosition", POINT),
("ptMinTrackSize", POINT),
("ptMaxTrackSize", POINT),
]
这个结构体中每个参数的定义都可以在MSDN的文档中找到,里面介绍的很详细。处理 WM_NCCALCSIZE 时需要将msg.lParam转换为结构体NCCALCSIZE_PARAMS的指针,这个是结构NCCALCSIZE_PARAMS的第二个成员的类型。为了偷懒,我在动态链接库中加了这个函数的接口里面,adjustMaximizedClientRect(HWND hWnd, LPARAM lParam)具体代码如下:
void adjustMaximizedClientRect(HWND hWnd, LPARAM lParam)
{
auto monitor = ::MonitorFromWindow(hWnd, MONITOR_DEFAULTTONULL);
if (!monitor) return;
MONITORINFO monitor_info{};
monitor_info.cbSize = sizeof(monitor_info);
if (!::GetMonitorInfoW(monitor, &monitor_info)) return;
auto& params = *reinterpret_cast<NCCALCSIZE_PARAMS*>(lParam);
params.rgrc[0] = monitor_info.rcWork;
}
这个函数拿来在窗口最大化时重新设置窗口大小和坐标,下面给出整个无边框窗体的代码,里面的重点就是重写 nativeEvent:
# coding:utf-8
import sys
from ctypes import POINTER,cast,Structure
from ctypes.wintypes import HWND, MSG, POINT
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QApplication, QWidget
from PyQt5.QtWinExtras import QtWin
from win32 import win32api, win32gui
from win32.lib import win32con
from effects.window_effect import WindowEffect
from my_title_bar import TitleBar
class Window(QWidget):
BORDER_WIDTH = 5
def __init__(self, parent=None):
super().__init__(parent)
self.titleBar = TitleBar(self)
self.windowEffect = WindowEffect()
self.hWnd = HWND(int(self.winId()))
self.__initWidget()
def __initWidget(self):
""" 初始化小部件 """
self.resize(800, 600)
self.setWindowFlags(Qt.FramelessWindowHint)
# 还原窗口动画
self.windowEffect.setWindowAnimation(self.winId())
# 打开亚克力效果
self.setStyleSheet('QWidget{background:transparent}') # 必须用qss开实现背景透明,不然会出现界面卡顿
self.windowEffect.setAcrylicEffect(self.hWnd,'F2F2F260')
def isWindowMaximized(self, hWnd: int) -> bool:
""" 判断窗口是否最大化 """
# 返回指定窗口的显示状态以及被恢复的、最大化的和最小化的窗口位置,返回值为元组
windowPlacement = win32gui.GetWindowPlacement(hWnd)
if not windowPlacement:
return False
return windowPlacement[1] == win32con.SW_MAXIMIZE
def nativeEvent(self, eventType, message):
""" 处理windows消息 """
msg = MSG.from_address(message.__int__())
if msg.message == win32con.WM_NCHITTEST:
# 处理鼠标拖拽消息
xPos = win32api.LOWORD(msg.lParam) - self.frameGeometry().x()
yPos = win32api.HIWORD(msg.lParam) - self.frameGeometry().y()
w, h = self.width(), self.height()
lx = xPos < self.BORDER_WIDTH
rx = xPos + 9 > w - self.BORDER_WIDTH
ty = yPos < self.BORDER_WIDTH
by = yPos > h - self.BORDER_WIDTH
# 左上角
if (lx and ty):
return True, win32con.HTTOPLEFT
# 右下角
elif (rx and by):
return True, win32con.HTBOTTOMRIGHT
# 右上角
elif (rx and ty):
return True, win32con.HTTOPRIGHT
# 左下角
elif (lx and by):
return True, win32con.HTBOTTOMLEFT
# 顶部
elif ty:
return True, win32con.HTTOP
# 底部
elif by:
return True, win32con.HTBOTTOM
# 左边
elif lx:
return True, win32con.HTLEFT
# 右边
elif rx:
return True, win32con.HTRIGHT
# 处理窗口最大化消息
elif msg.message == win32con.WM_NCCALCSIZE:
if self.isWindowMaximized(msg.hWnd):
self.windowEffect.adjustMaximizedClientRect(
HWND(msg.hWnd), msg.lParam)
return True, 0
elif msg.message == win32con.WM_GETMINMAXINFO:
if self.isWindowMaximized(msg.hWnd):
window_rect = win32gui.GetWindowRect(msg.hWnd)
if not window_rect:
return False, 0
# 获取显示器句柄
monitor = win32api.MonitorFromRect(window_rect)
if not monitor:
return False, 0
# 获取显示器信息
monitor_info = win32api.GetMonitorInfo(monitor)
monitor_rect = monitor_info['Monitor']
work_area = monitor_info['Work']
# 将lParam转换为MINMAXINFO指针
info = cast(msg.lParam, POINTER(MINMAXINFO)).contents
# 调整窗口大小
info.ptMaxSize.x = work_area[2] - work_area[0]
info.ptMaxSize.y = work_area[3] - work_area[1]
info.ptMaxTrackSize.x = info.ptMaxSize.x
info.ptMaxTrackSize.y = info.ptMaxSize.y
# 修改左上角坐标
info.ptMaxPosition.x = abs(
window_rect[0] - monitor_rect[0])
info.ptMaxPosition.y = abs(
window_rect[1] - monitor_rect[1])
return True, 1
return QWidget.nativeEvent(self, eventType, message)
def resizeEvent(self, e):
""" 改变标题栏大小 """
super().resizeEvent(e)
self.titleBar.resize(self.width(), 40)
# 更新最大化按钮图标
if self.isWindowMaximized(int(self.winId())):
self.titleBar.maxBt.setMaxState(True)
if __name__ == '__main__':
app = QApplication(sys.argv)
demo = Window()
demo.show()
sys.exit(app.exec_())
写在最后
以上就是Windows上的无边框窗体解决方案,代码我放在了百度网盘(提取码:1yhl)中,对你有帮助的话就点个赞吧(~ ̄▽ ̄)~
如何在pyqt中在实现无边框窗口的同时保留Windows窗口动画效果(一)的更多相关文章
- 如何在pyqt中给无边框窗口添加DWM环绕阴影
前言 在之前的博客<如何在pyqt中通过调用SetWindowCompositionAttribute实现Win10亚克力效果>中,我们实现了窗口的亚克力效果,同时也用SetWindowC ...
- 如何在pyqt中自定义无边框窗口
前言 之前写过很多关于无边框窗口并给窗口添加特效的博客,按照时间线罗列如下: 如何在pyqt中实现窗口磨砂效果 如何在pyqt中实现win10亚克力效果 如何在pyqt中通过调用SetWindowCo ...
- 如何在pyqt中通过调用 SetWindowCompositionAttribute 实现Win10亚克力效果
亚克力效果 在<如何在pyqt中实现窗口磨砂效果>和<如何在pyqt中实现win10亚克力效果>中,我们调用C++ dll来实现窗口效果,这种方法要求电脑上必须装有MSVC.V ...
- 如何在 pyqt 中捕获并处理 Alt+F4 快捷键
前言 如果在 Windows 系统的任意一个窗口中按下 Alt+F4,默认行为是关闭窗口(或者最小化到托盘).对于使用了亚克力效果的窗口,使用 Alt+F4 最小化到托盘,再次弹出窗口的时候可能出现亚 ...
- 如何在pyqt中实现窗口磨砂效果
磨砂效果的实现思路 这两周一直在思考怎么在pyqt上实现窗口磨砂效果,网上搜了一圈,全都是 C++ 的实现方法.正好今天查python的官方文档的时候看到了 ctypes 里面的 HWND,想想倒不如 ...
- 如何在pyqt中实现win10亚克力效果
亚克力效果的实现思路 上一篇博客<如何在pyqt中实现窗口磨砂效果> 中实现了win7中的Aero效果,但是和win10的亚克力效果相比,Aero还是差了点内味.所以今天早上又在网上搜了一 ...
- 如何在pyqt中实现带动画的动态QMenu
弹出菜单的视觉效果 QLineEdit 原生的菜单弹出效果十分生硬,而且样式很丑.所以照着Groove中单行输入框弹出菜单的样式和动画效果写了一个可以实现动态变化Item的弹出菜单,根据剪贴板的内容是 ...
- SB中设置UITextField 无边框,真机上输入汉字聚焦时,文字 下沉
解决方案:sb中一定要设置有边框,然后在代码里设置成无边框 然后正常了. 参考:https://segmentfault.com/q/1010000007244564/a-10200000073481 ...
- 如何在pyqt中使用 QGraphicsView 实现图片查看器
前言 在 PyQt 中可以使用很多方式实现照片查看器,最朴素的做法就是重写 QWidget 的 paintEvent().mouseMoveEvent 等事件,但是如果要在图像上多添加一些形状,那么在 ...
随机推荐
- 如何把 MySQL 备份验证性能提升 10 倍
JuiceFS 非常适合用来做 MySQL 物理备份,具体使用参考我们的官方文档.最近有个客户在测试时反馈,备份验证的数据准备(xtrabackup --prepare)过程非常慢.我们借助 Juic ...
- SOFA 通信
私有通信协议设计: 我们的分布式架构,所需要的内部通信模块,采用了私有协议来设计和研发. 可以有效地利用协议里的各个字段 灵活满足各种通信功能需求:比如 CRC 校验,Server Fail-Fast ...
- Java面向对象笔记 • 【第2章 面向对象进阶】
全部章节 >>>> 本章目录 2.1 成员变量 2.1.1 成员变量与局部变量的区别 2.1.2 成员变量的使用 2.1.3 实践练习 2.2 this关键字 2.2.1 ...
- Hadoop(HDFS,YARN)的HA集群安装
搭建Hadoop的HDFS HA及YARN HA集群,基于2.7.1版本安装. 安装规划 角色规划 IP/机器名 安装软件 运行进程 namenode1 zdh-240 hadoop NameNode ...
- .net core系列源码地址介绍
很早就想写.net core相关教程内容了,但是一方面感觉东西太多了,一方面是太懒了,最近才下定决心,一定要写点东西出来,希望能支持一下国内.net 的尴尬处境 好了,先从.net core开源开始吧 ...
- java 多态 总结
1.前言 引用教科书解释: 多态是同一个行为具有多个不同表现形式或形态的能力. 多态就是同一个接口,使用不同的实例而执行不同操作. 通俗来说: 总结:多态的抽象类与接口有点相似: 父类不需要具体实现方 ...
- Spark词频前十的统计练习
注:图片如果损坏,点击文章链接:https://www.toutiao.com/i6815390070254600712/ 承接上一个文档<Spark本地环境实现wordCount单词计数> ...
- Python面向对象时最常见的3类方法
为了节省读友的时间,先上结论(对于过程和细节感兴趣的读友可以继续往下阅读,一探究竟): [结论] 类中定义的方法类型 关键词 本质含义 如何定义 如何调用 使用场景举例 实例方法 一般无任何修饰时,默 ...
- stm32单片机利用ntc热敏电阻温度转换公式C语言版
首先 我们需要明确电路结构 热敏电阻的原理就不再赘述,本文不凑字数,只讲干货 必须要知道的就是串联电阻R9程序中定义为resistanceInSeries ,精度越高越好 为了方便,先在程序中定义好你 ...
- Kong 微服务网关在 Kubernetes 的实践
来源:分布式实验室译者:qianghaohao本文主要介绍将 Kong 微服务网关作为 Kubernetes (https://www.alauda.cn)集群统一入口的最佳实践,之前写过一篇文章使用 ...