QMainWindow用のカスタムウィンドウタイトルバーを作ってみる

2023/09/09 categories:Python| tags:Python|PyQt6|QToolBar|

PySide6でQMainWindowのウィンドウタイトルバーにメニューなどのボタンを表示するUIを作ってみました。

動作の様子

QToolBarをウィンドウタイトルバーと同じような見た目で同じような機能を持たせることで以下のように動作させることができました。ただし、ウィンドウのフレームを無くしているのでスナップ機能が使えなくなっていたり、最小化や最大化のアニメーションが無くなっていたりします。必要であれば自分で実装することになります。

ウィンドウのフレームを非表示にする

setWindowFlagにQt.WindowType.FramelessWindowHintを渡すことでウィンドウのフレームが非表示になります。

class MainWindow(QtWidgets.QMainWindow):
    def __init__(self, parent: QtWidgets.QWidget = None):
        super().__init__(parent)
        self.setWindowFlag(QtCore.Qt.WindowType.FramelessWindowHint)

QToolBarをウィンドウタイトルバーっぽくする

QToolBarを移動、フロートを出来ない設定にして、スタイルシートでボーダーを非表示にして、背景色を変えるとウィンドウタイトルバーのような見た目になります。そこに、QToolButtonをメニューのボタンや閉じるボタンなどとして追加しています。閉じるボタンなどは右寄せするために、メニューとの間にスペーサー代わりのQLabelを追加しています。

class WindowTitleBar(QtWidgets.QToolBar):

    close_clicked = QtCore.Signal()

    def __init__(self, parent: QtWidgets.QWidget = None) -> None:
        super().__init__(parent)
        
        self.close_button = QtWidgets.QToolButton()
        self.close_button.setSizePolicy(QtWidgets.QSizePolicy.Policy.Maximum, QtWidgets.QSizePolicy.Policy.MinimumExpanding)
        self.close_button.setStyleSheet('''
            QToolButton { border: 0px solid; }
            QToolButton:hover { background-color: #ff0000; }
            QToolButton:pressed { background-color: #ee0000; }
        ''')
        self.close_button.setDefaultAction( QtGui.QAction(self.style().standardPixmap(QtWidgets.QStyle.StandardPixmap.SP_TitleBarCloseButton), 'Close') )
        self.close_button.triggered.connect(lambda : self.close_clicked.emit())
        
        self.title_label = QtWidgets.QLabel()
        self.title_label.setSizePolicy(QtWidgets.QSizePolicy.Policy.MinimumExpanding, QtWidgets.QSizePolicy.Policy.Maximum)
        
        self.addWidget(self.close_button)

        self.setMovable(False)
        self.setFloatable(False)
        self.setContextMenuPolicy(QtGui.Qt.ContextMenuPolicy.PreventContextMenu)
        self.setStyleSheet('QToolBar { border: none; background-color: #dddddd; }')

QToolBarをドラッグするとウィンドウを移動できるようにする

ウィンドウタイトルバーのような見た目になってもQToolBarであるのに変わりはないので、そこをドラッグしてもウィンドウは移動しません。そこで、MainWindowのmousePressEventでウィンドウの外枠から5ピクセル以上離れている場合にself.windowHandle().startSystemMove()を実行することで、QToolBarをドラッグするとウィンドウを移動できるようになりました。

class MainWindow(QtWidgets.QMainWindow):
    def __init__(self, parent: QtWidgets.QWidget = None):
        super().__init__(parent)
        self.window_title_bar.clicked.connect(self.move_window)

    def move_window(self) -> None:
        self.windowHandle().startSystemMove()

class WindowTitleBar(QtWidgets.QToolBar):
    def mousePressEvent(self, event: QtGui.QMouseEvent) -> None:
        p = event.position()
        is_top, is_bottom, is_left, is_right = p.y() < 5, p.y() > self.height() - 5, p.x() < 5, p.x() > self.width() - 5
        if not is_top and not is_left and not is_right:
            self.clicked.emit()
        return super().mousePressEvent(event)

