如何在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】143. Reorder List 解题报告(Python)
[LeetCode]143. Reorder List 解题报告(Python) 标签(空格分隔): LeetCode 作者: 负雪明烛 id: fuxuemingzhu 个人博客: http://f ...
- IntelliJ IDEA打war包
1.按ctrl+alt+shift+s键打开Project Structure,点击+号图标,选择"Artifacts->Web Application Archive" 2 ...
- [数学]高数部分-Part III 中值定理与一元微分学应用
Part III 中值定理与一元微分学应用 回到总目录 Part III 中值定理与一元微分学应用 1. 中值定理 费马定理 罗尔定理 拉格朗日中值定理 柯西中值定理 柯西.拉格朗日.罗尔三者间的关系 ...
- 【JPA】Spring Data JPA 实现分页和条件查询
文章目录 1.在`Repository`层继承两个接口 2.在Service层进行查询操作 3.Page的方法 1.在Repository层继承两个接口 JpaRepository<Admin, ...
- Notepad++插件Base64编解码
我们平常进行Base64编码需要自己写代码转换, 或者使用其他人编写的小工具程序, 也可以使用在线base64编码工具, 现在我们还可以使用Notepad++自带的插件, 进行Base64编码和解码, ...
- SpringBoot 中拦截器的简介及使用方式
拦截器简介 拦截器通常通过动态代理的方式来执行. 拦截器的生命周期由IoC容器管理,可以通过注入等方式来获取其他Bean的实例,使用更方便. 拦截器配置使用方式 实现拦截器接口: import jav ...
- Dom 键盘事件以及实战案例
键盘事件 //键盘操作 //1.某键盘按下执行的操作 document是对文档进行触发 document.onkeyup = function(){ console.log('你好') } docum ...
- C# winform 遍历所有页面的所有控件 ,然后判断组件类型是什么
//循环整个form上的控件 foreach (Control c in this.Controls) { //看看是不是checkbox if (c is CheckBox) { //将找到的con ...
- 第10组 Alpha冲刺 (6/6)(组长)
1.1基本情况 ·队名:今晚不睡觉 ·组长博客:https://www.cnblogs.com/cpandbb/p/14008187.html ·作业博客:https://edu.cnblogs.co ...
- Go语言系列之RabbitMQ消息队列
1. RabbitMQ是什么? MQ 是什么?队列是什么,MQ 我们可以理解为消息队列,队列我们可以理解为管道.以管道的方式做消息传递. 生活场景: 1.其实我们在双11的时候,当我们凌晨大量的秒 ...