PySideでQDockWidgetの不便なところを解消する


QDockWidgetはレイアウト調整をユーザーに委ねることができる、非常に素晴らしいウィジェットである。
しかし、「こんな事もできないのか?」と思う所が多々あるのも事実である。
(Qtは全体的に標準の状態はかゆい所に手が届かない事が多い気がする)

QDockWidgetで不便な所を上げていこう。

  • タイトルバーにアイコンが表示されない
  • タブ化した際にタブにアイコンンが表示されない
  • DockWidget内にツールバーが配置できない
  • Float状態でのタブ化ができない(これはQt5.5で解決ずみ)

最後のFloat状態でのタブ化はQt5.5で対応しているので、ここでは対応しない。
Qt5対応のPySide2の完成を待とう。

それでは、上から順に解決していこう。

タイトルバーにアイコンを表示する(断念)

いきなりの断念である。

DockWidgetが増えてくると、どのDockがどの機能なのか視認性が下がってくる。
タイトルバーにQDockWidget::setWindowIconで設定したアイコンを表示されれば便利だろう。

しかし、それはどうも難しいようだ。

PySideでDrop Indicatorを独自描画する(黒魔術でQProxyStyleを実現する)

上の記事で使ったProxyStyleで、タイトルバーのdrowControlをフックする。

def drawControl(self, element, option, painter, widget):
    if element == QStyle.CE_DockWidgetTitle:
        width = self.pixelMetric(QStyle.PM_ToolBarIconSize)
        margin = self.pixelMetric(QStyle.PM_DockWidgetTitleMargin)

        painter.drawPixmap(margin, margin, widget.windowIcon().pixmap(QSize(width / 2, width / 2)))
        option.rect.adjust(width, 0, 0, 0)
    return self._style.drawControl(element, option, painter, widget)

自力でアイコン描画を行い、アイコンぶん表示範囲をずらして、オリジナルの描画に渡している。
しかし、結果は以下の通りアイコン部分の背景が塗りつぶしされていない。

この状態を回避すべく、Qtのソースを読んだがガチガチにハードコーディングされている。
やるのであれば、タイトルバーを全て独自に描画するしかないのである。
どちらにせよ、苦労して実装したところでFloat状態のタイトルバーは別実装なので、そちらも再実装する必要がある。

苦労のわりに報われる事があまりにも少ないため断念した。

タブ化した際にタブにアイコンンが表示されない

タイトルバーのアイコン表示は、ドックの中身にアイコンを表示してしまえばよい。
しかし、タブ化されたタブにアイコンが表示されないのはいただけない。
ドッグの中身が見えないためタブの文字でしか認識ができない、視認性に大きな問題がある。

QDockWidgetはタブ化されるとQMainWindow内のQTabBarにタブが格納される。
しかし、そのタブとQDockWidgetは全く切り離された別物なのだ。

そこで少々あらびきな方法で解決しよう、以下のコードを見てほしい。

#! usr/bin/python
# -*- coding: utf-8 -*-
from __future__ import unicode_literals, print_function, absolute_import
import sys
from PySide.QtCore import *
from PySide.QtGui import *


def main():
    app = QApplication(sys.argv)
    main_window = QMainWindow()

    warning_icon = QApplication.style().standardIcon(QStyle.SP_MessageBoxWarning)
    critical_icon = QApplication.style().standardIcon(QStyle.SP_MessageBoxCritical)

    dock_1 = QDockWidget("warning", main_window)
    dock_1.setWindowIcon(warning_icon)

    dock_2 = QDockWidget("critical", main_window)
    dock_2.setWindowIcon(critical_icon)

    main_window.addDockWidget(Qt.LeftDockWidgetArea, dock_1)
    main_window.addDockWidget(Qt.RightDockWidgetArea, dock_2)

    def onLocationChanged(area):
        icon_dict = {}
        for _dock in main_window.findChildren(QDockWidget):
            icon_dict[_dock.windowTitle()] = _dock.windowIcon()

        for tab_bar in main_window.findChildren(QTabBar):
            for idx in range(tab_bar.count()):
                if tab_bar.tabText(idx) in icon_dict:
                    tab_bar.setTabIcon(idx, icon_dict[tab_bar.tabText(idx)])

    for dock in main_window.findChildren(QDockWidget):
        dock.dockLocationChanged.connect(onLocationChanged)
    onLocationChanged(0)

    main_window.show()
    app.exec_()


if __name__ == "__main__":
    main()

実行結果は以下の通り、タブ化されたDockWidgetにアイコンが表示されている。

やっている事は、QDockWidgetのレイアウトが変更されるたびにメインウインドウ配下のQTabBarのタブを調べ上げ、タイトル名が一致しているタブにアイコンを設定している。

強引な方法ではあるが、Qtのソースを読むかぎり他に解決方法がないように思える。
QTabBar::tabDataにレイアウトのポインタが格納されているが、ユーザープログラム側でポインタからQDockWidgetを割り出すことはできない。
ユーザープログラム側には、タブ名とQDockWidgetのタイトル名しか結びつける要素がないのである。

QDockWidget内にツールバーが配置できない

メインウインドウのツールバーをQDockWidget内に配置することはできないが、QDockWidgetごとに独立したツールバーを持つことはできる。

#! usr/bin/python
# -*- coding: utf-8 -*-
from __future__ import unicode_literals, print_function, absolute_import
import sys
from PySide.QtCore import *
from PySide.QtGui import *


def main():
    app = QApplication(sys.argv)
    main_window = QMainWindow()

    home_icon = QApplication.style().standardIcon(QStyle.SP_DirHomeIcon)
    drive_icon = QApplication.style().standardIcon(QStyle.SP_DriveDVDIcon)
    home_action = QAction(home_icon, "Home", main_window)
    drive_action = QAction(drive_icon, "Drive", main_window)

    dock_1 = QDockWidget("warning", main_window)
    dock_2 = QDockWidget("critical", main_window)
    main_window.addDockWidget(Qt.LeftDockWidgetArea, dock_1)
    main_window.addDockWidget(Qt.RightDockWidgetArea, dock_2)

    def create_dock_toolbar(dock):
        __main_window = QMainWindow()
        __main_window.setParent(dock)
        dock.setWidget(__main_window)
        return __main_window

    _main_window = create_dock_toolbar(dock_1)
    bar = QToolBar(_main_window)
    bar.addActions([home_action, drive_action])
    _main_window.addToolBar(bar)

    _main_window = create_dock_toolbar(dock_2)
    bar = QToolBar(_main_window)
    bar.addActions([home_action, drive_action])
    _main_window.addToolBar(bar)

    main_window.show()
    app.exec_()


if __name__ == "__main__":
    main()

2つのQDockWidgetがおのおのツールバーを持っていることが確認できる。

ツールバーはQMainWindowしか配置する事ができない。
なので、QDockWidgetごとにQMainWindowを子供として持ち、そこにツールバーを配置している。

まとめ

Qtは素晴らしいGUIツールではあるが、いかんせん素の状態が不便である。

描画はQt完全に自前で行っているので、何でも自由に実装は可能である。
しかし、少し拡張するだけでユーザー側が全実装しなければならないシチュエーションが多すぎる。
(これはQtの設計が甘すぎる、というより拡張性皆無のハードコーディングが多い)

「お前の知識不足なだけで、もっとスマートな解決方法があるよ」という方は、是非ご教授をお願いします。