ウィンドウの枠付近をドラッグするとウィンドウサイズを変えられるようにする

QMainWindowのmousePressEventでウィンドウの枠から5ピクセル以内にマウスがホバーしているときに、self.windowHandle().startSystemResize(edges)を実行することでウィンドウの枠付近をドラッグするとウィンドウサイズを変えられるようにできました。

class MainWindow(QtWidgets.QMainWindow):
    def mousePressEvent(self, event: QtGui.QMouseEvent) -> None:
        if event.button() == QtGui.Qt.MouseButton.LeftButton:
            p = event.position()
            is_top, is_bottom, is_left, is_right = p.y() < 5, p.y() > self.height() - 5, p.x() < 5, p.x() > self.width() - 5
            edges_list = []
            if is_top:
                edges_list.append(QtGui.Qt.Edge.TopEdge)
            if is_bottom:
                edges_list.append(QtGui.Qt.Edge.BottomEdge)
            if is_right:
                edges_list.append(QtGui.Qt.Edge.RightEdge)
            if is_left:
                edges_list.append(QtGui.Qt.Edge.LeftEdge)
            if edges_list:
                edges = edges_list[0]
                for edge in edges_list[1:]:
                    edges |= edge
                self.windowHandle().startSystemResize(edges)
        return super().mousePressEvent(event)

ソースコード

from PySide6 import QtWidgets, QtGui, QtCore


class MainWindow(QtWidgets.QMainWindow):
    def __init__(self, parent: QtWidgets.QWidget = None):
        super().__init__(parent)
        
        file_open_action = QtGui.QAction('Open')
        file_open_action.triggered.connect(lambda : self.label.setText('open clicked'))

        file_save_action = QtGui.QAction('Save')
        file_save_action.triggered.connect(lambda : self.label.setText('save clicked'))

        edit_copy_action = QtGui.QAction('Copy')
        edit_copy_action.triggered.connect(lambda : self.label.setText('copy clicked'))

        edit_paste_action = QtGui.QAction('Paste')
        edit_paste_action.triggered.connect(lambda : self.label.setText('paste clicked'))

        self.window_title_bar = WindowTitleBar(self)
        self.window_title_bar.add_menu('File', [file_open_action, file_save_action])
        self.window_title_bar.add_menu('Edit', [edit_copy_action, edit_paste_action])
        self.window_title_bar.clicked.connect(self.move_window)
        self.window_title_bar.double_clicked.connect(self.toggle_maximize)
        self.window_title_bar.close_clicked.connect(self.close)
        self.window_title_bar.minimize_clicked.connect(self.showMinimized)
        self.window_title_bar.maximize_clicked.connect(self.toggle_maximize)
        self.addToolBar(self.window_title_bar)

        self.label = QtWidgets.QLabel('hello')
        self.label.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)

        self.setCentralWidget(self.label)
        self.setWindowFlag(QtCore.Qt.WindowType.FramelessWindowHint)
        self.resize(300, 200)

    def move_window(self) -> None:
        self.windowHandle().startSystemMove()

    def toggle_maximize(self) -> None:
        if self.windowState() == QtGui.Qt.WindowState.WindowMaximized:
            self.setWindowState(QtGui.Qt.WindowState.WindowNoState)
        else:
            self.setWindowState(QtGui.Qt.WindowState.WindowMaximized)
        
    def mousePressEvent(self, event: QtGui.QMouseEvent) -> None:
        if event.button() == QtGui.Qt.MouseButton.LeftButton:
            p = event.position()
            is_top, is_bottom, is_left, is_right = p.y() < 5, p.y() > self.height() - 5, p.x() < 5, p.x() > self.width() - 5
            edges_list = []
            if is_top:
                edges_list.append(QtGui.Qt.Edge.TopEdge)
            if is_bottom:
                edges_list.append(QtGui.Qt.Edge.BottomEdge)
            if is_right:
                edges_list.append(QtGui.Qt.Edge.RightEdge)
            if is_left:
                edges_list.append(QtGui.Qt.Edge.LeftEdge)
            if edges_list:
                edges = edges_list[0]
                for edge in edges_list[1:]:
                    edges |= edge
                self.windowHandle().startSystemResize(edges)
        return super().mousePressEvent(event)
        
    def event(self, event: QtCore.QEvent) -> None:
        if event.type() == QtCore.QEvent.Type.HoverMove:
            p = event.position()
            is_top, is_bottom, is_left, is_right = p.y() < 5, p.y() > self.height() - 5, p.x() < 5, p.x() > self.width() - 5

            cursor_shape = QtCore.Qt.CursorShape.ArrowCursor

            if [is_top, is_bottom, is_left, is_right] == [True, False, False, False]:
                cursor_shape = QtCore.Qt.CursorShape.SizeVerCursor

            if [is_top, is_bottom, is_left, is_right] == [False, True, False, False]:
                cursor_shape = QtCore.Qt.CursorShape.SizeVerCursor
                
            if [is_top, is_bottom, is_left, is_right] == [False, False, True, False]:
                cursor_shape = QtCore.Qt.CursorShape.SizeHorCursor

            if [is_top, is_bottom, is_left, is_right] == [False, False, False, True]:
                cursor_shape = QtCore.Qt.CursorShape.SizeHorCursor
                
            if [is_top, is_bottom, is_left, is_right] == [True, False, True, False]:
                cursor_shape = QtCore.Qt.CursorShape.SizeFDiagCursor

            if [is_top, is_bottom, is_left, is_right] == [False, True, False, True]:
                cursor_shape = QtCore.Qt.CursorShape.SizeFDiagCursor

            if [is_top, is_bottom, is_left, is_right] == [True, False, False, True]:
                cursor_shape = QtCore.Qt.CursorShape.SizeBDiagCursor

            if [is_top, is_bottom, is_left, is_right] == [False, True, True, False]:
                cursor_shape = QtCore.Qt.CursorShape.SizeBDiagCursor

            self.setCursor(cursor_shape)

        return super().event(event)


