如何在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 等事件,但是如果要在图像上多添加一些形状,那么在 ...
随机推荐
- 【LeetCode】817. Linked List Components 解题报告(Python & C++)
作者: 负雪明烛 id: fuxuemingzhu 个人博客: http://fuxuemingzhu.cn/ 目录 题目描述 题目大意 解题方法 日期 题目地址:https://leetcode.c ...
- 《Head First设计模式》读书笔记
前言:本文是记录我在阅读<Head First设计模式>这本书时,做得相关笔记,相关示例代码地址:design-patterns.由于本书不是将设计原则和设计模式分开讲述的,而是在讲一个设 ...
- TKE 用户故事 - 作业帮 PB 级低成本日志检索服务
作者 吕亚霖,2019年加入作业帮,作业帮架构研发负责人,在作业帮期间主导了云原生架构演进.推动实施容器化改造.服务治理.GO微服务框架.DevOps的落地实践. 莫仁鹏,2020年加入作业帮,作业帮 ...
- C++ switch 语句的用法
C++ 判断 一个 switch 语句允许测试一个变量等于多个值时的情况.每个值称为一个 case,且被测试的变量会对每个 switch case 进行检查. C++ 中 switch 语句的语法: ...
- <数据结构>XDOJ334.分组统计
问题与解答 问题描述 先输入一组数,然后输入其分组,按照分组统计出现次数并输出,参见样例. 输入格式 输入第一行表示样例数m,对于每个样例,第一行为数的个数n,接下来两行分别有n个数,第一行有n个数, ...
- 使用 DML语句,对 “锦图网” 数据进行操作,连接查询(内连接,左外连接,右外连接,全连接)
查看本章节 查看作业目录 需求说明: 对 "锦图网" 数据进行操作: 统计每一种线路类型的线路数量.最高线路价格.最低线路价格和平均线路价格,要求按照线路数量和平均线路价格升序显示 ...
- 关于linux的一点好奇心(一):linux启动过程
一直很好奇,操作系统是如何工作的?我们知道平时编程,是如何让代码跑起来的,但那些都是比较高层次的东西.越往后,你会越觉得,像是空中楼阁,或者说只是有人帮你铺平了许多道理,而你却对此一无所知. 1. 操 ...
- VMware客户端vSphereClient新建虚拟机
1.说明 VMware客户端工具vSphere Client, 用来连接和管理ESX或ESXi主机(下面称为宿主机), 可以方便的创建.管理虚拟机,并分配相应的资源.宿主机就是使用虚拟化软件运行虚拟机 ...
- Linux下校验SHA1和MD5的方法
当我们从互联网下载东西或者从U盘拷贝东西的时候,通常是不会和源文件有什么区别的,但是在偶然的情况下会出现下载或者拷贝出错的情况, 尤其是在下载大文件的时候,比如系统光盘......当你装机到一半才发现 ...
- ajax 异步 提交 含文件的表单
1.前言 需求是使用 jquery 的 ajax 异步提交表单,当然,不是简单的数据,而是包含文件数据的表单.于是我想到了 new FormData() 的用法, 可是仍然提交失败,原来是ajax的属 ...