如何在PyQT5或Pyside2中构建无边框圆角阴影窗口,并可拖动,阴影随窗口活动状态变化?
问题描述
1、先大致说说PyQT和Pyside的区别,PyQT5和PySide2都是QT项目的python绑定,都能完整地在python中实现QT的功能。两者的区别主要包含许可证授权、维护团队和API设计三个方面,其中Pyside使用LGPL协议,允许商业闭源使用(需要开放修改部分),而PyQT使用GPL协议,需要完全开源;
2、PyQT默认建立的窗口使用的就是当前操作系统的原生窗口样式。然而,有时候我们的app需要定制标题栏,比如标题栏和菜单栏合并(类似pycharm的界面),这时就需要屏蔽原生的边框,然后自己构建边框,从而实现圆角、边框阴影定制等。
开始码字^^
基本思路
通过屏蔽原生边框,然后把窗口设置为透明,再在窗口中放一个容器,让容器边框和窗口之间保持一定间距用来呈现阴影,再将容器设置圆角。所有的窗口部件都放置在容器中,这样就得到了无边框圆角阴影窗口。
内容
1、准备环境
2、屏蔽原生标题栏和边框:window.setWindowFlags(window.windowFlags() | Qt.FramelessWindowHint)
3、让原始窗口透明:window.setAttribute(Qt.WA_TranslucentBackground, True);构建container,然后设置container的边框和阴影
4、实现窗口活动状态变化时,阴影强度也跟着变化,如窗口为活动窗口时,阴影更强烈,失去焦点时阴影更弱
5、实现窗口最大化时阴影消失,还原时阴影恢复,且鼠标可拖动窗口
6、拓展:下一篇我们将进一步实现窗口大小可用鼠标拖动改变
1、准备环境
(1)安装python,这就不多说了,网上很多教程,预告一下:今年年底博主计划出一适合上班族的python小白到精通教程专栏,主要内容为实现高效自动办公,编写各类办公脚本。
(2)安装PyQT5,直接pip install pyqt5即可搞定。
2、屏蔽原生窗口边框
直接看代码吧~
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Created on 2025年8月16日
@author: wenye
@file: FramelessWidget
@description: 无边框圆角带阴影窗口,包括最大化最小化,失去焦点和重新获得焦点的阴影行为都完美实现,
没有任何副作用,完全跨平台
"""
from PyQt5 import QtWidgets, QtCore
from PyQt5.QtCore import Qt, QEvent
from PyQt5.QtWidgets import QGraphicsDropShadowEffect, QWidget, QStyle
from frameless import Ui_Dialog
__Author__ = 'wenye'
class Window2(QWidget):
"""将UI设置和逻辑放到一起"""
def __init__(self, *args, **kwargs):
super(Window2, self).__init__(*args, **kwargs)
self.mPos = None
self.setupUi()
self.Margins = self.layout().getContentsMargins()
# 无边框,需要自己在容器widget上处理窗口相关行为,如最大化,最小化关闭等
self.setWindowFlags(self.windowFlags() | Qt.FramelessWindowHint)
# 背景透明(就是ui中黑色背景的那个控件)
self.setAttribute(Qt.WA_TranslucentBackground, True)
效果:
3、构建容器并通过QGraphicsDropShadowEffect实现窗口阴影
QGraphicsEffect作为QT图形效果,用于控制绘图风格。QGraphicsEffect类是所有图形效果的基类,其通过挂接到渲染管道并在源(例如QGraphicsPixmapItem、QWidget)和目标设备(例如QGraphicsView的视口)之间进行操作来更改元素的外观。 QT内置提供以下图形效果:
- QGraphicsBlurEffect:模糊
- QGraphicsDropShadowEffect:阴影
- QGraphicsColorizeEffect:颜色
- QGraphicsOpacityEffect:透明度
这里我们使用的是QGraphicsDropShadowEffect,让以QWidget为基类的窗口可以呈现阴影。
class Window2(QWidget):
"""将UI设置和逻辑放到一起"""
def __init__(self, *args, **kwargs):
super(Window2, self).__init__(*args, **kwargs)
self.mPos = None
self.setupUi()
self.Margins = self.layout().getContentsMargins()
# 重点
# 无边框,需要自己在容器widget上处理窗口相关行为,如最大化,最小化关闭等
self.setWindowFlags(self.windowFlags() | Qt.FramelessWindowHint)
# 背景透明(就是ui中黑色背景的那个控件)
self.setAttribute(Qt.WA_TranslucentBackground, True)
self.shadow_effect = QGraphicsDropShadowEffect(self)
self.shadow_effect.setBlurRadius(15)
self.shadow_effect.setOffset(1, 1)
self.shadow_effect.setColor(Qt.black)
"""
将效果应用到中间承载内容的容器widget上,需要保证widget边界和背景边界有间隙用来显示阴影,
也就是self的layout需要有边界,如果调用self.layout().setContentsMargins(0,0,0,0)后,
将看不到阴影
"""
self.widget.setGraphicsEffect(self.shadow_effect)
效果:
解释:
(1)其中setBlurRadius(radius: float)成员方法设置阴影模糊半径,setOffset(x, y)设置x方向和y方向阴影的偏移,setColor(color: QColor)设置阴影颜色,从而实现自定义的阴影风格。
(2)圆角通过setStyleSheet来实现,具体见下面的setupUi()函数:
def setupUi(self):
self.resize(600, 430)
self.setObjectName("main")
self.setStyleSheet("""
#main { background-color: rgb(0, 0, 0); }
#widget { background-color: rgb(255, 255, 255); border-radius: 10px; }
""")
self.main_layout = QtWidgets.QVBoxLayout(self)
self.main_layout.setObjectName("main_layout")
self.widget = QtWidgets.QWidget(self)
self.widget.setObjectName("widget")
self.gridLayout = QtWidgets.QGridLayout(self.widget)
self.gridLayout.setObjectName("gridLayout")
spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
self.gridLayout.addItem(spacerItem, 0, 0, 1, 1)
self.closeButton = QtWidgets.QPushButton(self.widget)
self.closeButton.setMinimumSize(QtCore.QSize(30, 30))
self.closeButton.setObjectName("closeButton")
self.closeButton.setIcon(self.style().standardIcon(QStyle.SP_TitleBarCloseButton))
self.closeButton.clicked.connect(self.close)
self.gridLayout.addWidget(self.closeButton, 0, 3, 1, 1)
self.maximumButton = QtWidgets.QPushButton(self.widget)
self.maximumButton.setMinimumSize(QtCore.QSize(30, 30))
self.maximumButton.setObjectName("maximumButton")
self.maximumButton.clicked.connect(self.show_max_or_normal)
self.maximumButton.setIcon(self.style().standardIcon(QStyle.SP_TitleBarMaxButton))
self.gridLayout.addWidget(self.maximumButton, 0, 2, 1, 1)
self.minimumButton = QtWidgets.QPushButton(self.widget)
self.minimumButton.setMinimumSize(QtCore.QSize(30, 30))
self.minimumButton.setObjectName("minimumButton")
self.minimumButton.clicked.connect(self.showMinimized)
self.minimumButton.setIcon(self.style().standardIcon(QStyle.SP_TitleBarMinButton))
self.gridLayout.addWidget(self.minimumButton, 0, 1, 1, 1)
self.plainTextEdit = QtWidgets.QPlainTextEdit(self.widget)
self.plainTextEdit.setObjectName("plainTextEdit")
self.gridLayout.addWidget(self.plainTextEdit, 1, 0, 1, 4)
self.main_layout.addWidget(self.widget)
4、阴影随窗口活动状态变化而变化
通过重写事件处理,实现当窗口状态变化时,调节阴影强度:
def changeEvent(self, event):
super().changeEvent(event)
# 检测激活状态变化事件
if event.type() == QEvent.ActivationChange:
if not self.isActiveWindow():
# 窗口失去焦点:用户切换到其他应用
self.shadow_effect.setBlurRadius(10)
self.shadow_effect.setOffset(0, 0)
self.shadow_effect.setColor(Qt.gray)
else:
# 窗口获得焦点:用户返回本应用
self.shadow_effect.setBlurRadius(15)
self.shadow_effect.setOffset(1, 1)
self.shadow_effect.setColor(Qt.black)
效果:
解释:
(1)changeEvent(event: QEvent)为QWidget成员方法,当窗口状态变化时会被调用,如窗口大小、活动状态等变化都会被调用。这里我们重写这个方法,并判断事件是否是ActivationChange,从而在窗口活动状态改变时设置阴影。
(2)super().changeEvent(event)是调用父类的changeEvent,不要自己调用父类,应该用supper(),这样可以避免父类的父类这种情况无法完全调用。
5、实现窗口最大化时阴影消失,还原时阴影恢复,鼠标可以拖动窗口
同样的思路,只要重写窗口最大化、最小化、还原以及鼠标事件处理方法,即可实现:
def show_max_or_normal(self):
if self.isMaximized():
self.maximumButton.setIcon(self.style().standardIcon(QStyle.SP_TitleBarNormalButton))
self.showNormal()
else:
self.maximumButton.setIcon(self.style().standardIcon(QStyle.SP_TitleBarMaxButton))
self.showMaximized()
# ---------加上简单的移动功能------------
def mousePressEvent(self, event):
"""鼠标点击事件"""
if event.button() == Qt.LeftButton:
self.mPos = event.pos()
event.accept()
def mouseReleaseEvent(self, event):
"""鼠标弹起事件"""
self.mPos = None
event.accept()
def mouseMoveEvent(self, event):
if event.buttons() == Qt.LeftButton and self.mPos:
self.move(self.mapToGlobal(event.pos() - self.mPos))
event.accept()
def showMaximized(self):
"""最大化, 要去除上下左右边界, 如果不去除则边框地方会有空隙"""
self.setStyleSheet("#widget { background-color: rgb(255, 255, 255); border-radius: 0; }")
self.layout().setContentsMargins(0, 0, 0, 0)
self.update()
super().showMaximized()
def showNormal(self):
"""还原,要保留上下左右边界,否则没有边框无法调整"""
self.setStyleSheet("#widget { background-color: rgb(255, 255, 255); border-radius: 10px; }")
self.layout().setContentsMargins(*self.Margins)
self.update()
super().showNormal()
解释:
(1)show_max_or_normal方法会在最大化/还原按钮被点击时调用(见setupUi中将按钮点击事件连接到了对应的成员方法),如果当前是最大化则还原,否则最大化;最小化时不需要额外操作,所以不需要重写。
(2)在处理mouseMoveEvent时,要确保此时鼠标左键按下且当前鼠标所在位置不为空即event.buttons() == Qt.LeftButton and self.mPos
补充:
如果要换为PySide2,只需要在导入部分将PyQT5替换为PySide2即可,如:
from PySide2 import QtWidgets, QtCore
下一篇:我们将进一步实现一个自定义标题栏,鼠标光标在标题栏内才能拖动窗口,且鼠标能够在4个角和4边拖动改变窗口大小
以下是包含完整功能(阴影、圆角、鼠标拖动窗口位置、最大化最小化、窗口活动状态改变时阴影强度改变)的代码:
https://download.csdn.net/download/weixin_43866138/91687585