class WindowTitleBar(QtWidgets.QToolBar):

    clicked = QtCore.Signal()
    close_clicked = QtCore.Signal()
    double_clicked = QtCore.Signal()
    icon_clicked = QtCore.Signal()
    minimize_clicked = QtCore.Signal()
    maximize_clicked = QtCore.Signal()

    def __init__(self, parent: QtWidgets.QWidget = None) -> None:
        super().__init__(parent)
        
        self.icon = QtWidgets.QToolButton()
        self.icon.setSizePolicy(QtWidgets.QSizePolicy.Policy.Maximum, QtWidgets.QSizePolicy.Policy.MinimumExpanding)
        self.icon.setStyleSheet('QToolButton { border: 0px solid; }')
        self.icon.setDefaultAction( QtGui.QAction(self.style().standardPixmap(QtWidgets.QStyle.StandardPixmap.SP_TitleBarMenuButton), '') )
        self.icon.triggered.connect(lambda : self.icon_clicked.emit())

        self.minimize_button = QtWidgets.QToolButton()
        self.minimize_button.setSizePolicy(QtWidgets.QSizePolicy.Policy.Maximum, QtWidgets.QSizePolicy.Policy.MinimumExpanding)
        self.minimize_button.setStyleSheet('''
            QToolButton { border: 0px solid; }
            QToolButton:hover { background-color: #eeeeee; }
            QToolButton:pressed { background-color: #eeeeee; }
        ''')
        self.minimize_button.setDefaultAction( QtGui.QAction(self.style().standardPixmap(QtWidgets.QStyle.StandardPixmap.SP_TitleBarMinButton), 'Minimize') )
        self.minimize_button.triggered.connect(lambda : self.minimize_clicked.emit())
        
        self.maximize_button = QtWidgets.QToolButton()
        self.maximize_button.setSizePolicy(QtWidgets.QSizePolicy.Policy.Maximum, QtWidgets.QSizePolicy.Policy.MinimumExpanding)
        self.maximize_button.setStyleSheet('''
            QToolButton { border: 0px solid; }
            QToolButton:hover { background-color: #eeeeee; }
            QToolButton:pressed { background-color: #eeeeee; }
        ''')
        self.maximize_button.setDefaultAction( QtGui.QAction(self.style().standardPixmap(QtWidgets.QStyle.StandardPixmap.SP_TitleBarMaxButton), 'Maximize') )
        self.maximize_button.triggered.connect(lambda : self.maximize_clicked.emit())
        
        self.close_button = QtWidgets.QToolButton()
        self.close_button.setSizePolicy(QtWidgets.QSizePolicy.Policy.Maximum, QtWidgets.QSizePolicy.Policy.MinimumExpanding)
        self.close_button.setStyleSheet('''
            QToolButton { border: 0px solid; }
            QToolButton:hover { background-color: #ff0000; }
            QToolButton:pressed { background-color: #ee0000; }
        ''')
        self.close_button.setDefaultAction( QtGui.QAction(self.style().standardPixmap(QtWidgets.QStyle.StandardPixmap.SP_TitleBarCloseButton), 'Close') )
        self.close_button.triggered.connect(lambda : self.close_clicked.emit())
        
        self.title_label = QtWidgets.QLabel()
        self.title_label.setSizePolicy(QtWidgets.QSizePolicy.Policy.MinimumExpanding, QtWidgets.QSizePolicy.Policy.Maximum)
        
        self.addWidget(self.icon)
        self.addWidget(self.title_label)
        self.addWidget(self.minimize_button)
        self.addWidget(self.maximize_button)
        self.addWidget(self.close_button)

        self.setMovable(False)
        self.setFloatable(False)
        self.setContextMenuPolicy(QtGui.Qt.ContextMenuPolicy.PreventContextMenu)
        self.setStyleSheet('QToolBar { border: none; background-color: #dddddd; }')

    def set_title(self, text: str) -> None:
        self.title_label.setText(text)

    def add_menu(self, text: str, actions: list[QtGui.QAction]) -> None:
        button = QtWidgets.QToolButton()
        button.setText(text)
        button.setSizePolicy(QtWidgets.QSizePolicy.Policy.Maximum, QtWidgets.QSizePolicy.Policy.MinimumExpanding)
        button.setStyleSheet('''
            QToolButton { border: 0px solid; }
            QToolButton:hover { background-color: #eeeeee; }
            QToolButton:pressed { background-color: #eeeeee; }
        ''')
        last_action = self.actions()[-4]
        self.insertWidget(last_action, button)

        button.setStyleSheet( button.styleSheet() + 'QToolButton::menu-indicator { image: none; }' )
        button.setMenu(QtWidgets.QMenu(button))
        button.setPopupMode(QtWidgets.QToolButton.ToolButtonPopupMode.InstantPopup)
        for action in actions:
            action.setParent(button)
            button.menu().addAction(action)

    def mouseDoubleClickEvent(self, event: QtGui.QMouseEvent) -> None:
        p = event.position()
        is_top, is_bottom, is_left, is_right = p.y() < 5, p.y() > self.height() - 5, p.x() < 5, p.x() > self.width() - 5
        if not is_top and not is_left and not is_right:
            self.double_clicked.emit()
        return super().mouseDoubleClickEvent(event)

    def mousePressEvent(self, event: QtGui.QMouseEvent) -> None:
        p = event.position()
        is_top, is_bottom, is_left, is_right = p.y() < 5, p.y() > self.height() - 5, p.x() < 5, p.x() > self.width() - 5
        if not is_top and not is_left and not is_right:
            self.clicked.emit()
        return super().mousePressEvent(event)


if __name__ == '__main__':
    import sys
    app = QtWidgets.QApplication(sys.argv)
    main_window = MainWindow()
    main_window.show()
    sys.exit( app.exec() )

Share post

Related Posts

コメント