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 提供的答案很好,但我想添加我自己的建议。
它有点复杂,但他的解决方案缺少一些功能,我认为这些功能很重要。

  1. 它实际上使用了一个 QMainWindow;
  2. 它可以与在 Designer 中创建的 ui 文件一起使用。我只尝试使用 uic.loadUi,但我认为它也适用于使用 pyuic 创建的文件;
  3. 由于可以使用ui个文件,可以直接在设计器中创建菜单;
  4. 作为 QMainWindow,statusBar 是 actual QStatusBar;
  5. 支持当前系统主题风格和颜色;
  6. 在Windows下支持window系统菜单;
  7. 没有大小限制,因为菜单和标题会根据可用的 space
  8. 自行调整大小

也就是说,它并不完美。到目前为止,我发现的唯一缺点是它不支持顶部区域的工具栏和停靠小部件;我认为可以做到,但会有点复杂。

诀窍是创建一个 不是 原生的 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())