PyQt5 中带有框架的自定义标题栏
Custom Titlebar with frame in PyQt5
我正在为 Windows/Linux 开发一个开源 markdown 支持的最小笔记记录应用程序。我正在尝试删除标题栏并添加我自己的按钮。我想要一个只有两个自定义按钮的标题栏,如图
目前我有这个:
我试过修改 window 标志:
- 没有 window 标志,window 既是 re-sizable 又是可移动的。但是没有自定义按钮。
- 使用
self.setWindowFlags(QtCore.Qt.FramelessWindowHint)
,window 没有边框,但无法移动或调整 window 的大小
- 使用
self.setWindowFlags(QtCore.Qt.CustomizeWindowHint)
,window可调整大小但不能移动,也无法去除window顶部的白色部分。
感谢任何帮助。您可以在 GitHub here.
上找到该项目
谢谢..
这是我的python代码:
from PyQt5 import QtCore, QtWidgets, QtWebEngineWidgets, uic
import sys
import os
import markdown2 # https://github.com/trentm/python-markdown2
from PyQt5.QtCore import QRect
from PyQt5.QtGui import QFont
simpleUiForm = uic.loadUiType("Simple.ui")[0]
class SimpleWindow(QtWidgets.QMainWindow, simpleUiForm):
def __init__(self, parent=None):
QtWidgets.QMainWindow.__init__(self, parent)
self.setupUi(self)
self.markdown = markdown2.Markdown()
self.css = open(os.path.join("css", "default.css")).read()
self.editNote.setPlainText("")
#self.noteView = QtWebEngineWidgets.QWebEngineView(self)
self.installEventFilter(self)
self.displayNote.setContextMenuPolicy(QtCore.Qt.NoContextMenu)
#self.setWindowFlags(QtCore.Qt.FramelessWindowHint)
def eventFilter(self, object, event):
if event.type() == QtCore.QEvent.WindowActivate:
print("widget window has gained focus")
self.editNote.show()
self.displayNote.hide()
elif event.type() == QtCore.QEvent.WindowDeactivate:
print("widget window has lost focus")
note = self.editNote.toPlainText()
htmlNote = self.getStyledPage(note)
# print(note)
self.editNote.hide()
self.displayNote.show()
# print(htmlNote)
self.displayNote.setHtml(htmlNote)
elif event.type() == QtCore.QEvent.FocusIn:
print("widget has gained keyboard focus")
elif event.type() == QtCore.QEvent.FocusOut:
print("widget has lost keyboard focus")
return False
UI 文件在以下层次结构中创建
以下是您必须遵循的步骤:
- 拥有您的 MainWindow,无论是 QMainWindow 还是 QWidget,或者您想要继承的任何 [widget]。
- 设置其标志,self.setWindowFlags(Qt.FramelessWindowHint)
- 实现你自己的移动。
- 实现您自己的按钮(关闭、最大、最小)
- 实现您自己的调整大小。
这是一个实现了四处移动和按钮的小示例。您仍然应该使用相同的逻辑来实现调整大小。
import sys
from PyQt5.QtCore import QPoint
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QApplication
from PyQt5.QtWidgets import QHBoxLayout
from PyQt5.QtWidgets import QLabel
from PyQt5.QtWidgets import QPushButton
from PyQt5.QtWidgets import QVBoxLayout
from PyQt5.QtWidgets import QWidget
class MainWindow(QWidget):
def __init__(self):
super(MainWindow, self).__init__()
self.layout = QVBoxLayout()
self.layout.addWidget(MyBar(self))
self.setLayout(self.layout)
self.layout.setContentsMargins(0,0,0,0)
self.layout.addStretch(-1)
self.setMinimumSize(800,400)
self.setWindowFlags(Qt.FramelessWindowHint)
self.pressing = False
class MyBar(QWidget):
def __init__(self, parent):
super(MyBar, self).__init__()
self.parent = parent
print(self.parent.width())
self.layout = QHBoxLayout()
self.layout.setContentsMargins(0,0,0,0)
self.title = QLabel("My Own Bar")
btn_size = 35
self.btn_close = QPushButton("x")
self.btn_close.clicked.connect(self.btn_close_clicked)
self.btn_close.setFixedSize(btn_size,btn_size)
self.btn_close.setStyleSheet("background-color: red;")
self.btn_min = QPushButton("-")
self.btn_min.clicked.connect(self.btn_min_clicked)
self.btn_min.setFixedSize(btn_size, btn_size)
self.btn_min.setStyleSheet("background-color: gray;")
self.btn_max = QPushButton("+")
self.btn_max.clicked.connect(self.btn_max_clicked)
self.btn_max.setFixedSize(btn_size, btn_size)
self.btn_max.setStyleSheet("background-color: gray;")
self.title.setFixedHeight(35)
self.title.setAlignment(Qt.AlignCenter)
self.layout.addWidget(self.title)
self.layout.addWidget(self.btn_min)
self.layout.addWidget(self.btn_max)
self.layout.addWidget(self.btn_close)
self.title.setStyleSheet("""
background-color: black;
color: white;
""")
self.setLayout(self.layout)
self.start = QPoint(0, 0)
self.pressing = False
def resizeEvent(self, QResizeEvent):
super(MyBar, self).resizeEvent(QResizeEvent)
self.title.setFixedWidth(self.parent.width())
def mousePressEvent(self, event):
self.start = self.mapToGlobal(event.pos())
self.pressing = True
def mouseMoveEvent(self, event):
if self.pressing:
self.end = self.mapToGlobal(event.pos())
self.movement = self.end-self.start
self.parent.setGeometry(self.mapToGlobal(self.movement).x(),
self.mapToGlobal(self.movement).y(),
self.parent.width(),
self.parent.height())
self.start = self.end
def mouseReleaseEvent(self, QMouseEvent):
self.pressing = False
def btn_close_clicked(self):
self.parent.close()
def btn_max_clicked(self):
self.parent.showMaximized()
def btn_min_clicked(self):
self.parent.showMinimized()
if __name__ == "__main__":
app = QApplication(sys.argv)
mw = MainWindow()
mw.show()
sys.exit(app.exec_())
这里有一些提示:
选项 1:
- 有一个 QGridLayout,每个角和边都有小部件(例如左侧、左上角、菜单栏、右上角、右侧、右下角、底部和左下角)
- 使用方法 (1),您会知道何时单击每个边框,您只需定义每个尺寸并将每个尺寸添加到它们的位置。
- 当你点击每一个时,以各自的方式对待它们,例如,如果你点击左边的一个并向左拖动,你必须将它的大小调整得更大,同时将它向左移动,所以它似乎会停在正确的位置并变宽。
- 将此推理应用于每条边,每条边都按其必须的方式行事。
选项 2:
您可以通过点击位置检测您点击的位置,而不是使用 QGridLayout。
验证点击的 x 是否小于移动位置的 x 以了解它是向左移动还是向右移动以及它被点击的位置。
计算方式同Option1
选项 3:
- 可能还有其他方法,但我只是想到了这些。例如使用 CustomizeWindowHint 你说你可以调整大小,所以你只需要实现我给你的例子。漂亮!
提示:
- 小心localPos(在自己的小部件内),globalPos(与您的屏幕相关)。例如:如果您单击左侧小部件的最左侧,它的 'x' 将为零,如果您单击中间(内容)的最左侧,它也将为零,尽管如果您使用 mapToGlobal,您将拥有根据屏幕的位置不同的值。
- 调整大小或移动时要注意,当你必须增加宽度或减去宽度,或只是移动,或两者兼而有之时,我建议你在纸上画画并在实施之前弄清楚调整大小的逻辑是如何工作的它出乎意料。
祝你好运:D
虽然接受的答案可以被认为是有效的,但它有一些问题。
- 使用
setGeometry()
是不合适的(并且使用它的原因是错误的)因为它没有考虑样式设置的可能的帧边距;
- 位置计算过于复杂;
- 将标题栏的大小调整为总宽度是错误的,因为它没有考虑按钮并且在某些情况下还会导致递归问题(例如不设置最小大小主要 window);此外,如果标题太大,则无法调整主标题 window;
- 按钮不应接受焦点;
- 设置布局会限制“主窗口小部件”或布局,因此应不 添加标题,但应使用窗口小部件的内容边距;
我修改了代码,为主要 window 提供了更好的基础,简化了移动代码,并添加了其他功能,如 Qt windowTitle()
属性 支持、标准 QStyle 图标用于按钮(而不是文本),以及适当的 maximize/normal 按钮图标。请注意,标题标签未添加到布局中。
class MainWindow(QWidget):
def __init__(self):
super(MainWindow, self).__init__()
self.setWindowFlags(self.windowFlags() | Qt.FramelessWindowHint)
self.titleBar = MyBar(self)
self.setContentsMargins(0, self.titleBar.height(), 0, 0)
self.resize(640, self.titleBar.height() + 480)
def changeEvent(self, event):
if event.type() == event.WindowStateChange:
self.titleBar.windowStateChanged(self.windowState())
def resizeEvent(self, event):
self.titleBar.resize(self.width(), self.titleBar.height())
class MyBar(QWidget):
clickPos = None
def __init__(self, parent):
super(MyBar, self).__init__(parent)
self.setAutoFillBackground(True)
self.setBackgroundRole(QPalette.Shadow)
# alternatively:
# palette = self.palette()
# palette.setColor(palette.Window, Qt.black)
# palette.setColor(palette.WindowText, Qt.white)
# self.setPalette(palette)
layout = QHBoxLayout(self)
layout.setContentsMargins(1, 1, 1, 1)
layout.addStretch()
self.title = QLabel("My Own Bar", self, alignment=Qt.AlignCenter)
# if setPalette() was used above, this is not required
self.title.setForegroundRole(QPalette.Light)
style = self.style()
ref_size = self.fontMetrics().height()
ref_size += style.pixelMetric(style.PM_ButtonMargin) * 2
self.setMaximumHeight(ref_size + 2)
btn_size = QSize(ref_size, ref_size)
for target in ('min', 'normal', 'max', 'close'):
btn = QToolButton(self, focusPolicy=Qt.NoFocus)
layout.addWidget(btn)
btn.setFixedSize(btn_size)
iconType = getattr(style,
'SP_TitleBar{}Button'.format(target.capitalize()))
btn.setIcon(style.standardIcon(iconType))
if target == 'close':
colorNormal = 'red'
colorHover = 'orangered'
else:
colorNormal = 'palette(mid)'
colorHover = 'palette(light)'
btn.setStyleSheet('''
QToolButton {{
background-color: {};
}}
QToolButton:hover {{
background-color: {}
}}
'''.format(colorNormal, colorHover))
signal = getattr(self, target + 'Clicked')
btn.clicked.connect(signal)
setattr(self, target + 'Button', btn)
self.normalButton.hide()
self.updateTitle(parent.windowTitle())
parent.windowTitleChanged.connect(self.updateTitle)
def updateTitle(self, title=None):
if title is None:
title = self.window().windowTitle()
width = self.title.width()
width -= self.style().pixelMetric(QStyle.PM_LayoutHorizontalSpacing) * 2
self.title.setText(self.fontMetrics().elidedText(
title, Qt.ElideRight, width))
def windowStateChanged(self, state):
self.normalButton.setVisible(state == Qt.WindowMaximized)
self.maxButton.setVisible(state != Qt.WindowMaximized)
def mousePressEvent(self, event):
if event.button() == Qt.LeftButton:
self.clickPos = event.windowPos().toPoint()
def mouseMoveEvent(self, event):
if self.clickPos is not None:
self.window().move(event.globalPos() - self.clickPos)
def mouseReleaseEvent(self, QMouseEvent):
self.clickPos = None
def closeClicked(self):
self.window().close()
def maxClicked(self):
self.window().showMaximized()
def normalClicked(self):
self.window().showNormal()
def minClicked(self):
self.window().showMinimized()
def resizeEvent(self, event):
self.title.resize(self.minButton.x(), self.height())
self.updateTitle()
if __name__ == "__main__":
app = QApplication(sys.argv)
mw = MainWindow()
layout = QVBoxLayout(mw)
widget = QTextEdit()
layout.addWidget(widget)
mw.show()
mw.setWindowTitle('My custom window with a very, very long title')
sys.exit(app.exec_())
我正在为 Windows/Linux 开发一个开源 markdown 支持的最小笔记记录应用程序。我正在尝试删除标题栏并添加我自己的按钮。我想要一个只有两个自定义按钮的标题栏,如图
目前我有这个:
我试过修改 window 标志:
- 没有 window 标志,window 既是 re-sizable 又是可移动的。但是没有自定义按钮。
- 使用
self.setWindowFlags(QtCore.Qt.FramelessWindowHint)
,window 没有边框,但无法移动或调整 window 的大小
- 使用
self.setWindowFlags(QtCore.Qt.CustomizeWindowHint)
,window可调整大小但不能移动,也无法去除window顶部的白色部分。
感谢任何帮助。您可以在 GitHub here.
上找到该项目谢谢..
这是我的python代码:
from PyQt5 import QtCore, QtWidgets, QtWebEngineWidgets, uic
import sys
import os
import markdown2 # https://github.com/trentm/python-markdown2
from PyQt5.QtCore import QRect
from PyQt5.QtGui import QFont
simpleUiForm = uic.loadUiType("Simple.ui")[0]
class SimpleWindow(QtWidgets.QMainWindow, simpleUiForm):
def __init__(self, parent=None):
QtWidgets.QMainWindow.__init__(self, parent)
self.setupUi(self)
self.markdown = markdown2.Markdown()
self.css = open(os.path.join("css", "default.css")).read()
self.editNote.setPlainText("")
#self.noteView = QtWebEngineWidgets.QWebEngineView(self)
self.installEventFilter(self)
self.displayNote.setContextMenuPolicy(QtCore.Qt.NoContextMenu)
#self.setWindowFlags(QtCore.Qt.FramelessWindowHint)
def eventFilter(self, object, event):
if event.type() == QtCore.QEvent.WindowActivate:
print("widget window has gained focus")
self.editNote.show()
self.displayNote.hide()
elif event.type() == QtCore.QEvent.WindowDeactivate:
print("widget window has lost focus")
note = self.editNote.toPlainText()
htmlNote = self.getStyledPage(note)
# print(note)
self.editNote.hide()
self.displayNote.show()
# print(htmlNote)
self.displayNote.setHtml(htmlNote)
elif event.type() == QtCore.QEvent.FocusIn:
print("widget has gained keyboard focus")
elif event.type() == QtCore.QEvent.FocusOut:
print("widget has lost keyboard focus")
return False
UI 文件在以下层次结构中创建
以下是您必须遵循的步骤:
- 拥有您的 MainWindow,无论是 QMainWindow 还是 QWidget,或者您想要继承的任何 [widget]。
- 设置其标志,self.setWindowFlags(Qt.FramelessWindowHint)
- 实现你自己的移动。
- 实现您自己的按钮(关闭、最大、最小)
- 实现您自己的调整大小。
这是一个实现了四处移动和按钮的小示例。您仍然应该使用相同的逻辑来实现调整大小。
import sys
from PyQt5.QtCore import QPoint
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QApplication
from PyQt5.QtWidgets import QHBoxLayout
from PyQt5.QtWidgets import QLabel
from PyQt5.QtWidgets import QPushButton
from PyQt5.QtWidgets import QVBoxLayout
from PyQt5.QtWidgets import QWidget
class MainWindow(QWidget):
def __init__(self):
super(MainWindow, self).__init__()
self.layout = QVBoxLayout()
self.layout.addWidget(MyBar(self))
self.setLayout(self.layout)
self.layout.setContentsMargins(0,0,0,0)
self.layout.addStretch(-1)
self.setMinimumSize(800,400)
self.setWindowFlags(Qt.FramelessWindowHint)
self.pressing = False
class MyBar(QWidget):
def __init__(self, parent):
super(MyBar, self).__init__()
self.parent = parent
print(self.parent.width())
self.layout = QHBoxLayout()
self.layout.setContentsMargins(0,0,0,0)
self.title = QLabel("My Own Bar")
btn_size = 35
self.btn_close = QPushButton("x")
self.btn_close.clicked.connect(self.btn_close_clicked)
self.btn_close.setFixedSize(btn_size,btn_size)
self.btn_close.setStyleSheet("background-color: red;")
self.btn_min = QPushButton("-")
self.btn_min.clicked.connect(self.btn_min_clicked)
self.btn_min.setFixedSize(btn_size, btn_size)
self.btn_min.setStyleSheet("background-color: gray;")
self.btn_max = QPushButton("+")
self.btn_max.clicked.connect(self.btn_max_clicked)
self.btn_max.setFixedSize(btn_size, btn_size)
self.btn_max.setStyleSheet("background-color: gray;")
self.title.setFixedHeight(35)
self.title.setAlignment(Qt.AlignCenter)
self.layout.addWidget(self.title)
self.layout.addWidget(self.btn_min)
self.layout.addWidget(self.btn_max)
self.layout.addWidget(self.btn_close)
self.title.setStyleSheet("""
background-color: black;
color: white;
""")
self.setLayout(self.layout)
self.start = QPoint(0, 0)
self.pressing = False
def resizeEvent(self, QResizeEvent):
super(MyBar, self).resizeEvent(QResizeEvent)
self.title.setFixedWidth(self.parent.width())
def mousePressEvent(self, event):
self.start = self.mapToGlobal(event.pos())
self.pressing = True
def mouseMoveEvent(self, event):
if self.pressing:
self.end = self.mapToGlobal(event.pos())
self.movement = self.end-self.start
self.parent.setGeometry(self.mapToGlobal(self.movement).x(),
self.mapToGlobal(self.movement).y(),
self.parent.width(),
self.parent.height())
self.start = self.end
def mouseReleaseEvent(self, QMouseEvent):
self.pressing = False
def btn_close_clicked(self):
self.parent.close()
def btn_max_clicked(self):
self.parent.showMaximized()
def btn_min_clicked(self):
self.parent.showMinimized()
if __name__ == "__main__":
app = QApplication(sys.argv)
mw = MainWindow()
mw.show()
sys.exit(app.exec_())
这里有一些提示:
选项 1:
- 有一个 QGridLayout,每个角和边都有小部件(例如左侧、左上角、菜单栏、右上角、右侧、右下角、底部和左下角)
- 使用方法 (1),您会知道何时单击每个边框,您只需定义每个尺寸并将每个尺寸添加到它们的位置。
- 当你点击每一个时,以各自的方式对待它们,例如,如果你点击左边的一个并向左拖动,你必须将它的大小调整得更大,同时将它向左移动,所以它似乎会停在正确的位置并变宽。
- 将此推理应用于每条边,每条边都按其必须的方式行事。
选项 2:
您可以通过点击位置检测您点击的位置,而不是使用 QGridLayout。
验证点击的 x 是否小于移动位置的 x 以了解它是向左移动还是向右移动以及它被点击的位置。
计算方式同Option1
选项 3:
- 可能还有其他方法,但我只是想到了这些。例如使用 CustomizeWindowHint 你说你可以调整大小,所以你只需要实现我给你的例子。漂亮!
提示:
- 小心localPos(在自己的小部件内),globalPos(与您的屏幕相关)。例如:如果您单击左侧小部件的最左侧,它的 'x' 将为零,如果您单击中间(内容)的最左侧,它也将为零,尽管如果您使用 mapToGlobal,您将拥有根据屏幕的位置不同的值。
- 调整大小或移动时要注意,当你必须增加宽度或减去宽度,或只是移动,或两者兼而有之时,我建议你在纸上画画并在实施之前弄清楚调整大小的逻辑是如何工作的它出乎意料。
祝你好运:D
虽然接受的答案可以被认为是有效的,但它有一些问题。
- 使用
setGeometry()
是不合适的(并且使用它的原因是错误的)因为它没有考虑样式设置的可能的帧边距; - 位置计算过于复杂;
- 将标题栏的大小调整为总宽度是错误的,因为它没有考虑按钮并且在某些情况下还会导致递归问题(例如不设置最小大小主要 window);此外,如果标题太大,则无法调整主标题 window;
- 按钮不应接受焦点;
- 设置布局会限制“主窗口小部件”或布局,因此应不 添加标题,但应使用窗口小部件的内容边距;
我修改了代码,为主要 window 提供了更好的基础,简化了移动代码,并添加了其他功能,如 Qt windowTitle()
属性 支持、标准 QStyle 图标用于按钮(而不是文本),以及适当的 maximize/normal 按钮图标。请注意,标题标签未添加到布局中。
class MainWindow(QWidget):
def __init__(self):
super(MainWindow, self).__init__()
self.setWindowFlags(self.windowFlags() | Qt.FramelessWindowHint)
self.titleBar = MyBar(self)
self.setContentsMargins(0, self.titleBar.height(), 0, 0)
self.resize(640, self.titleBar.height() + 480)
def changeEvent(self, event):
if event.type() == event.WindowStateChange:
self.titleBar.windowStateChanged(self.windowState())
def resizeEvent(self, event):
self.titleBar.resize(self.width(), self.titleBar.height())
class MyBar(QWidget):
clickPos = None
def __init__(self, parent):
super(MyBar, self).__init__(parent)
self.setAutoFillBackground(True)
self.setBackgroundRole(QPalette.Shadow)
# alternatively:
# palette = self.palette()
# palette.setColor(palette.Window, Qt.black)
# palette.setColor(palette.WindowText, Qt.white)
# self.setPalette(palette)
layout = QHBoxLayout(self)
layout.setContentsMargins(1, 1, 1, 1)
layout.addStretch()
self.title = QLabel("My Own Bar", self, alignment=Qt.AlignCenter)
# if setPalette() was used above, this is not required
self.title.setForegroundRole(QPalette.Light)
style = self.style()
ref_size = self.fontMetrics().height()
ref_size += style.pixelMetric(style.PM_ButtonMargin) * 2
self.setMaximumHeight(ref_size + 2)
btn_size = QSize(ref_size, ref_size)
for target in ('min', 'normal', 'max', 'close'):
btn = QToolButton(self, focusPolicy=Qt.NoFocus)
layout.addWidget(btn)
btn.setFixedSize(btn_size)
iconType = getattr(style,
'SP_TitleBar{}Button'.format(target.capitalize()))
btn.setIcon(style.standardIcon(iconType))
if target == 'close':
colorNormal = 'red'
colorHover = 'orangered'
else:
colorNormal = 'palette(mid)'
colorHover = 'palette(light)'
btn.setStyleSheet('''
QToolButton {{
background-color: {};
}}
QToolButton:hover {{
background-color: {}
}}
'''.format(colorNormal, colorHover))
signal = getattr(self, target + 'Clicked')
btn.clicked.connect(signal)
setattr(self, target + 'Button', btn)
self.normalButton.hide()
self.updateTitle(parent.windowTitle())
parent.windowTitleChanged.connect(self.updateTitle)
def updateTitle(self, title=None):
if title is None:
title = self.window().windowTitle()
width = self.title.width()
width -= self.style().pixelMetric(QStyle.PM_LayoutHorizontalSpacing) * 2
self.title.setText(self.fontMetrics().elidedText(
title, Qt.ElideRight, width))
def windowStateChanged(self, state):
self.normalButton.setVisible(state == Qt.WindowMaximized)
self.maxButton.setVisible(state != Qt.WindowMaximized)
def mousePressEvent(self, event):
if event.button() == Qt.LeftButton:
self.clickPos = event.windowPos().toPoint()
def mouseMoveEvent(self, event):
if self.clickPos is not None:
self.window().move(event.globalPos() - self.clickPos)
def mouseReleaseEvent(self, QMouseEvent):
self.clickPos = None
def closeClicked(self):
self.window().close()
def maxClicked(self):
self.window().showMaximized()
def normalClicked(self):
self.window().showNormal()
def minClicked(self):
self.window().showMinimized()
def resizeEvent(self, event):
self.title.resize(self.minButton.x(), self.height())
self.updateTitle()
if __name__ == "__main__":
app = QApplication(sys.argv)
mw = MainWindow()
layout = QVBoxLayout(mw)
widget = QTextEdit()
layout.addWidget(widget)
mw.show()
mw.setWindowTitle('My custom window with a very, very long title')
sys.exit(app.exec_())