Search code examples
qtpyqtpyqt5

Custom title bar with dockable toolbar


I want to create a custom title bar for my PyQt application, and my application uses dockable toolbars. To be dockable, the toolbars should be added to the MainWindow. However, with my custom toolbar being a Widget added to a frameless window, the toolbars dock themselves around the title bar. They can be docked above the title bar, and whenever docked on the sides, they push the title bar, which is not the expected behavior. I understand that this is due to the fact the the toolbar areas are always around the central widget of the window, and my custom title bar is inside the central widget. However, I don't see how I can make this work the way I want. Here is a MWE (I'm using PyQt=5.12.3) :

import sys
from typing import Optional

from PyQt5.QtCore import Qt, QSize, QPoint
from PyQt5.QtGui import QMouseEvent
from PyQt5.QtWidgets import QMainWindow, QApplication, QVBoxLayout, QPushButton, QWidget, QToolBar, QHBoxLayout, QLabel, \
    QToolButton


class CustomTitleBar(QWidget):
    def __init__(self, title: str, parent: Optional[QWidget] = None):
        super().__init__(parent=parent)
        self.window_parent = parent
        layout = QHBoxLayout()
        self.setObjectName("CustomTitleBar")
        self.setLayout(layout)
        layout.setContentsMargins(0, 0, 0, 0)
        layout.setSpacing(0)
        self.setFixedHeight(40)

        self.title = title
        self.title_label = QLabel(self.title)
        self.title_label.setObjectName("TitleBarLabel")
        layout.addWidget(self.title_label)
        layout.addStretch(1)

        but_minimize = QToolButton()
        but_minimize.setText("🗕")
        but_minimize.setObjectName("MinimizeButton")
        layout.addWidget(but_minimize)
        but_minimize.clicked.connect(self.window().showMinimized)

        self.but_resize = QToolButton()
        if self.window().isMaximized():
            self.but_resize.setText("🗗")
        else:
            self.but_resize.setText("🗖")
        layout.addWidget(self.but_resize)
        self.but_resize.clicked.connect(self.toggle_maximized)
        self.but_resize.setObjectName("ResizeButton")

        but_close = QToolButton()
        but_close.setText("🗙")
        layout.addWidget(but_close)
        but_close.clicked.connect(self.window().close)
        but_close.setObjectName("CloseButton")

        self.m_pCursor = QPoint(0, 0)
        self.moving = False

    def toggle_maximized(self):
        if self.window().isMaximized():
            self.but_resize.setText("🗖")
            self.window().showNormal()
        else:
            self.but_resize.setText("🗗")
            self.window().showMaximized()

    def mousePressEvent(self, event: QMouseEvent) -> None:
        pass

    def mouseDoubleClickEvent(self, event: QMouseEvent) -> None:
        pass

    def mouseMoveEvent(self, event: QMouseEvent) -> None:
        pass

    def mouseReleaseEvent(self, event: QMouseEvent) -> None:
        pass


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowSystemMenuHint)
        self.resize(QSize(800, 600))

        main_widget = QWidget(self)
        self.setCentralWidget(main_widget)

        layout = QVBoxLayout(self)
        main_widget.setLayout(layout)

        titlebar = CustomTitleBar("Custom TitleBar Test Window", self)
        layout.addWidget(titlebar)
        layout.addWidget(QPushButton("Hello world"))
        layout.addStretch(1)

        my_toolbar = QToolBar(self)

        self.addToolBar(Qt.RightToolBarArea, my_toolbar)
        my_toolbar.addWidget(QPushButton("A"))
        my_toolbar.addWidget(QPushButton("B"))
        my_toolbar.addWidget(QPushButton("C"))


if __name__ == '__main__':
    app = QApplication(sys.argv)

    w = MainWindow()

    w.show()
    app.exec_()

The resulting window is as follow: Window with custom title bar

How can I get the dockable toolbars to behave around my custom title bar the way they should behave around a standard title bar ?


Solution

  • Since the title bar should be put outside the standard contents, you cannot put it inside the central widget.

    The solution is to setContentsMargins() using the height of the title bar for the top margin. Then, since the title bar is not managed by any layout, you need to resize it by overriding the resizeEvent():

    class MainWindow(QMainWindow):
        def __init__(self):
            # ...
            self.titlebar = CustomTitleBar("Custom TitleBar Test Window", self)
            self.setContentsMargins(0, self.titlebar.sizeHint().height(), 0, 0)
    
        def resizeEvent(self, event):
            super().resizeEvent(event)
            self.titlebar.resize(self.width(), self.titlebar.sizeHint().height())