如何在PyQT5或Pyside2中构建无边框圆角阴影窗口,并可拖动,阴影随窗口活动状态变化?

如何在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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

文野TEC

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值