背景
众所周知,PyQt的窗口支持隐藏系统默认的窗口,实现自定义窗口的样式。
但是,启用这种模式的窗口随之而来的问题是:无法使用系统默认的窗口缩放,窗口移动等功能。因此,今天博主带来自己编写的一个自定义窗口类,需要使用的小伙伴可以直接下载此代码,将您自己的窗口类继承该类即可拥有以下功能。
- 附:隐藏默认框架代码:
self.setWindowFlags(Qt.FramelessWindowHint)
功能
- 窗口向右缩放
- 窗口向下缩放
- 窗口向右下缩放
- 鼠标跟随缩放区域切换鼠标样式
- 双击标题栏实现窗口缩放与还原
- 点击标题栏实现窗口移动
- 最小化、最大化按钮功能(需调用预置的函数,传入自己的按钮对象)
- 禁用窗口缩放
- 禁用双击标题栏缩放
- …
部分功能演示
原理
- 坐标状态切换:判断坐标所处位置,切换鼠标样式(注:该功能单独用一个定时器定时检测鼠标状态实现的。若直接放在mouseMoveEvent中会出现切换不及时问题!!!)
- 其他功能通过重写鼠标点击、双击事件实现
- 实现以上功能需要自定义几个状态变量来协调各种状态变化
源代码
1、定制窗口基类
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtCore import Qt
'''
定制窗口基类
@Author: 浅若清风cyf
@Date: 2023/06/16
@Description: 支持窗口缩放,标题栏双击最大最小化,解决了依赖mouseMoveEvent识别鼠标位置导致鼠标样式更新不及时的问题
'''
class MainWinCustom(QMainWindow):
def __init__(self, titleHeight: int = 20):
super(MainWinCustom, self).__init__()
# 鼠标拖拽缩放窗口的识别区域宽度
self._padding = 20
# 设置标题栏的高度,作为双击标题栏的识别区域范围
self.titleHeight = titleHeight
# 隐藏系统默认框架
self.setWindowFlags(Qt.FramelessWindowHint)
# 一些鼠标状态
self.is_mousePressed = False
self.is_resizing = False
self.resize_direction = 'right'
# 一些功能启用状态
self.support_resize = True
self.support_double_max = True
self.timer_cursorUpdater = QTimer(self)
def set_mouse_tracking(self):
self.ui.frame.setMouseTracking(True)
self.ui.centralwidget.setMouseTracking(True)
self.setMouseTracking(True) # / 设置widget鼠标跟踪
def disable_window_resize(self):
self.support_resize = False
self.stop_cursorUpdater()
def disable_double_max(self):
self.support_double_max = False
def set_titleHeight(self, height: int):
self.titleHeight = height
def set_frame_style(self, frame: QFrame):
frame.setFrameStyle(QFrame.Panel)
def set_comboBoxView(self):
comboBoxes = self.findChildren(QComboBox)
for comboBox in comboBoxes:
comboBox.setView(QListView())
def set_max_button(self, btn: QToolButton or QPushButton):
btn.clicked.connect(self.change_window_size)
def set_min_button(self, btn: QToolButton or QPushButton):
btn.clicked.connect(self.showMinimized)
def set_close_button(self, btn: QToolButton or QPushButton):
btn.clicked.connect(self.close)
'''
以下重写窗口拖拽事件
'''
def resizeEvent(self, QResizeEvent):
# 获取有效识别区域
self.right_l = self.width() - self._padding
self.right_r = self.width() + 1
self.bottom_u = self.height() - self._padding
self.bottom_d = self.height() + 1
# # 识别区域
# print('x识别区间:', self.right_l, ',', self.right_r)
# print('y识别区间:', self.bottom_u, ',', self.bottom_d)
def get_mouse_pos_of_window(self) -> QPoint:
global_pos = QCursor.pos()
window_pos = self.mapFromGlobal(global_pos)
return window_pos
def get_mouse_global_pos(self) -> QPoint:
return QCursor.pos()
def check_pos_status(self):
'''
获取鼠标相对于窗口的坐标,根据鼠标所处的位置返回鼠标的状态
'''
pos: QPoint = self.get_mouse_pos_of_window()
if pos.x() >= self.right_l and pos.x() < self.right_r:
if pos.y() >= 0:
if pos.y() <= self.bottom_u:
return pos, "right" # 右边界
elif pos.y() < self.bottom_d:
return pos, "corner" # 右下角
if pos.y() >= self.bottom_u and pos.y() < self.bottom_d:
if pos.x() >= 0:
if pos.x() <= self.right_l:
return pos, 'bottom' # 下边界
if pos.x() >= 0 and pos.x() <= self.right_l and pos.y() >= 0 and pos.y() <= self.titleHeight:
return pos, 'title'
return pos, False
def start_cursorUpdater(self):
'''
启动计时器更新鼠标样式
'''
if self.timer_cursorUpdater:
self.timer_cursorUpdater.timeout.connect(self.update_cursor)
self.timer_cursorUpdater.start(100) # 每隔1秒触发一次鼠标移动事件
def stop_cursorUpdater(self):
'''
停止计时器更新鼠标样式
'''
if self.timer_cursorUpdater:
self.timer_cursorUpdater.stop()
def update_cursor(self):
'''
根据鼠标坐标获取状态,设定鼠标样式
'''
pos, status = self.check_pos_status()
if status is False:
self.setCursor(Qt.ArrowCursor)
elif status == 'right':
self.setCursor(Qt.SizeHorCursor)
elif status == 'bottom':
self.setCursor(Qt.SizeVerCursor)
elif status == 'corner':
self.setCursor(Qt.SizeFDiagCursor)
else:
self.setCursor(Qt.ArrowCursor)
def change_window_size(self):
if not self.isMaximized():
self.showMaximized()
self.stop_cursorUpdater()
elif self.isMaximized():
self.showNormal()
self.start_cursorUpdater()
return self.isMaximized()
def mousePressEvent(self, event: QMouseEvent):
self.is_mousePressed = True
self.mouse_pos = self.get_mouse_global_pos()
def mouseReleaseEvent(self, event: QMouseEvent):
self.is_mousePressed = False
self.is_resizing = False
def mouseDoubleClickEvent(self, *args, **kwargs):
'''
检测标题栏的双击状态,切换最大化与正常窗口
'''
if self.check_pos_status()[1] == 'title' and self.support_double_max:
self.change_window_size()
def mouseMoveEvent(self, event: QMouseEvent):
if self.is_resizing:
pos = self.get_mouse_pos_of_window()
if self.resize_direction == 'right' and self.support_resize:
self.resize(pos.x(), self.height())
elif self.resize_direction == 'down' and self.support_resize:
self.resize(self.width(), pos.y())
elif self.resize_direction == 'right-down' and self.support_resize:
self.resize(pos.x(), pos.y())
elif self.resize_direction == 'move':
current_pos = self.get_mouse_global_pos()
self.move(self.pos() + current_pos - self.mouse_pos)
self.mouse_pos = current_pos
else:
if self.is_mousePressed:
pos, status = self.check_pos_status()
if status is False:
self.is_resizing = False
if status == 'right':
# self.resize(pos.x(), self.height())
self.is_resizing = True
self.resize_direction = 'right'
elif status == 'bottom':
self.is_resizing = True
self.resize_direction = 'down'
elif status == 'corner':
self.is_resizing = True
self.resize_direction = 'right-down'
elif status == 'title':
self.is_resizing = True
self.resize_direction = 'move'
def hideEvent(self, *args, **kwargs):
self.stop_cursorUpdater()
def showEvent(self, *args, **kwargs):
if self.support_resize:
self.start_cursorUpdater()
2、使用示例
class MyWin(MainWinCustom):
def __init__(self):
super(MyWin2, self).__init__()
self.resize(200, 300)
# 创建一个 QWidget 作为中心区域的容器
central_widget = QWidget(self)
# 在中心容器中添加其他控件
label_title = QLabel("这是标题栏", central_widget)
label_title.setAlignment(Qt.AlignHCenter | Qt.AlignVCenter)
label_title.setFixedHeight(40)
label_title.setStyleSheet("""
QLabel{
background: yellow;
}""")
layout = QVBoxLayout(central_widget)
layout.addWidget(label_title)
label = QLabel('Power by 浅若清风cyf\n\t2023/06/16\n')
label.setAlignment(Qt.AlignHCenter | Qt.AlignVCenter)
layout.addWidget(label)
layout.setContentsMargins(0, 0, 0, 0)
# 将中心容器设置为 QMainWindow 的中心部件
self.setCentralWidget(central_widget)
# 设置label_title的高度为有效识别区域
self.set_titleHeight(label_title.height())
if __name__ == '__main__':
QtCore.QCoreApplication.setAttribute(Qt.AA_EnableHighDpiScaling)
QtGui.QGuiApplication.setAttribute(Qt.HighDpiScaleFactorRoundingPolicy.PassThrough)
app = QApplication(sys.argv)
win = MyWin()
win.show()
sys.exit(app.exec_())