PyQt5實現自定義窗口的拖動關閉縮放功能
#!/usr/bin/env python
# -*- coding:utf-8 -*-
# Author: jyroy
import sys
from PyQt5.QtCore import QSize
from PyQt5.QtWidgets import QApplication
from PyQt5.QtCore import Qt, pyqtSignal, QPoint
from PyQt5.QtGui import QFont, QEnterEvent, QPainter, QColor, QPen
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QLabel,QSpacerItem, QSizePolicy, QPushButton
from PyQt5.QtGui import QIcon
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QPushButton, QTextEdit
# from LeftTabWidget import LeftTabWidget
# 樣式
StyleSheet = """
/*標題欄*/
TitleBar {
background-color: red;
}
/*最小化最大化關閉按鈕通用默認背景*/
#buttonMinimum,#buttonMaximum,#buttonClose {
border: none;
background-color: red;
}
/*懸停*/
#buttonMinimum:hover,#buttonMaximum:hover {
background-color: red;
color: white;
}
#buttonClose:hover {
color: white;
}
/*鼠標按下不放*/
#buttonMinimum:pressed,#buttonMaximum:pressed {
background-color: Firebrick;
}
#buttonClose:pressed {
color: white;
background-color: Firebrick;
}
"""
class TitleBar(QWidget):
# 窗口最小化信號
windowMinimumed = pyqtSignal()
# 窗口最大化信號
windowMaximumed = pyqtSignal()
# 窗口還原信號
windowNormaled = pyqtSignal()
# 窗口關閉信號
windowClosed = pyqtSignal()
# 窗口移動
windowMoved = pyqtSignal(QPoint)
def __init__(self, *args, **kwargs):
super(TitleBar, self).__init__(*args, **kwargs)
# 支持qss設置背景
self.setAttribute(Qt.WA_StyledBackground, True)
self.mPos = None
self.iconSize = 20 # 圖標的默認大小
# 設置默認背景顏色,否則由於受到父窗口的影響導致透明
self.setAutoFillBackground(True)
palette = self.palette()
palette.setColor(palette.Window, QColor(240, 240, 240))
self.setPalette(palette)
# 布局
layout = QHBoxLayout(self, spacing=0)
layout.setContentsMargins(0, 0, 0, 0)
# 窗口圖標
self.iconLabel = QLabel(self)
# self.iconLabel.setScaledContents(True)
layout.addWidget(self.iconLabel)
# 窗口標題
self.titleLabel = QLabel(self)
self.titleLabel.setMargin(2)
layout.addWidget(self.titleLabel)
# 中間伸縮條
layout.addSpacerItem(QSpacerItem(
40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum))
# 利用Webdings字體來顯示圖標
font = self.font() or QFont()
font.setFamily('Webdings')
# 最小化按鈕
self.buttonMinimum = QPushButton(
'0', self, clicked=self.windowMinimumed.emit, font=font, objectName='buttonMinimum')
layout.addWidget(self.buttonMinimum)
# 最大化/還原按鈕
self.buttonMaximum = QPushButton(
'1', self, clicked=self.showMaximized, font=font, objectName='buttonMaximum')
layout.addWidget(self.buttonMaximum)
# 關閉按鈕
self.buttonClose = QPushButton(
'r', self, clicked=self.windowClosed.emit, font=font, objectName='buttonClose')
layout.addWidget(self.buttonClose)
# 初始高度
self.setHeight()
def showMaximized(self):
if self.buttonMaximum.text() == '1':
# 最大化
self.buttonMaximum.setText('2')
self.windowMaximumed.emit()
else: # 還原
self.buttonMaximum.setText('1')
self.windowNormaled.emit()
def setHeight(self, height=38):
"""設置標題欄高度"""
self.setMinimumHeight(height)
self.setMaximumHeight(height)
# 設置右邊按鈕的大小
self.buttonMinimum.setMinimumSize(height, height)
self.buttonMinimum.setMaximumSize(height, height)
self.buttonMaximum.setMinimumSize(height, height)
self.buttonMaximum.setMaximumSize(height, height)
self.buttonClose.setMinimumSize(height, height)
self.buttonClose.setMaximumSize(height, height)
def setTitle(self, title):
"""設置標題"""
self.titleLabel.setText(title)
def setIcon(self, icon):
"""設置圖標"""
self.iconLabel.setPixmap(icon.pixmap(self.iconSize, self.iconSize))
def setIconSize(self, size):
"""設置圖標大小"""
self.iconSize = size
def enterEvent(self, event):
self.setCursor(Qt.ArrowCursor)
super(TitleBar, self).enterEvent(event)
def mouseDoubleClickEvent(self, event):
super(TitleBar, self).mouseDoubleClickEvent(event)
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.windowMoved.emit(self.mapToGlobal(event.pos() - self.mPos))
event.accept()
# 枚舉左上右下以及四個定點
Left, Top, Right, Bottom, LeftTop, RightTop, LeftBottom, RightBottom = range(8)
class FramelessWindow(QWidget):
# 四周邊距
Margins = 5
def __init__(self, *args, **kwargs):
super(FramelessWindow, self).__init__(*args, **kwargs)
self._pressed = False
self.Direction = None
# 背景透明
self.setAttribute(Qt.WA_TranslucentBackground, True)
# 無邊框
self.setWindowFlags(Qt.FramelessWindowHint) # 隱藏邊框
# 鼠標跟蹤
self.setMouseTracking(True)
# 布局
layout = QVBoxLayout(self, spacing=0)
# 預留邊界用於實現無邊框窗口調整大小
layout.setContentsMargins(
self.Margins, self.Margins, self.Margins, self.Margins)
# 標題欄
self.titleBar = TitleBar(self)
layout.addWidget(self.titleBar)
# 信號槽
self.titleBar.windowMinimumed.connect(self.showMinimized)
self.titleBar.windowMaximumed.connect(self.showMaximized)
self.titleBar.windowNormaled.connect(self.showNormal)
self.titleBar.windowClosed.connect(self.close)
self.titleBar.windowMoved.connect(self.move)
self.windowTitleChanged.connect(self.titleBar.setTitle)
self.windowIconChanged.connect(self.titleBar.setIcon)
def setTitleBarHeight(self, height=38):
"""設置標題欄高度"""
self.titleBar.setHeight(height)
def setIconSize(self, size):
"""設置圖標的大小"""
self.titleBar.setIconSize(size)
def setWidget(self, widget):
"""設置自己的控件"""
if hasattr(self, '_widget'):
return
self._widget = widget
# 設置默認背景顏色,否則由於受到父窗口的影響導致透明
self._widget.setAutoFillBackground(True)
palette = self._widget.palette()
palette.setColor(palette.Window, QColor(240, 240, 240))
self._widget.setPalette(palette)
self._widget.installEventFilter(self)
self.layout().addWidget(self._widget)
def move(self, pos):
if self.windowState() == Qt.WindowMaximized or self.windowState() == Qt.WindowFullScreen:
# 最大化或者全屏則不允許移動
return
super(FramelessWindow, self).move(pos)
def showMaximized(self):
"""最大化,要去除上下左右邊界,如果不去除則邊框地方會有空隙"""
super(FramelessWindow, self).showMaximized()
self.layout().setContentsMargins(0, 0, 0, 0)
def showNormal(self):
"""還原,要保留上下左右邊界,否則沒有邊框無法調整"""
super(FramelessWindow, self).showNormal()
self.layout().setContentsMargins(
self.Margins, self.Margins, self.Margins, self.Margins)
def eventFilter(self, obj, event):
"""事件過濾器,用於解決鼠標進入其它控件后還原為標准鼠標樣式"""
if isinstance(event, QEnterEvent):
self.setCursor(Qt.ArrowCursor)
return super(FramelessWindow, self).eventFilter(obj, event)
def paintEvent(self, event):
"""由於是全透明背景窗口,重繪事件中繪制透明度為1的難以發現的邊框,用於調整窗口大小"""
super(FramelessWindow, self).paintEvent(event)
painter = QPainter(self)
painter.setPen(QPen(QColor(255, 255, 255, 1), 2 * self.Margins))
painter.drawRect(self.rect())
def mousePressEvent(self, event):
"""鼠標點擊事件"""
super(FramelessWindow, self).mousePressEvent(event)
if event.button() == Qt.LeftButton:
self._mpos = event.pos()
self._pressed = True
def mouseReleaseEvent(self, event):
'''鼠標彈起事件'''
super(FramelessWindow, self).mouseReleaseEvent(event)
self._pressed = False
self.Direction = None
def mouseMoveEvent(self, event):
"""鼠標移動事件"""
super(FramelessWindow, self).mouseMoveEvent(event)
pos = event.pos()
xPos, yPos = pos.x(), pos.y()
wm, hm = self.width() - self.Margins, self.height() - self.Margins
if self.isMaximized() or self.isFullScreen():
self.Direction = None
self.setCursor(Qt.ArrowCursor)
return
if event.buttons() == Qt.LeftButton and self._pressed:
self._resizeWidget(pos)
return
if xPos <= self.Margins and yPos <= self.Margins:
# 左上角
self.Direction = LeftTop
self.setCursor(Qt.SizeFDiagCursor)
elif wm <= xPos <= self.width() and hm <= yPos <= self.height():
# 右下角
self.Direction = RightBottom
self.setCursor(Qt.SizeFDiagCursor)
elif wm <= xPos and yPos <= self.Margins:
# 右上角
self.Direction = RightTop
self.setCursor(Qt.SizeBDiagCursor)
elif xPos <= self.Margins and hm <= yPos:
# 左下角
self.Direction = LeftBottom
self.setCursor(Qt.SizeBDiagCursor)
elif 0 <= xPos <= self.Margins and self.Margins <= yPos <= hm:
# 左邊
self.Direction = Left
self.setCursor(Qt.SizeHorCursor)
elif wm <= xPos <= self.width() and self.Margins <= yPos <= hm:
# 右邊
self.Direction = Right
self.setCursor(Qt.SizeHorCursor)
elif self.Margins <= xPos <= wm and 0 <= yPos <= self.Margins:
# 上面
self.Direction = Top
self.setCursor(Qt.SizeVerCursor)
elif self.Margins <= xPos <= wm and hm <= yPos <= self.height():
# 下面
self.Direction = Bottom
self.setCursor(Qt.SizeVerCursor)
def _resizeWidget(self, pos):
"""調整窗口大小"""
if self.Direction == None:
return
mpos = pos - self._mpos
xPos, yPos = mpos.x(), mpos.y()
geometry = self.geometry()
x, y, w, h = geometry.x(), geometry.y(), geometry.width(), geometry.height()
if self.Direction == LeftTop: # 左上角
if w - xPos > self.minimumWidth():
x += xPos
w -= xPos
if h - yPos > self.minimumHeight():
y += yPos
h -= yPos
elif self.Direction == RightBottom: # 右下角
if w + xPos > self.minimumWidth():
w += xPos
self._mpos = pos
if h + yPos > self.minimumHeight():
h += yPos
self._mpos = pos
elif self.Direction == RightTop: # 右上角
if h - yPos > self.minimumHeight():
y += yPos
h -= yPos
if w + xPos > self.minimumWidth():
w += xPos
self._mpos.setX(pos.x())
elif self.Direction == LeftBottom: # 左下角
if w - xPos > self.minimumWidth():
x += xPos
w -= xPos
if h + yPos > self.minimumHeight():
h += yPos
self._mpos.setY(pos.y())
elif self.Direction == Left: # 左邊
if w - xPos > self.minimumWidth():
x += xPos
w -= xPos
else:
return
elif self.Direction == Right: # 右邊
if w + xPos > self.minimumWidth():
w += xPos
self._mpos = pos
else:
return
elif self.Direction == Top: # 上面
if h - yPos > self.minimumHeight():
y += yPos
h -= yPos
else:
return
elif self.Direction == Bottom: # 下面
if h + yPos > self.minimumHeight():
h += yPos
self._mpos = pos
else:
return
self.setGeometry(x, y, w, h)
class MainWindow(QWidget):
def __init__(self, *args, **kwargs):
super(MainWindow, self).__init__(*args, **kwargs)
layout = QVBoxLayout(self, spacing=0)
layout.setContentsMargins(0, 0, 0, 0)
# self.left_tag = LeftTabWidget()
# layout.addWidget(self.left_tag)
if __name__ == '__main__':
app = QApplication(sys.argv)
app.setStyleSheet(StyleSheet)
mainWnd = FramelessWindow()
mainWnd.setWindowTitle('測試標題欄')
mainWnd.setWindowIcon(QIcon('Qt.ico'))
mainWnd.resize(QSize(1250,780))
mainWnd.setWidget(MainWindow(mainWnd)) # 把自己的窗口添加進來
mainWnd.show()
sys.exit(app.exec_())