PyQt:如何创建自定义组合标题栏和菜单栏
PyQt: How to create custom combined titlebar and menubar
我想将菜单和标题设置在一个栏中,但不知道如何布局菜单栏和标题栏(或者我自己的标题栏)。
# -*- coding:utf-8 -*-
from PyQt5 import QtWidgets,QtGui,QtCore
import sys
qss = ""
class UI(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.setui()
def setui(self):
#----------main-window----------------------
self.setGeometry(0,0,1366,768) #x,y,w,h
self.setWindowTitle('hello world')
self.setWindowFlag(QtCore.Qt.FramelessWindowHint)
#----------menu-bar---------------------
#--------file-menu-----
self.menu_file=self.menuBar().addMenu('file')
self.menu_file_open=self.menu_file.addAction('open')
self.menu_file_save=self.menu_file.addAction('save')
self.menu_file_saveas=self.menu_file.addAction('save as...')
self.menu_file_quit=self.menu_file.addAction('exit')
#-----------experient-menu----------
self.menu_work=self.menuBar().addMenu('work')
#-------------analysis-menu---------
self.menu_analysis=self.menuBar().addMenu('analysis')
#------------edit-menu--------------
self.menu_edit=self.menuBar().addMenu('edit')
#------------window-menu--------------
self.menu_window=self.menuBar().addMenu('window')
#------------help---menu--------------
self.menu_help=self.menuBar().addMenu('help')
#-------------set---qss----------------------
self.setStyleSheet(qss)
#-------functions--connect-------------------
self.menu_file_quit.triggered.connect(QtWidgets.qApp.quit)
self.show()
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
ex = UI()
sys.exit(app.exec_())
我希望一个栏包括图标、菜单、标题和三个按钮,就像vscode的菜单栏一样。
试一试:
import sys
from PyQt5.QtCore import pyqtSlot, QPoint, Qt, QRect
from PyQt5.QtWidgets import (QMainWindow, QApplication, QPushButton, QHBoxLayout,
QVBoxLayout, QTabWidget, QWidget, QAction,
QLabel, QSizeGrip, QMenuBar, qApp)
from PyQt5.QtGui import QIcon
class TitleBar(QWidget):
height = 35
def __init__(self, parent):
super(TitleBar, self).__init__()
self.parent = parent
self.layout = QHBoxLayout()
self.layout.setContentsMargins(0,0,0,0)
self.menu_bar = QMenuBar()
self.menu_bar.setStyleSheet("""
color: #fff;
background-color: #23272A;
font-size: 14px;
padding: 4px;
""")
self.menu_file = self.menu_bar.addMenu('file')
self.menu_file_open=self.menu_file.addAction('open')
self.menu_file_save=self.menu_file.addAction('save')
self.menu_file_saveas=self.menu_file.addAction('save as...')
self.menu_file_quit=self.menu_file.addAction('exit')
self.menu_file_quit.triggered.connect(qApp.quit)
self.menu_work=self.menu_bar.addMenu('work')
self.menu_analysis=self.menu_bar.addMenu('analysis')
self.menu_edit=self.menu_bar.addMenu('edit')
self.menu_window=self.menu_bar.addMenu('window')
self.menu_help=self.menu_bar.addMenu('help')
self.layout.addWidget(self.menu_bar)
self.title = QLabel("Hello World!")
self.title.setFixedHeight(self.height)
self.layout.addWidget(self.title)
self.title.setStyleSheet("""
background-color: #23272a; /* 23272a #f00*/
font-weight: bold;
font-size: 16px;
color: blue;
padding-left: 170px;
""")
self.closeButton = QPushButton(' ')
self.closeButton.clicked.connect(self.on_click_close)
self.closeButton.setStyleSheet("""
background-color: #DC143C;
border-radius: 10px;
height: {};
width: {};
margin-right: 3px;
font-weight: bold;
color: #000;
font-family: "Webdings";
qproperty-text: "r";
""".format(self.height/1.7,self.height/1.7))
self.maxButton = QPushButton(' ')
self.maxButton.clicked.connect(self.on_click_maximize)
self.maxButton.setStyleSheet("""
background-color: #32CD32;
border-radius: 10px;
height: {};
width: {};
margin-right: 3px;
font-weight: bold;
color: #000;
font-family: "Webdings";
qproperty-text: "1";
""".format(self.height/1.7,self.height/1.7))
self.hideButton = QPushButton(' ')
self.hideButton.clicked.connect(self.on_click_hide)
self.hideButton.setStyleSheet("""
background-color: #FFFF00;
border-radius: 10px;
height: {};
width: {};
margin-right: 3px;
font-weight: bold;
color: #000;
font-family: "Webdings";
qproperty-text: "0";
""".format(self.height/1.7,self.height/1.7))
self.layout.addWidget(self.hideButton)
self.layout.addWidget(self.maxButton)
self.layout.addWidget(self.closeButton)
self.setLayout(self.layout)
self.start = QPoint(0, 0)
self.pressing = False
self.maximaze = False
def resizeEvent(self, QResizeEvent):
super(TitleBar, 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.move(self.mapToGlobal(self.movement))
self.start = self.end
def mouseReleaseEvent(self, QMouseEvent):
self.pressing = False
def on_click_close(self):
sys.exit()
def on_click_maximize(self):
self.maximaze = not self.maximaze
if self.maximaze: self.parent.setWindowState(Qt.WindowNoState)
if not self.maximaze:
self.parent.setWindowState(Qt.WindowMaximized)
def on_click_hide(self):
self.parent.showMinimized()
class StatusBar(QWidget):
def __init__(self, parent):
super(StatusBar, self).__init__()
self.initUI()
self.showMessage("showMessage: Hello world!")
def initUI(self):
self.label = QLabel("Status bar...")
self.label.setFixedHeight(24)
self.label.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)
self.label.setStyleSheet("""
background-color: #23272a;
font-size: 12px;
padding-left: 5px;
color: white;
""")
self.layout = QHBoxLayout()
self.layout.setContentsMargins(0,0,0,0)
self.layout.addWidget(self.label)
self.setLayout(self.layout)
def showMessage(self, text):
self.label.setText(text)
class MainWindow(QWidget):
def __init__(self):
super(MainWindow, self).__init__()
self.setFixedSize(800, 400)
self.setWindowFlags(self.windowFlags() | Qt.FramelessWindowHint)
self.setStyleSheet("background-color: #2c2f33;")
self.setWindowTitle('Code Maker')
self.title_bar = TitleBar(self)
self.status_bar = StatusBar(self)
self.layout = QVBoxLayout()
self.layout.setContentsMargins(0,0,0,0)
self.layout.addWidget(self.title_bar)
self.layout.addStretch(1)
self.layout.addWidget(self.status_bar)
self.layout.setSpacing(0)
self.setLayout(self.layout)
if __name__ == "__main__":
app = QApplication(sys.argv)
mw = MainWindow()
mw.show()
sys.exit(app.exec_())
虽然@SNick 提供的答案很好,但我想添加我自己的建议。
它有点复杂,但他的解决方案缺少一些功能,我认为这些功能很重要。
- 它实际上使用了一个 QMainWindow;
- 它可以与在 Designer 中创建的 ui 文件一起使用。我只尝试使用
uic.loadUi
,但我认为它也适用于使用 pyuic
创建的文件;
- 由于可以使用ui个文件,可以直接在设计器中创建菜单;
- 作为 QMainWindow,statusBar 是 actual QStatusBar;
- 支持当前系统主题风格和颜色;
- 在Windows下支持window系统菜单;
- 没有大小限制,因为菜单和标题会根据可用的 space
自行调整大小
也就是说,它并不完美。到目前为止,我发现的唯一缺点是它不支持顶部区域的工具栏和停靠小部件;我认为可以做到,但会有点复杂。
诀窍是创建一个 不是 原生的 QMenuBar(或使用 Designer 中的一组),这对 MacOS 非常重要,并添加上边距到中央小部件,然后使用内部 QStyleOptionTitleBar 计算其大小并绘制其内容。
请注意,无论何时您想要添加菜单栏 items/menu、设置其他属性或手动设置中央小部件,都必须确保在 *** END OF SETUP ***
评论。
import sys
if sys.platform == 'win32':
import win32gui
import win32con
from PyQt5 import QtCore, QtGui, QtWidgets
# a "fake" button class that we need for hover and click events
class HiddenButton(QtWidgets.QPushButton):
hover = QtCore.pyqtSignal()
def __init__(self, parent):
super(HiddenButton, self).__init__(parent)
# prevent any painting to keep this button "invisible" while
# still reacting to its events
self.setUpdatesEnabled(False)
self.setFocusPolicy(QtCore.Qt.NoFocus)
def enterEvent(self, event):
self.hover.emit()
def leaveEvent(self, event):
self.hover.emit()
class SpecialTitleWindow(QtWidgets.QMainWindow):
__watchedActions = (
QtCore.QEvent.ActionAdded,
QtCore.QEvent.ActionChanged,
QtCore.QEvent.ActionRemoved
)
titleOpt = None
__menuBar = None
__titleBarMousePos = None
__sysMenuLock = False
__topMargin = 0
def __init__(self):
super(SpecialTitleWindow, self).__init__()
self.widgetHelpers = []
# uic.loadUi('titlebar.ui', self)
# enable the system menu
self.setWindowFlags(
QtCore.Qt.FramelessWindowHint | QtCore.Qt.WindowSystemMenuHint
)
# set the WindowActive to ensure that the title bar is painted as active
self.setWindowState(self.windowState() | QtCore.Qt.WindowActive)
# create a StyleOption that we need for painting and computing sizes
self.titleOpt = QtWidgets.QStyleOptionTitleBar()
self.titleOpt.initFrom(self)
self.titleOpt.titleBarFlags = (
QtCore.Qt.Window | QtCore.Qt.MSWindowsOwnDC |
QtCore.Qt.CustomizeWindowHint | QtCore.Qt.WindowTitleHint |
QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.WindowMinMaxButtonsHint |
QtCore.Qt.WindowCloseButtonHint
)
self.titleOpt.state |= (QtWidgets.QStyle.State_Active |
QtWidgets.QStyle.State_HasFocus)
self.titleOpt.titleBarState = (int(self.windowState()) |
int(QtWidgets.QStyle.State_Active))
# create "fake" buttons
self.systemButton = HiddenButton(self)
self.systemButton.pressed.connect(self.showSystemMenu)
self.minimizeButton = HiddenButton(self)
self.minimizeButton.hover.connect(self.checkHoverStates)
self.minimizeButton.clicked.connect(self.minimize)
self.maximizeButton = HiddenButton(self)
self.maximizeButton.hover.connect(self.checkHoverStates)
self.maximizeButton.clicked.connect(self.maximize)
self.closeButton = HiddenButton(self)
self.closeButton.hover.connect(self.checkHoverStates)
self.closeButton.clicked.connect(self.close)
self.ctrlButtons = {
QtWidgets.QStyle.SC_TitleBarMinButton: self.minimizeButton,
QtWidgets.QStyle.SC_TitleBarMaxButton: self.maximizeButton,
QtWidgets.QStyle.SC_TitleBarNormalButton: self.maximizeButton,
QtWidgets.QStyle.SC_TitleBarCloseButton: self.closeButton,
}
self.widgetHelpers.extend([self.minimizeButton, self.maximizeButton, self.closeButton])
self.resetTitleHeight()
# *** END OF SETUP ***
fileMenu = self.menuBar().addMenu('File')
fileMenu.addAction('Open')
fileMenu.addAction('Save')
workMenu = self.menuBar().addMenu('Work')
workMenu.addAction('Work something')
analysisMenu = self.menuBar().addMenu('Analysis')
analysisMenu.addAction('Analize action')
# just call the statusBar to create one, we use it for resizing purposes
self.statusBar()
def resetTitleHeight(self):
# minimum height for the menu can change everytime an action is added,
# removed or modified; let's update it accordingly
if not self.titleOpt:
return
# set the minimum height of the titlebar
self.titleHeight = max(
self.style().pixelMetric(
QtWidgets.QStyle.PM_TitleBarHeight, self.titleOpt, self),
self.menuBar().sizeHint().height()
)
self.titleOpt.rect.setHeight(self.titleHeight)
self.menuBar().setMaximumHeight(self.titleHeight)
if self.minimumHeight() < self.titleHeight:
self.setMinimumHeight(self.titleHeight)
def checkHoverStates(self):
if not self.titleOpt:
return
# update the window buttons when hovering
pos = self.mapFromGlobal(QtGui.QCursor.pos())
for ctrl, btn in self.ctrlButtons.items():
rect = self.style().subControlRect(QtWidgets.QStyle.CC_TitleBar,
self.titleOpt, ctrl, self)
# since the maximize button can become a "restore", ensure that it
# actually exists according to the current state, if the rect
# has an actual size
if rect and pos in rect:
self.titleOpt.activeSubControls = ctrl
self.titleOpt.state |= QtWidgets.QStyle.State_MouseOver
break
else:
# no hover
self.titleOpt.state &= ~QtWidgets.QStyle.State_MouseOver
self.titleOpt.activeSubControls = QtWidgets.QStyle.SC_None
self.titleOpt.state |= QtWidgets.QStyle.State_Active
self.update()
def showSystemMenu(self, pos=None):
# show the system menu on windows
if sys.platform != 'win32':
return
if self.__sysMenuLock:
self.__sysMenuLock = False
return
winId = int(self.effectiveWinId())
sysMenu = win32gui.GetSystemMenu(winId, False)
if pos is None:
pos = self.systemButton.mapToGlobal(self.systemButton.rect().bottomLeft())
self.__sysMenuLock = True
cmd = win32gui.TrackPopupMenu(sysMenu,
win32gui.TPM_LEFTALIGN | win32gui.TPM_TOPALIGN | win32gui.TPM_RETURNCMD,
pos.x(), pos.y(), 0, winId, None)
win32gui.PostMessage(winId, win32con.WM_SYSCOMMAND, cmd, 0)
# restore the menu lock to hide it when clicking the system menu icon
QtCore.QTimer.singleShot(0, lambda: setattr(self, '__sysMenuLock', False))
def actualWindowTitle(self):
# window title can show "*" for modified windows
title = self.windowTitle()
if title:
title = title.replace('[*]', '*' if self.isWindowModified() else '')
return title
def updateTitleBar(self):
# compute again sizes when resizing or changing window title
menuWidth = self.menuBar().sizeHint().width()
availableRect = self.style().subControlRect(QtWidgets.QStyle.CC_TitleBar,
self.titleOpt, QtWidgets.QStyle.SC_TitleBarLabel, self)
left = availableRect.left()
if self.menuBar().sizeHint().height() < self.titleHeight:
top = (self.titleHeight - self.menuBar().sizeHint().height()) // 2
height = self.menuBar().sizeHint().height()
else:
top = 0
height = self.titleHeight
title = self.actualWindowTitle()
titleWidth = self.fontMetrics().width(title)
if not title and menuWidth > availableRect.width():
# resize the menubar to its maximum, but without hiding the buttons
width = availableRect.width()
elif menuWidth + titleWidth > availableRect.width():
# if the menubar and title require more than the available space,
# divide it equally, giving precedence to the window title space,
# since it is also necessary for window movement
width = availableRect.width() // 2
if menuWidth > titleWidth:
width = max(left, min(availableRect.width() - titleWidth, width))
# keep a minimum size for the menu arrow
if availableRect.width() - width < left:
width = left
extButton = self.menuBar().findChild(QtWidgets.QToolButton, 'qt_menubar_ext_button')
if self.isVisible() and extButton:
# if the "extButton" is visible (meaning that some item
# is hidden due to the menubar cannot be completely shown)
# resize to the last visible item + extButton, so that
# there's as much space available for the title
minWidth = extButton.width()
menuBar = self.menuBar()
spacing = self.style().pixelMetric(QtWidgets.QStyle.PM_MenuBarItemSpacing)
for i, action in enumerate(menuBar.actions()):
actionWidth = menuBar.actionGeometry(action).width()
if minWidth + actionWidth > width:
width = minWidth
break
minWidth += actionWidth + spacing
else:
width = menuWidth
self.menuBar().setGeometry(left, top, width, height)
# ensure that our internal widget are always on top
for w in self.widgetHelpers:
w.raise_()
self.update()
# helper function to avoid "ugly" colors on menubar items
def __setMenuBar(self, menuBar):
if self.__menuBar:
if self.__menuBar in self.widgetHelpers:
self.widgetHelpers.remove(self.__menuBar)
self.__menuBar.removeEventFilter(self)
self.__menuBar = menuBar
self.widgetHelpers.append(menuBar)
self.__menuBar.installEventFilter(self)
self.__menuBar.setNativeMenuBar(False)
self.__menuBar.setStyleSheet('''
QMenuBar {
background-color: transparent;
}
QMenuBar::item {
background-color: transparent;
}
QMenuBar::item:selected {
background-color: palette(button);
}
''')
def setMenuBar(self, menuBar):
self.__setMenuBar(menuBar)
def menuBar(self):
# QMainWindow.menuBar() returns a new blank menu bar if none exists
if not self.__menuBar:
self.__setMenuBar(QtWidgets.QMenuBar(self))
return self.__menuBar
def setCentralWidget(self, widget):
if self.centralWidget():
self.centralWidget().removeEventFilter(self)
# store the top content margin, we need it later
l, self.__topMargin, r, b = widget.getContentsMargins()
super(SpecialTitleWindow, self).setCentralWidget(widget)
# since the central widget always uses all the available space and can
# capture mouse events, install an event filter to catch them and
# allow us to grab them
widget.installEventFilter(self)
def eventFilter(self, source, event):
if source == self.centralWidget():
# do not propagate mouse press events to the centralWidget!
if (event.type() == QtCore.QEvent.MouseButtonPress and
event.button() == QtCore.Qt.LeftButton and
event.y() <= self.titleHeight):
self.__titleBarMousePos = event.pos()
event.accept()
return True
elif source == self.__menuBar and event.type() in self.__watchedActions:
self.resetTitleHeight()
return super(SpecialTitleWindow, self).eventFilter(source, event)
def minimize(self):
self.setWindowState(QtCore.Qt.WindowMinimized)
def maximize(self):
if self.windowState() & QtCore.Qt.WindowMaximized:
self.setWindowState(
self.windowState() & (~QtCore.Qt.WindowMaximized | QtCore.Qt.WindowActive))
else:
self.setWindowState(
self.windowState() | QtCore.Qt.WindowMaximized | QtCore.Qt.WindowActive)
# whenever a window is resized, its button states have to be checked again
self.checkHoverStates()
def contextMenuEvent(self, event):
if event.pos() not in self.menuBar().geometry():
self.showSystemMenu(event.globalPos())
def mousePressEvent(self, event):
if not self.centralWidget() and (event.type() == QtCore.QEvent.MouseButtonPress and
event.button() == QtCore.Qt.LeftButton and event.y() <= self.titleHeight):
self.__titleBarMousePos = event.pos()
def mouseMoveEvent(self, event):
super(SpecialTitleWindow, self).mouseMoveEvent(event)
if event.buttons() == QtCore.Qt.LeftButton and self.__titleBarMousePos:
# move the window
self.move(self.pos() + event.pos() - self.__titleBarMousePos)
def mouseDoubleClickEvent(self, event):
if event.button() == QtCore.Qt.LeftButton:
self.maximize()
def mouseReleaseEvent(self, event):
super(SpecialTitleWindow, self).mouseReleaseEvent(event)
self.__titleBarMousePos = None
def changeEvent(self, event):
# change the appearance of the titlebar according to the window state
if event.type() == QtCore.QEvent.ActivationChange:
if self.isActiveWindow():
self.titleOpt.titleBarState = (
int(self.windowState()) | int(QtWidgets.QStyle.State_Active))
self.titleOpt.palette.setCurrentColorGroup(QtGui.QPalette.Active)
else:
self.titleOpt.titleBarState = 0
self.titleOpt.palette.setCurrentColorGroup(QtGui.QPalette.Inactive)
self.update()
elif event.type() == QtCore.QEvent.WindowStateChange:
self.checkHoverStates()
elif event.type() == QtCore.QEvent.WindowTitleChange:
if self.titleOpt:
self.updateTitleBar()
def showEvent(self, event):
if not event.spontaneous():
# update the titlebar as soon as it's shown, to ensure that
# most of the title text is visible
self.updateTitleBar()
def resizeEvent(self, event):
super(SpecialTitleWindow, self).resizeEvent(event)
# update the centralWidget contents margins, adding the titlebar height
# to the top margin found before
if (self.centralWidget() and
self.centralWidget().getContentsMargins()[1] + self.__topMargin != self.titleHeight):
l, t, r, b = self.centralWidget().getContentsMargins()
self.centralWidget().setContentsMargins(
l, self.titleHeight + self.__topMargin, r, b)
# resize the width of the titlebar option, and move its buttons
self.titleOpt.rect.setWidth(self.width())
for ctrl, btn in self.ctrlButtons.items():
rect = self.style().subControlRect(
QtWidgets.QStyle.CC_TitleBar, self.titleOpt, ctrl, self)
if rect:
btn.setGeometry(rect)
sysRect = self.style().subControlRect(QtWidgets.QStyle.CC_TitleBar,
self.titleOpt, QtWidgets.QStyle.SC_TitleBarSysMenu, self)
if sysRect:
self.systemButton.setGeometry(sysRect)
self.titleOpt.titleBarState = int(self.windowState())
if self.isActiveWindow():
self.titleOpt.titleBarState |= int(QtWidgets.QStyle.State_Active)
self.updateTitleBar()
def paintEvent(self, event):
qp = QtGui.QPainter(self)
self.style().drawComplexControl(QtWidgets.QStyle.CC_TitleBar, self.titleOpt, qp, self)
titleRect = self.style().subControlRect(QtWidgets.QStyle.CC_TitleBar,
self.titleOpt, QtWidgets.QStyle.SC_TitleBarLabel, self)
icon = self.windowIcon()
if not icon.isNull():
iconRect = QtCore.QRect(0, 0, titleRect.left(), self.titleHeight)
qp.drawPixmap(iconRect, icon.pixmap(iconRect.size()))
title = self.actualWindowTitle()
titleRect.setLeft(self.menuBar().geometry().right())
if title:
# move left of the rectangle available for the title to the right of
# the menubar; if the title is bigger than the available space, elide it
elided = self.fontMetrics().elidedText(
title, QtCore.Qt.ElideRight, titleRect.width() - 2)
qp.drawText(titleRect, QtCore.Qt.AlignCenter, elided)
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
w = SpecialTitleWindow()
w.setWindowTitle('My window')
w.show()
sys.exit(app.exec_())
为了@musicamante 提供的代码可以在 PySide6 上运行,我做了一些补丁,但是我发现移动 window 时,它在晃动。当我查看 mouseMoveEvent 并阅读 QMouseEvent
中关于 event.pos()
的 qt 文档时,它说:
If you move the widget as a result of the mouse event, use the global position returned by globalPos()
to avoid a shaking motion.
所以我替换了
self.move(self.pos() + event.pos() - self.__titleBarMousePos)
和
self.move(event.globalPos() - self.__titleBarMousePos)
在mouseMoveEvent()
这里是 PySide6 版本:
import sys
if sys.platform == 'win32':
import win32gui
import win32con
from PySide6 import QtCore, QtGui, QtWidgets
# a "fake" button class that we need for hover and click events
class HiddenButton(QtWidgets.QPushButton):
hover = QtCore.Signal()
def __init__(self, parent):
super(HiddenButton, self).__init__(parent)
# prevent any painting to keep this button "invisible" while
# still reacting to its events
self.setUpdatesEnabled(False)
self.setFocusPolicy(QtCore.Qt.NoFocus)
def enterEvent(self, event):
self.hover.emit()
def leaveEvent(self, event):
self.hover.emit()
class SpecialTitleWindow(QtWidgets.QMainWindow):
__watchedActions = (
QtCore.QEvent.ActionAdded,
QtCore.QEvent.ActionChanged,
QtCore.QEvent.ActionRemoved
)
titleOpt = None
__menuBar = None
__titleBarMousePos = None
__sysMenuLock = False
__topMargin = 0
def __init__(self):
super(SpecialTitleWindow, self).__init__()
self.widgetHelpers = []
# uic.loadUi('titlebar.ui', self)
# enable the system menu
self.setWindowFlags(
QtCore.Qt.FramelessWindowHint | QtCore.Qt.WindowSystemMenuHint
)
# set the WindowActive to ensure that the title bar is painted as active
self.setWindowState(self.windowState() | QtCore.Qt.WindowActive)
# create a StyleOption that we need for painting and computing sizes
self.titleOpt = QtWidgets.QStyleOptionTitleBar()
self.titleOpt.initFrom(self)
self.titleOpt.titleBarFlags = (
QtCore.Qt.Window | QtCore.Qt.MSWindowsOwnDC |
QtCore.Qt.CustomizeWindowHint | QtCore.Qt.WindowTitleHint |
QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.WindowMinMaxButtonsHint |
QtCore.Qt.WindowCloseButtonHint
)
self.titleOpt.state |= (QtWidgets.QStyle.State_Active |
QtWidgets.QStyle.State_HasFocus)
self.titleOpt.titleBarState = (int(self.windowState()) |
int(QtWidgets.QStyle.State_Active))
# create "fake" buttons
self.systemButton = HiddenButton(self)
self.systemButton.pressed.connect(self.showSystemMenu)
self.minimizeButton = HiddenButton(self)
self.minimizeButton.hover.connect(self.checkHoverStates)
self.minimizeButton.clicked.connect(self.minimize)
self.maximizeButton = HiddenButton(self)
self.maximizeButton.hover.connect(self.checkHoverStates)
self.maximizeButton.clicked.connect(self.maximize)
self.closeButton = HiddenButton(self)
self.closeButton.hover.connect(self.checkHoverStates)
self.closeButton.clicked.connect(self.close)
self.ctrlButtons = {
QtWidgets.QStyle.SC_TitleBarMinButton: self.minimizeButton,
QtWidgets.QStyle.SC_TitleBarMaxButton: self.maximizeButton,
QtWidgets.QStyle.SC_TitleBarNormalButton: self.maximizeButton,
QtWidgets.QStyle.SC_TitleBarCloseButton: self.closeButton,
}
self.widgetHelpers.extend([self.minimizeButton, self.maximizeButton, self.closeButton])
self.resetTitleHeight()
# *** END OF SETUP ***
fileMenu = self.menuBar().addMenu('File')
fileMenu.addAction('Open')
fileMenu.addAction('Save')
workMenu = self.menuBar().addMenu('Work')
workMenu.addAction('Work something')
analysisMenu = self.menuBar().addMenu('Analysis')
analysisMenu.addAction('Analize action')
# just call the statusBar to create one, we use it for resizing purposes
self.statusBar()
self.resize(400, 250)
def resetTitleHeight(self):
# minimum height for the menu can change everytime an action is added,
# removed or modified; let's update it accordingly
if not self.titleOpt:
return
# set the minimum height of the titlebar
self.titleHeight = max(
self.style().pixelMetric(
QtWidgets.QStyle.PM_TitleBarHeight, self.titleOpt, self),
self.menuBar().sizeHint().height()
)
self.titleOpt.rect.setHeight(self.titleHeight)
self.menuBar().setMaximumHeight(self.titleHeight)
if self.minimumHeight() < self.titleHeight:
self.setMinimumHeight(self.titleHeight)
def checkHoverStates(self):
if not self.titleOpt:
return
# update the window buttons when hovering
pos = self.mapFromGlobal(QtGui.QCursor.pos())
for ctrl, btn in self.ctrlButtons.items():
rect = self.style().subControlRect(QtWidgets.QStyle.CC_TitleBar,
self.titleOpt, ctrl, self)
# since the maximize button can become a "restore", ensure that it
# actually exists according to the current state, if the rect
# has an actual size
if rect and rect.contains(pos):
self.titleOpt.activeSubControls = ctrl
self.titleOpt.state |= QtWidgets.QStyle.State_MouseOver
break
else:
# no hover
self.titleOpt.state &= ~QtWidgets.QStyle.State_MouseOver
self.titleOpt.activeSubControls = QtWidgets.QStyle.SC_None
self.titleOpt.state |= QtWidgets.QStyle.State_Active
self.update()
def showSystemMenu(self, pos=None):
# show the system menu on windows
if sys.platform != 'win32':
return
if self.__sysMenuLock:
self.__sysMenuLock = False
return
winId = int(self.effectiveWinId())
sysMenu = win32gui.GetSystemMenu(winId, False)
if pos is None:
pos = self.systemButton.mapToGlobal(self.systemButton.rect().bottomLeft())
self.__sysMenuLock = True
cmd = win32gui.TrackPopupMenu(sysMenu,
win32gui.TPM_LEFTALIGN | win32gui.TPM_TOPALIGN | win32gui.TPM_RETURNCMD,
pos.x(), pos.y(), 0, winId, None)
win32gui.PostMessage(winId, win32con.WM_SYSCOMMAND, cmd, 0)
# restore the menu lock to hide it when clicking the system menu icon
QtCore.QTimer.singleShot(0, lambda: setattr(self, '__sysMenuLock', False))
def actualWindowTitle(self):
# window title can show "*" for modified windows
title = self.windowTitle()
if title:
title = title.replace('[*]', '*' if self.isWindowModified() else '')
return title
def updateTitleBar(self):
# compute again sizes when resizing or changing window title
menuWidth = self.menuBar().sizeHint().width()
availableRect = self.style().subControlRect(QtWidgets.QStyle.CC_TitleBar,
self.titleOpt, QtWidgets.QStyle.SC_TitleBarLabel, self)
left = availableRect.left()
if self.menuBar().sizeHint().height() < self.titleHeight:
top = (self.titleHeight - self.menuBar().sizeHint().height()) // 2
height = self.menuBar().sizeHint().height()
else:
top = 0
height = self.titleHeight
title = self.actualWindowTitle()
titleWidth = self.fontMetrics().boundingRect(title).width()
if not title and menuWidth > availableRect.width():
# resize the menubar to its maximum, but without hiding the buttons
width = availableRect.width()
elif menuWidth + titleWidth > availableRect.width():
# if the menubar and title require more than the available space,
# divide it equally, giving precedence to the window title space,
# since it is also necessary for window movement
width = availableRect.width() // 2
if menuWidth > titleWidth:
width = max(left, min(availableRect.width() - titleWidth, width))
# keep a minimum size for the menu arrow
if availableRect.width() - width < left:
width = left
extButton = self.menuBar().findChild(QtWidgets.QToolButton, 'qt_menubar_ext_button')
if self.isVisible() and extButton:
# if the "extButton" is visible (meaning that some item
# is hidden due to the menubar cannot be completely shown)
# resize to the last visible item + extButton, so that
# there's as much space available for the title
minWidth = extButton.width()
menuBar = self.menuBar()
spacing = self.style().pixelMetric(QtWidgets.QStyle.PM_MenuBarItemSpacing)
for i, action in enumerate(menuBar.actions()):
actionWidth = menuBar.actionGeometry(action).width()
if minWidth + actionWidth > width:
width = minWidth
break
minWidth += actionWidth + spacing
else:
width = menuWidth
self.menuBar().setGeometry(left, top, width, height)
# ensure that our internal widget are always on top
for w in self.widgetHelpers:
w.raise_()
self.update()
# helper function to avoid "ugly" colors on menubar items
def __setMenuBar(self, menuBar):
if self.__menuBar:
if self.__menuBar in self.widgetHelpers:
self.widgetHelpers.remove(self.__menuBar)
self.__menuBar.removeEventFilter(self)
self.__menuBar = menuBar
self.widgetHelpers.append(menuBar)
self.__menuBar.installEventFilter(self)
self.__menuBar.setNativeMenuBar(False)
self.__menuBar.setStyleSheet('''
QMenuBar {
background-color: transparent;
}
QMenuBar::item {
background-color: transparent;
}
QMenuBar::item:selected {
background-color: palette(button);
}
''')
def setMenuBar(self, menuBar):
self.__setMenuBar(menuBar)
def menuBar(self):
# QMainWindow.menuBar() returns a new blank menu bar if none exists
if not self.__menuBar:
self.__setMenuBar(QtWidgets.QMenuBar(self))
return self.__menuBar
def setCentralWidget(self, widget):
if self.centralWidget():
self.centralWidget().removeEventFilter(self)
# store the top content margin, we need it later
l, self.__topMargin, r, b = widget.contentsMargins()
super(SpecialTitleWindow, self).setCentralWidget(widget)
# since the central widget always uses all the available space and can
# capture mouse events, install an event filter to catch them and
# allow us to grab them
widget.installEventFilter(self)
def eventFilter(self, source, event):
if source == self.centralWidget():
# do not propagate mouse press events to the centralWidget!
if (event.type() == QtCore.QEvent.MouseButtonPress and
event.button() == QtCore.Qt.LeftButton and
event.y() <= self.titleHeight):
self.__titleBarMousePos = event.pos()
event.accept()
return True
elif source == self.__menuBar and event.type() in self.__watchedActions:
self.resetTitleHeight()
return super(SpecialTitleWindow, self).eventFilter(source, event)
def minimize(self):
self.setWindowState(QtCore.Qt.WindowMinimized)
def maximize(self):
if self.windowState() & QtCore.Qt.WindowMaximized:
self.setWindowState(
self.windowState() & (~QtCore.Qt.WindowMaximized | QtCore.Qt.WindowActive))
else:
self.setWindowState(
self.windowState() | QtCore.Qt.WindowMaximized | QtCore.Qt.WindowActive)
# whenever a window is resized, its button states have to be checked again
self.checkHoverStates()
def contextMenuEvent(self, event):
if not self.menuBar().geometry().contains(event.pos()):
self.showSystemMenu(event.globalPos())
def mousePressEvent(self, event):
if not self.centralWidget() and (event.type() == QtCore.QEvent.MouseButtonPress and
event.button() == QtCore.Qt.LeftButton and event.position().y() <= self.titleHeight):
self.__titleBarMousePos = event.position()
def mouseMoveEvent(self, event):
super(SpecialTitleWindow, self).mouseMoveEvent(event)
if event.buttons() == QtCore.Qt.LeftButton and self.__titleBarMousePos:
# move the window
self.move((event.globalPosition() - self.__titleBarMousePos).toPoint())
def mouseDoubleClickEvent(self, event):
if event.button() == QtCore.Qt.LeftButton:
self.maximize()
def mouseReleaseEvent(self, event):
super(SpecialTitleWindow, self).mouseReleaseEvent(event)
self.__titleBarMousePos = None
def changeEvent(self, event):
# change the appearance of the titlebar according to the window state
if event.type() == QtCore.QEvent.ActivationChange:
if self.isActiveWindow():
self.titleOpt.titleBarState = (
int(self.windowState()) | int(QtWidgets.QStyle.State_Active))
self.titleOpt.palette.setCurrentColorGroup(QtGui.QPalette.Active)
else:
self.titleOpt.titleBarState = 0
self.titleOpt.palette.setCurrentColorGroup(QtGui.QPalette.Inactive)
self.update()
elif event.type() == QtCore.QEvent.WindowStateChange:
self.checkHoverStates()
elif event.type() == QtCore.QEvent.WindowTitleChange:
if self.titleOpt:
self.updateTitleBar()
def showEvent(self, event):
if not event.spontaneous():
# update the titlebar as soon as it's shown, to ensure that
# most of the title text is visible
self.updateTitleBar()
def resizeEvent(self, event):
super(SpecialTitleWindow, self).resizeEvent(event)
# update the centralWidget contents margins, adding the titlebar height
# to the top margin found before
if (self.centralWidget() and
self.centralWidget().getContentsMargins()[1] + self.__topMargin != self.titleHeight):
l, t, r, b = self.centralWidget().getContentsMargins()
self.centralWidget().setContentsMargins(
l, self.titleHeight + self.__topMargin, r, b)
# resize the width of the titlebar option, and move its buttons
self.titleOpt.rect.setWidth(self.width())
for ctrl, btn in self.ctrlButtons.items():
rect = self.style().subControlRect(
QtWidgets.QStyle.CC_TitleBar, self.titleOpt, ctrl, self)
if rect:
btn.setGeometry(rect)
sysRect = self.style().subControlRect(QtWidgets.QStyle.CC_TitleBar,
self.titleOpt, QtWidgets.QStyle.SC_TitleBarSysMenu, self)
if sysRect:
self.systemButton.setGeometry(sysRect)
self.titleOpt.titleBarState = int(self.windowState())
if self.isActiveWindow():
self.titleOpt.titleBarState |= int(QtWidgets.QStyle.State_Active)
self.updateTitleBar()
def paintEvent(self, event):
qp = QtGui.QPainter(self)
self.style().drawComplexControl(QtWidgets.QStyle.CC_TitleBar, self.titleOpt, qp, self)
titleRect = self.style().subControlRect(QtWidgets.QStyle.CC_TitleBar,
self.titleOpt, QtWidgets.QStyle.SC_TitleBarLabel, self)
icon = self.windowIcon()
if not icon.isNull():
iconRect = QtCore.QRect(0, 0, titleRect.left(), self.titleHeight)
qp.drawPixmap(iconRect, icon.pixmap(iconRect.size()))
title = self.actualWindowTitle()
titleRect.setLeft(self.menuBar().geometry().right())
if title:
# move left of the rectangle available for the title to the right of
# the menubar; if the title is bigger than the available space, elide it
elided = self.fontMetrics().elidedText(
title, QtCore.Qt.ElideRight, titleRect.width() - 2)
qp.drawText(titleRect, QtCore.Qt.AlignCenter, elided)
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
w = SpecialTitleWindow()
w.setWindowTitle('My window')
w.show()
sys.exit(app.exec())
我想将菜单和标题设置在一个栏中,但不知道如何布局菜单栏和标题栏(或者我自己的标题栏)。
# -*- coding:utf-8 -*-
from PyQt5 import QtWidgets,QtGui,QtCore
import sys
qss = ""
class UI(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.setui()
def setui(self):
#----------main-window----------------------
self.setGeometry(0,0,1366,768) #x,y,w,h
self.setWindowTitle('hello world')
self.setWindowFlag(QtCore.Qt.FramelessWindowHint)
#----------menu-bar---------------------
#--------file-menu-----
self.menu_file=self.menuBar().addMenu('file')
self.menu_file_open=self.menu_file.addAction('open')
self.menu_file_save=self.menu_file.addAction('save')
self.menu_file_saveas=self.menu_file.addAction('save as...')
self.menu_file_quit=self.menu_file.addAction('exit')
#-----------experient-menu----------
self.menu_work=self.menuBar().addMenu('work')
#-------------analysis-menu---------
self.menu_analysis=self.menuBar().addMenu('analysis')
#------------edit-menu--------------
self.menu_edit=self.menuBar().addMenu('edit')
#------------window-menu--------------
self.menu_window=self.menuBar().addMenu('window')
#------------help---menu--------------
self.menu_help=self.menuBar().addMenu('help')
#-------------set---qss----------------------
self.setStyleSheet(qss)
#-------functions--connect-------------------
self.menu_file_quit.triggered.connect(QtWidgets.qApp.quit)
self.show()
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
ex = UI()
sys.exit(app.exec_())
我希望一个栏包括图标、菜单、标题和三个按钮,就像vscode的菜单栏一样。
试一试:
import sys
from PyQt5.QtCore import pyqtSlot, QPoint, Qt, QRect
from PyQt5.QtWidgets import (QMainWindow, QApplication, QPushButton, QHBoxLayout,
QVBoxLayout, QTabWidget, QWidget, QAction,
QLabel, QSizeGrip, QMenuBar, qApp)
from PyQt5.QtGui import QIcon
class TitleBar(QWidget):
height = 35
def __init__(self, parent):
super(TitleBar, self).__init__()
self.parent = parent
self.layout = QHBoxLayout()
self.layout.setContentsMargins(0,0,0,0)
self.menu_bar = QMenuBar()
self.menu_bar.setStyleSheet("""
color: #fff;
background-color: #23272A;
font-size: 14px;
padding: 4px;
""")
self.menu_file = self.menu_bar.addMenu('file')
self.menu_file_open=self.menu_file.addAction('open')
self.menu_file_save=self.menu_file.addAction('save')
self.menu_file_saveas=self.menu_file.addAction('save as...')
self.menu_file_quit=self.menu_file.addAction('exit')
self.menu_file_quit.triggered.connect(qApp.quit)
self.menu_work=self.menu_bar.addMenu('work')
self.menu_analysis=self.menu_bar.addMenu('analysis')
self.menu_edit=self.menu_bar.addMenu('edit')
self.menu_window=self.menu_bar.addMenu('window')
self.menu_help=self.menu_bar.addMenu('help')
self.layout.addWidget(self.menu_bar)
self.title = QLabel("Hello World!")
self.title.setFixedHeight(self.height)
self.layout.addWidget(self.title)
self.title.setStyleSheet("""
background-color: #23272a; /* 23272a #f00*/
font-weight: bold;
font-size: 16px;
color: blue;
padding-left: 170px;
""")
self.closeButton = QPushButton(' ')
self.closeButton.clicked.connect(self.on_click_close)
self.closeButton.setStyleSheet("""
background-color: #DC143C;
border-radius: 10px;
height: {};
width: {};
margin-right: 3px;
font-weight: bold;
color: #000;
font-family: "Webdings";
qproperty-text: "r";
""".format(self.height/1.7,self.height/1.7))
self.maxButton = QPushButton(' ')
self.maxButton.clicked.connect(self.on_click_maximize)
self.maxButton.setStyleSheet("""
background-color: #32CD32;
border-radius: 10px;
height: {};
width: {};
margin-right: 3px;
font-weight: bold;
color: #000;
font-family: "Webdings";
qproperty-text: "1";
""".format(self.height/1.7,self.height/1.7))
self.hideButton = QPushButton(' ')
self.hideButton.clicked.connect(self.on_click_hide)
self.hideButton.setStyleSheet("""
background-color: #FFFF00;
border-radius: 10px;
height: {};
width: {};
margin-right: 3px;
font-weight: bold;
color: #000;
font-family: "Webdings";
qproperty-text: "0";
""".format(self.height/1.7,self.height/1.7))
self.layout.addWidget(self.hideButton)
self.layout.addWidget(self.maxButton)
self.layout.addWidget(self.closeButton)
self.setLayout(self.layout)
self.start = QPoint(0, 0)
self.pressing = False
self.maximaze = False
def resizeEvent(self, QResizeEvent):
super(TitleBar, 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.move(self.mapToGlobal(self.movement))
self.start = self.end
def mouseReleaseEvent(self, QMouseEvent):
self.pressing = False
def on_click_close(self):
sys.exit()
def on_click_maximize(self):
self.maximaze = not self.maximaze
if self.maximaze: self.parent.setWindowState(Qt.WindowNoState)
if not self.maximaze:
self.parent.setWindowState(Qt.WindowMaximized)
def on_click_hide(self):
self.parent.showMinimized()
class StatusBar(QWidget):
def __init__(self, parent):
super(StatusBar, self).__init__()
self.initUI()
self.showMessage("showMessage: Hello world!")
def initUI(self):
self.label = QLabel("Status bar...")
self.label.setFixedHeight(24)
self.label.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)
self.label.setStyleSheet("""
background-color: #23272a;
font-size: 12px;
padding-left: 5px;
color: white;
""")
self.layout = QHBoxLayout()
self.layout.setContentsMargins(0,0,0,0)
self.layout.addWidget(self.label)
self.setLayout(self.layout)
def showMessage(self, text):
self.label.setText(text)
class MainWindow(QWidget):
def __init__(self):
super(MainWindow, self).__init__()
self.setFixedSize(800, 400)
self.setWindowFlags(self.windowFlags() | Qt.FramelessWindowHint)
self.setStyleSheet("background-color: #2c2f33;")
self.setWindowTitle('Code Maker')
self.title_bar = TitleBar(self)
self.status_bar = StatusBar(self)
self.layout = QVBoxLayout()
self.layout.setContentsMargins(0,0,0,0)
self.layout.addWidget(self.title_bar)
self.layout.addStretch(1)
self.layout.addWidget(self.status_bar)
self.layout.setSpacing(0)
self.setLayout(self.layout)
if __name__ == "__main__":
app = QApplication(sys.argv)
mw = MainWindow()
mw.show()
sys.exit(app.exec_())
虽然@SNick 提供的答案很好,但我想添加我自己的建议。
它有点复杂,但他的解决方案缺少一些功能,我认为这些功能很重要。
- 它实际上使用了一个 QMainWindow;
- 它可以与在 Designer 中创建的 ui 文件一起使用。我只尝试使用
uic.loadUi
,但我认为它也适用于使用pyuic
创建的文件; - 由于可以使用ui个文件,可以直接在设计器中创建菜单;
- 作为 QMainWindow,statusBar 是 actual QStatusBar;
- 支持当前系统主题风格和颜色;
- 在Windows下支持window系统菜单;
- 没有大小限制,因为菜单和标题会根据可用的 space 自行调整大小
也就是说,它并不完美。到目前为止,我发现的唯一缺点是它不支持顶部区域的工具栏和停靠小部件;我认为可以做到,但会有点复杂。
诀窍是创建一个 不是 原生的 QMenuBar(或使用 Designer 中的一组),这对 MacOS 非常重要,并添加上边距到中央小部件,然后使用内部 QStyleOptionTitleBar 计算其大小并绘制其内容。
请注意,无论何时您想要添加菜单栏 items/menu、设置其他属性或手动设置中央小部件,都必须确保在 *** END OF SETUP ***
评论。
import sys
if sys.platform == 'win32':
import win32gui
import win32con
from PyQt5 import QtCore, QtGui, QtWidgets
# a "fake" button class that we need for hover and click events
class HiddenButton(QtWidgets.QPushButton):
hover = QtCore.pyqtSignal()
def __init__(self, parent):
super(HiddenButton, self).__init__(parent)
# prevent any painting to keep this button "invisible" while
# still reacting to its events
self.setUpdatesEnabled(False)
self.setFocusPolicy(QtCore.Qt.NoFocus)
def enterEvent(self, event):
self.hover.emit()
def leaveEvent(self, event):
self.hover.emit()
class SpecialTitleWindow(QtWidgets.QMainWindow):
__watchedActions = (
QtCore.QEvent.ActionAdded,
QtCore.QEvent.ActionChanged,
QtCore.QEvent.ActionRemoved
)
titleOpt = None
__menuBar = None
__titleBarMousePos = None
__sysMenuLock = False
__topMargin = 0
def __init__(self):
super(SpecialTitleWindow, self).__init__()
self.widgetHelpers = []
# uic.loadUi('titlebar.ui', self)
# enable the system menu
self.setWindowFlags(
QtCore.Qt.FramelessWindowHint | QtCore.Qt.WindowSystemMenuHint
)
# set the WindowActive to ensure that the title bar is painted as active
self.setWindowState(self.windowState() | QtCore.Qt.WindowActive)
# create a StyleOption that we need for painting and computing sizes
self.titleOpt = QtWidgets.QStyleOptionTitleBar()
self.titleOpt.initFrom(self)
self.titleOpt.titleBarFlags = (
QtCore.Qt.Window | QtCore.Qt.MSWindowsOwnDC |
QtCore.Qt.CustomizeWindowHint | QtCore.Qt.WindowTitleHint |
QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.WindowMinMaxButtonsHint |
QtCore.Qt.WindowCloseButtonHint
)
self.titleOpt.state |= (QtWidgets.QStyle.State_Active |
QtWidgets.QStyle.State_HasFocus)
self.titleOpt.titleBarState = (int(self.windowState()) |
int(QtWidgets.QStyle.State_Active))
# create "fake" buttons
self.systemButton = HiddenButton(self)
self.systemButton.pressed.connect(self.showSystemMenu)
self.minimizeButton = HiddenButton(self)
self.minimizeButton.hover.connect(self.checkHoverStates)
self.minimizeButton.clicked.connect(self.minimize)
self.maximizeButton = HiddenButton(self)
self.maximizeButton.hover.connect(self.checkHoverStates)
self.maximizeButton.clicked.connect(self.maximize)
self.closeButton = HiddenButton(self)
self.closeButton.hover.connect(self.checkHoverStates)
self.closeButton.clicked.connect(self.close)
self.ctrlButtons = {
QtWidgets.QStyle.SC_TitleBarMinButton: self.minimizeButton,
QtWidgets.QStyle.SC_TitleBarMaxButton: self.maximizeButton,
QtWidgets.QStyle.SC_TitleBarNormalButton: self.maximizeButton,
QtWidgets.QStyle.SC_TitleBarCloseButton: self.closeButton,
}
self.widgetHelpers.extend([self.minimizeButton, self.maximizeButton, self.closeButton])
self.resetTitleHeight()
# *** END OF SETUP ***
fileMenu = self.menuBar().addMenu('File')
fileMenu.addAction('Open')
fileMenu.addAction('Save')
workMenu = self.menuBar().addMenu('Work')
workMenu.addAction('Work something')
analysisMenu = self.menuBar().addMenu('Analysis')
analysisMenu.addAction('Analize action')
# just call the statusBar to create one, we use it for resizing purposes
self.statusBar()
def resetTitleHeight(self):
# minimum height for the menu can change everytime an action is added,
# removed or modified; let's update it accordingly
if not self.titleOpt:
return
# set the minimum height of the titlebar
self.titleHeight = max(
self.style().pixelMetric(
QtWidgets.QStyle.PM_TitleBarHeight, self.titleOpt, self),
self.menuBar().sizeHint().height()
)
self.titleOpt.rect.setHeight(self.titleHeight)
self.menuBar().setMaximumHeight(self.titleHeight)
if self.minimumHeight() < self.titleHeight:
self.setMinimumHeight(self.titleHeight)
def checkHoverStates(self):
if not self.titleOpt:
return
# update the window buttons when hovering
pos = self.mapFromGlobal(QtGui.QCursor.pos())
for ctrl, btn in self.ctrlButtons.items():
rect = self.style().subControlRect(QtWidgets.QStyle.CC_TitleBar,
self.titleOpt, ctrl, self)
# since the maximize button can become a "restore", ensure that it
# actually exists according to the current state, if the rect
# has an actual size
if rect and pos in rect:
self.titleOpt.activeSubControls = ctrl
self.titleOpt.state |= QtWidgets.QStyle.State_MouseOver
break
else:
# no hover
self.titleOpt.state &= ~QtWidgets.QStyle.State_MouseOver
self.titleOpt.activeSubControls = QtWidgets.QStyle.SC_None
self.titleOpt.state |= QtWidgets.QStyle.State_Active
self.update()
def showSystemMenu(self, pos=None):
# show the system menu on windows
if sys.platform != 'win32':
return
if self.__sysMenuLock:
self.__sysMenuLock = False
return
winId = int(self.effectiveWinId())
sysMenu = win32gui.GetSystemMenu(winId, False)
if pos is None:
pos = self.systemButton.mapToGlobal(self.systemButton.rect().bottomLeft())
self.__sysMenuLock = True
cmd = win32gui.TrackPopupMenu(sysMenu,
win32gui.TPM_LEFTALIGN | win32gui.TPM_TOPALIGN | win32gui.TPM_RETURNCMD,
pos.x(), pos.y(), 0, winId, None)
win32gui.PostMessage(winId, win32con.WM_SYSCOMMAND, cmd, 0)
# restore the menu lock to hide it when clicking the system menu icon
QtCore.QTimer.singleShot(0, lambda: setattr(self, '__sysMenuLock', False))
def actualWindowTitle(self):
# window title can show "*" for modified windows
title = self.windowTitle()
if title:
title = title.replace('[*]', '*' if self.isWindowModified() else '')
return title
def updateTitleBar(self):
# compute again sizes when resizing or changing window title
menuWidth = self.menuBar().sizeHint().width()
availableRect = self.style().subControlRect(QtWidgets.QStyle.CC_TitleBar,
self.titleOpt, QtWidgets.QStyle.SC_TitleBarLabel, self)
left = availableRect.left()
if self.menuBar().sizeHint().height() < self.titleHeight:
top = (self.titleHeight - self.menuBar().sizeHint().height()) // 2
height = self.menuBar().sizeHint().height()
else:
top = 0
height = self.titleHeight
title = self.actualWindowTitle()
titleWidth = self.fontMetrics().width(title)
if not title and menuWidth > availableRect.width():
# resize the menubar to its maximum, but without hiding the buttons
width = availableRect.width()
elif menuWidth + titleWidth > availableRect.width():
# if the menubar and title require more than the available space,
# divide it equally, giving precedence to the window title space,
# since it is also necessary for window movement
width = availableRect.width() // 2
if menuWidth > titleWidth:
width = max(left, min(availableRect.width() - titleWidth, width))
# keep a minimum size for the menu arrow
if availableRect.width() - width < left:
width = left
extButton = self.menuBar().findChild(QtWidgets.QToolButton, 'qt_menubar_ext_button')
if self.isVisible() and extButton:
# if the "extButton" is visible (meaning that some item
# is hidden due to the menubar cannot be completely shown)
# resize to the last visible item + extButton, so that
# there's as much space available for the title
minWidth = extButton.width()
menuBar = self.menuBar()
spacing = self.style().pixelMetric(QtWidgets.QStyle.PM_MenuBarItemSpacing)
for i, action in enumerate(menuBar.actions()):
actionWidth = menuBar.actionGeometry(action).width()
if minWidth + actionWidth > width:
width = minWidth
break
minWidth += actionWidth + spacing
else:
width = menuWidth
self.menuBar().setGeometry(left, top, width, height)
# ensure that our internal widget are always on top
for w in self.widgetHelpers:
w.raise_()
self.update()
# helper function to avoid "ugly" colors on menubar items
def __setMenuBar(self, menuBar):
if self.__menuBar:
if self.__menuBar in self.widgetHelpers:
self.widgetHelpers.remove(self.__menuBar)
self.__menuBar.removeEventFilter(self)
self.__menuBar = menuBar
self.widgetHelpers.append(menuBar)
self.__menuBar.installEventFilter(self)
self.__menuBar.setNativeMenuBar(False)
self.__menuBar.setStyleSheet('''
QMenuBar {
background-color: transparent;
}
QMenuBar::item {
background-color: transparent;
}
QMenuBar::item:selected {
background-color: palette(button);
}
''')
def setMenuBar(self, menuBar):
self.__setMenuBar(menuBar)
def menuBar(self):
# QMainWindow.menuBar() returns a new blank menu bar if none exists
if not self.__menuBar:
self.__setMenuBar(QtWidgets.QMenuBar(self))
return self.__menuBar
def setCentralWidget(self, widget):
if self.centralWidget():
self.centralWidget().removeEventFilter(self)
# store the top content margin, we need it later
l, self.__topMargin, r, b = widget.getContentsMargins()
super(SpecialTitleWindow, self).setCentralWidget(widget)
# since the central widget always uses all the available space and can
# capture mouse events, install an event filter to catch them and
# allow us to grab them
widget.installEventFilter(self)
def eventFilter(self, source, event):
if source == self.centralWidget():
# do not propagate mouse press events to the centralWidget!
if (event.type() == QtCore.QEvent.MouseButtonPress and
event.button() == QtCore.Qt.LeftButton and
event.y() <= self.titleHeight):
self.__titleBarMousePos = event.pos()
event.accept()
return True
elif source == self.__menuBar and event.type() in self.__watchedActions:
self.resetTitleHeight()
return super(SpecialTitleWindow, self).eventFilter(source, event)
def minimize(self):
self.setWindowState(QtCore.Qt.WindowMinimized)
def maximize(self):
if self.windowState() & QtCore.Qt.WindowMaximized:
self.setWindowState(
self.windowState() & (~QtCore.Qt.WindowMaximized | QtCore.Qt.WindowActive))
else:
self.setWindowState(
self.windowState() | QtCore.Qt.WindowMaximized | QtCore.Qt.WindowActive)
# whenever a window is resized, its button states have to be checked again
self.checkHoverStates()
def contextMenuEvent(self, event):
if event.pos() not in self.menuBar().geometry():
self.showSystemMenu(event.globalPos())
def mousePressEvent(self, event):
if not self.centralWidget() and (event.type() == QtCore.QEvent.MouseButtonPress and
event.button() == QtCore.Qt.LeftButton and event.y() <= self.titleHeight):
self.__titleBarMousePos = event.pos()
def mouseMoveEvent(self, event):
super(SpecialTitleWindow, self).mouseMoveEvent(event)
if event.buttons() == QtCore.Qt.LeftButton and self.__titleBarMousePos:
# move the window
self.move(self.pos() + event.pos() - self.__titleBarMousePos)
def mouseDoubleClickEvent(self, event):
if event.button() == QtCore.Qt.LeftButton:
self.maximize()
def mouseReleaseEvent(self, event):
super(SpecialTitleWindow, self).mouseReleaseEvent(event)
self.__titleBarMousePos = None
def changeEvent(self, event):
# change the appearance of the titlebar according to the window state
if event.type() == QtCore.QEvent.ActivationChange:
if self.isActiveWindow():
self.titleOpt.titleBarState = (
int(self.windowState()) | int(QtWidgets.QStyle.State_Active))
self.titleOpt.palette.setCurrentColorGroup(QtGui.QPalette.Active)
else:
self.titleOpt.titleBarState = 0
self.titleOpt.palette.setCurrentColorGroup(QtGui.QPalette.Inactive)
self.update()
elif event.type() == QtCore.QEvent.WindowStateChange:
self.checkHoverStates()
elif event.type() == QtCore.QEvent.WindowTitleChange:
if self.titleOpt:
self.updateTitleBar()
def showEvent(self, event):
if not event.spontaneous():
# update the titlebar as soon as it's shown, to ensure that
# most of the title text is visible
self.updateTitleBar()
def resizeEvent(self, event):
super(SpecialTitleWindow, self).resizeEvent(event)
# update the centralWidget contents margins, adding the titlebar height
# to the top margin found before
if (self.centralWidget() and
self.centralWidget().getContentsMargins()[1] + self.__topMargin != self.titleHeight):
l, t, r, b = self.centralWidget().getContentsMargins()
self.centralWidget().setContentsMargins(
l, self.titleHeight + self.__topMargin, r, b)
# resize the width of the titlebar option, and move its buttons
self.titleOpt.rect.setWidth(self.width())
for ctrl, btn in self.ctrlButtons.items():
rect = self.style().subControlRect(
QtWidgets.QStyle.CC_TitleBar, self.titleOpt, ctrl, self)
if rect:
btn.setGeometry(rect)
sysRect = self.style().subControlRect(QtWidgets.QStyle.CC_TitleBar,
self.titleOpt, QtWidgets.QStyle.SC_TitleBarSysMenu, self)
if sysRect:
self.systemButton.setGeometry(sysRect)
self.titleOpt.titleBarState = int(self.windowState())
if self.isActiveWindow():
self.titleOpt.titleBarState |= int(QtWidgets.QStyle.State_Active)
self.updateTitleBar()
def paintEvent(self, event):
qp = QtGui.QPainter(self)
self.style().drawComplexControl(QtWidgets.QStyle.CC_TitleBar, self.titleOpt, qp, self)
titleRect = self.style().subControlRect(QtWidgets.QStyle.CC_TitleBar,
self.titleOpt, QtWidgets.QStyle.SC_TitleBarLabel, self)
icon = self.windowIcon()
if not icon.isNull():
iconRect = QtCore.QRect(0, 0, titleRect.left(), self.titleHeight)
qp.drawPixmap(iconRect, icon.pixmap(iconRect.size()))
title = self.actualWindowTitle()
titleRect.setLeft(self.menuBar().geometry().right())
if title:
# move left of the rectangle available for the title to the right of
# the menubar; if the title is bigger than the available space, elide it
elided = self.fontMetrics().elidedText(
title, QtCore.Qt.ElideRight, titleRect.width() - 2)
qp.drawText(titleRect, QtCore.Qt.AlignCenter, elided)
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
w = SpecialTitleWindow()
w.setWindowTitle('My window')
w.show()
sys.exit(app.exec_())
为了@musicamante 提供的代码可以在 PySide6 上运行,我做了一些补丁,但是我发现移动 window 时,它在晃动。当我查看 mouseMoveEvent 并阅读 QMouseEvent
中关于 event.pos()
的 qt 文档时,它说:
If you move the widget as a result of the mouse event, use the global position returned by
globalPos()
to avoid a shaking motion.
所以我替换了
self.move(self.pos() + event.pos() - self.__titleBarMousePos)
和
self.move(event.globalPos() - self.__titleBarMousePos)
在mouseMoveEvent()
这里是 PySide6 版本:
import sys
if sys.platform == 'win32':
import win32gui
import win32con
from PySide6 import QtCore, QtGui, QtWidgets
# a "fake" button class that we need for hover and click events
class HiddenButton(QtWidgets.QPushButton):
hover = QtCore.Signal()
def __init__(self, parent):
super(HiddenButton, self).__init__(parent)
# prevent any painting to keep this button "invisible" while
# still reacting to its events
self.setUpdatesEnabled(False)
self.setFocusPolicy(QtCore.Qt.NoFocus)
def enterEvent(self, event):
self.hover.emit()
def leaveEvent(self, event):
self.hover.emit()
class SpecialTitleWindow(QtWidgets.QMainWindow):
__watchedActions = (
QtCore.QEvent.ActionAdded,
QtCore.QEvent.ActionChanged,
QtCore.QEvent.ActionRemoved
)
titleOpt = None
__menuBar = None
__titleBarMousePos = None
__sysMenuLock = False
__topMargin = 0
def __init__(self):
super(SpecialTitleWindow, self).__init__()
self.widgetHelpers = []
# uic.loadUi('titlebar.ui', self)
# enable the system menu
self.setWindowFlags(
QtCore.Qt.FramelessWindowHint | QtCore.Qt.WindowSystemMenuHint
)
# set the WindowActive to ensure that the title bar is painted as active
self.setWindowState(self.windowState() | QtCore.Qt.WindowActive)
# create a StyleOption that we need for painting and computing sizes
self.titleOpt = QtWidgets.QStyleOptionTitleBar()
self.titleOpt.initFrom(self)
self.titleOpt.titleBarFlags = (
QtCore.Qt.Window | QtCore.Qt.MSWindowsOwnDC |
QtCore.Qt.CustomizeWindowHint | QtCore.Qt.WindowTitleHint |
QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.WindowMinMaxButtonsHint |
QtCore.Qt.WindowCloseButtonHint
)
self.titleOpt.state |= (QtWidgets.QStyle.State_Active |
QtWidgets.QStyle.State_HasFocus)
self.titleOpt.titleBarState = (int(self.windowState()) |
int(QtWidgets.QStyle.State_Active))
# create "fake" buttons
self.systemButton = HiddenButton(self)
self.systemButton.pressed.connect(self.showSystemMenu)
self.minimizeButton = HiddenButton(self)
self.minimizeButton.hover.connect(self.checkHoverStates)
self.minimizeButton.clicked.connect(self.minimize)
self.maximizeButton = HiddenButton(self)
self.maximizeButton.hover.connect(self.checkHoverStates)
self.maximizeButton.clicked.connect(self.maximize)
self.closeButton = HiddenButton(self)
self.closeButton.hover.connect(self.checkHoverStates)
self.closeButton.clicked.connect(self.close)
self.ctrlButtons = {
QtWidgets.QStyle.SC_TitleBarMinButton: self.minimizeButton,
QtWidgets.QStyle.SC_TitleBarMaxButton: self.maximizeButton,
QtWidgets.QStyle.SC_TitleBarNormalButton: self.maximizeButton,
QtWidgets.QStyle.SC_TitleBarCloseButton: self.closeButton,
}
self.widgetHelpers.extend([self.minimizeButton, self.maximizeButton, self.closeButton])
self.resetTitleHeight()
# *** END OF SETUP ***
fileMenu = self.menuBar().addMenu('File')
fileMenu.addAction('Open')
fileMenu.addAction('Save')
workMenu = self.menuBar().addMenu('Work')
workMenu.addAction('Work something')
analysisMenu = self.menuBar().addMenu('Analysis')
analysisMenu.addAction('Analize action')
# just call the statusBar to create one, we use it for resizing purposes
self.statusBar()
self.resize(400, 250)
def resetTitleHeight(self):
# minimum height for the menu can change everytime an action is added,
# removed or modified; let's update it accordingly
if not self.titleOpt:
return
# set the minimum height of the titlebar
self.titleHeight = max(
self.style().pixelMetric(
QtWidgets.QStyle.PM_TitleBarHeight, self.titleOpt, self),
self.menuBar().sizeHint().height()
)
self.titleOpt.rect.setHeight(self.titleHeight)
self.menuBar().setMaximumHeight(self.titleHeight)
if self.minimumHeight() < self.titleHeight:
self.setMinimumHeight(self.titleHeight)
def checkHoverStates(self):
if not self.titleOpt:
return
# update the window buttons when hovering
pos = self.mapFromGlobal(QtGui.QCursor.pos())
for ctrl, btn in self.ctrlButtons.items():
rect = self.style().subControlRect(QtWidgets.QStyle.CC_TitleBar,
self.titleOpt, ctrl, self)
# since the maximize button can become a "restore", ensure that it
# actually exists according to the current state, if the rect
# has an actual size
if rect and rect.contains(pos):
self.titleOpt.activeSubControls = ctrl
self.titleOpt.state |= QtWidgets.QStyle.State_MouseOver
break
else:
# no hover
self.titleOpt.state &= ~QtWidgets.QStyle.State_MouseOver
self.titleOpt.activeSubControls = QtWidgets.QStyle.SC_None
self.titleOpt.state |= QtWidgets.QStyle.State_Active
self.update()
def showSystemMenu(self, pos=None):
# show the system menu on windows
if sys.platform != 'win32':
return
if self.__sysMenuLock:
self.__sysMenuLock = False
return
winId = int(self.effectiveWinId())
sysMenu = win32gui.GetSystemMenu(winId, False)
if pos is None:
pos = self.systemButton.mapToGlobal(self.systemButton.rect().bottomLeft())
self.__sysMenuLock = True
cmd = win32gui.TrackPopupMenu(sysMenu,
win32gui.TPM_LEFTALIGN | win32gui.TPM_TOPALIGN | win32gui.TPM_RETURNCMD,
pos.x(), pos.y(), 0, winId, None)
win32gui.PostMessage(winId, win32con.WM_SYSCOMMAND, cmd, 0)
# restore the menu lock to hide it when clicking the system menu icon
QtCore.QTimer.singleShot(0, lambda: setattr(self, '__sysMenuLock', False))
def actualWindowTitle(self):
# window title can show "*" for modified windows
title = self.windowTitle()
if title:
title = title.replace('[*]', '*' if self.isWindowModified() else '')
return title
def updateTitleBar(self):
# compute again sizes when resizing or changing window title
menuWidth = self.menuBar().sizeHint().width()
availableRect = self.style().subControlRect(QtWidgets.QStyle.CC_TitleBar,
self.titleOpt, QtWidgets.QStyle.SC_TitleBarLabel, self)
left = availableRect.left()
if self.menuBar().sizeHint().height() < self.titleHeight:
top = (self.titleHeight - self.menuBar().sizeHint().height()) // 2
height = self.menuBar().sizeHint().height()
else:
top = 0
height = self.titleHeight
title = self.actualWindowTitle()
titleWidth = self.fontMetrics().boundingRect(title).width()
if not title and menuWidth > availableRect.width():
# resize the menubar to its maximum, but without hiding the buttons
width = availableRect.width()
elif menuWidth + titleWidth > availableRect.width():
# if the menubar and title require more than the available space,
# divide it equally, giving precedence to the window title space,
# since it is also necessary for window movement
width = availableRect.width() // 2
if menuWidth > titleWidth:
width = max(left, min(availableRect.width() - titleWidth, width))
# keep a minimum size for the menu arrow
if availableRect.width() - width < left:
width = left
extButton = self.menuBar().findChild(QtWidgets.QToolButton, 'qt_menubar_ext_button')
if self.isVisible() and extButton:
# if the "extButton" is visible (meaning that some item
# is hidden due to the menubar cannot be completely shown)
# resize to the last visible item + extButton, so that
# there's as much space available for the title
minWidth = extButton.width()
menuBar = self.menuBar()
spacing = self.style().pixelMetric(QtWidgets.QStyle.PM_MenuBarItemSpacing)
for i, action in enumerate(menuBar.actions()):
actionWidth = menuBar.actionGeometry(action).width()
if minWidth + actionWidth > width:
width = minWidth
break
minWidth += actionWidth + spacing
else:
width = menuWidth
self.menuBar().setGeometry(left, top, width, height)
# ensure that our internal widget are always on top
for w in self.widgetHelpers:
w.raise_()
self.update()
# helper function to avoid "ugly" colors on menubar items
def __setMenuBar(self, menuBar):
if self.__menuBar:
if self.__menuBar in self.widgetHelpers:
self.widgetHelpers.remove(self.__menuBar)
self.__menuBar.removeEventFilter(self)
self.__menuBar = menuBar
self.widgetHelpers.append(menuBar)
self.__menuBar.installEventFilter(self)
self.__menuBar.setNativeMenuBar(False)
self.__menuBar.setStyleSheet('''
QMenuBar {
background-color: transparent;
}
QMenuBar::item {
background-color: transparent;
}
QMenuBar::item:selected {
background-color: palette(button);
}
''')
def setMenuBar(self, menuBar):
self.__setMenuBar(menuBar)
def menuBar(self):
# QMainWindow.menuBar() returns a new blank menu bar if none exists
if not self.__menuBar:
self.__setMenuBar(QtWidgets.QMenuBar(self))
return self.__menuBar
def setCentralWidget(self, widget):
if self.centralWidget():
self.centralWidget().removeEventFilter(self)
# store the top content margin, we need it later
l, self.__topMargin, r, b = widget.contentsMargins()
super(SpecialTitleWindow, self).setCentralWidget(widget)
# since the central widget always uses all the available space and can
# capture mouse events, install an event filter to catch them and
# allow us to grab them
widget.installEventFilter(self)
def eventFilter(self, source, event):
if source == self.centralWidget():
# do not propagate mouse press events to the centralWidget!
if (event.type() == QtCore.QEvent.MouseButtonPress and
event.button() == QtCore.Qt.LeftButton and
event.y() <= self.titleHeight):
self.__titleBarMousePos = event.pos()
event.accept()
return True
elif source == self.__menuBar and event.type() in self.__watchedActions:
self.resetTitleHeight()
return super(SpecialTitleWindow, self).eventFilter(source, event)
def minimize(self):
self.setWindowState(QtCore.Qt.WindowMinimized)
def maximize(self):
if self.windowState() & QtCore.Qt.WindowMaximized:
self.setWindowState(
self.windowState() & (~QtCore.Qt.WindowMaximized | QtCore.Qt.WindowActive))
else:
self.setWindowState(
self.windowState() | QtCore.Qt.WindowMaximized | QtCore.Qt.WindowActive)
# whenever a window is resized, its button states have to be checked again
self.checkHoverStates()
def contextMenuEvent(self, event):
if not self.menuBar().geometry().contains(event.pos()):
self.showSystemMenu(event.globalPos())
def mousePressEvent(self, event):
if not self.centralWidget() and (event.type() == QtCore.QEvent.MouseButtonPress and
event.button() == QtCore.Qt.LeftButton and event.position().y() <= self.titleHeight):
self.__titleBarMousePos = event.position()
def mouseMoveEvent(self, event):
super(SpecialTitleWindow, self).mouseMoveEvent(event)
if event.buttons() == QtCore.Qt.LeftButton and self.__titleBarMousePos:
# move the window
self.move((event.globalPosition() - self.__titleBarMousePos).toPoint())
def mouseDoubleClickEvent(self, event):
if event.button() == QtCore.Qt.LeftButton:
self.maximize()
def mouseReleaseEvent(self, event):
super(SpecialTitleWindow, self).mouseReleaseEvent(event)
self.__titleBarMousePos = None
def changeEvent(self, event):
# change the appearance of the titlebar according to the window state
if event.type() == QtCore.QEvent.ActivationChange:
if self.isActiveWindow():
self.titleOpt.titleBarState = (
int(self.windowState()) | int(QtWidgets.QStyle.State_Active))
self.titleOpt.palette.setCurrentColorGroup(QtGui.QPalette.Active)
else:
self.titleOpt.titleBarState = 0
self.titleOpt.palette.setCurrentColorGroup(QtGui.QPalette.Inactive)
self.update()
elif event.type() == QtCore.QEvent.WindowStateChange:
self.checkHoverStates()
elif event.type() == QtCore.QEvent.WindowTitleChange:
if self.titleOpt:
self.updateTitleBar()
def showEvent(self, event):
if not event.spontaneous():
# update the titlebar as soon as it's shown, to ensure that
# most of the title text is visible
self.updateTitleBar()
def resizeEvent(self, event):
super(SpecialTitleWindow, self).resizeEvent(event)
# update the centralWidget contents margins, adding the titlebar height
# to the top margin found before
if (self.centralWidget() and
self.centralWidget().getContentsMargins()[1] + self.__topMargin != self.titleHeight):
l, t, r, b = self.centralWidget().getContentsMargins()
self.centralWidget().setContentsMargins(
l, self.titleHeight + self.__topMargin, r, b)
# resize the width of the titlebar option, and move its buttons
self.titleOpt.rect.setWidth(self.width())
for ctrl, btn in self.ctrlButtons.items():
rect = self.style().subControlRect(
QtWidgets.QStyle.CC_TitleBar, self.titleOpt, ctrl, self)
if rect:
btn.setGeometry(rect)
sysRect = self.style().subControlRect(QtWidgets.QStyle.CC_TitleBar,
self.titleOpt, QtWidgets.QStyle.SC_TitleBarSysMenu, self)
if sysRect:
self.systemButton.setGeometry(sysRect)
self.titleOpt.titleBarState = int(self.windowState())
if self.isActiveWindow():
self.titleOpt.titleBarState |= int(QtWidgets.QStyle.State_Active)
self.updateTitleBar()
def paintEvent(self, event):
qp = QtGui.QPainter(self)
self.style().drawComplexControl(QtWidgets.QStyle.CC_TitleBar, self.titleOpt, qp, self)
titleRect = self.style().subControlRect(QtWidgets.QStyle.CC_TitleBar,
self.titleOpt, QtWidgets.QStyle.SC_TitleBarLabel, self)
icon = self.windowIcon()
if not icon.isNull():
iconRect = QtCore.QRect(0, 0, titleRect.left(), self.titleHeight)
qp.drawPixmap(iconRect, icon.pixmap(iconRect.size()))
title = self.actualWindowTitle()
titleRect.setLeft(self.menuBar().geometry().right())
if title:
# move left of the rectangle available for the title to the right of
# the menubar; if the title is bigger than the available space, elide it
elided = self.fontMetrics().elidedText(
title, QtCore.Qt.ElideRight, titleRect.width() - 2)
qp.drawText(titleRect, QtCore.Qt.AlignCenter, elided)
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
w = SpecialTitleWindow()
w.setWindowTitle('My window')
w.show()
sys.exit(app.exec())