LoginSignup
5
5

More than 5 years have passed since last update.

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

Last updated at Posted at 2016-12-28

PySideでウィジェットの背景色を変えていると、Drop Indicator(ドラッグ&ドロップ時にどこにドロップされるかの表示)が見にくい場合がある。
独自に描画してIndicatorを目立つようにしたい。

しかし、事は簡単ではなく、StyleSheetではDrop Indicatorの設定がない。

QtでDrop Indicatorを独自描画するにはQProxyStyleを使うのだが、PySideおよびPyQtではQProxyStyleが存在しないのである。

そこで、PySideをHackしてDrop Indicatorの描画を横取りする。
なお、この方法は正当な方法ではない黒魔術である、未来のPySideでも有効かは保証できない。

まずは正当な方法で描画を横取りしてみよう。

正当な方法:独自のスタイルを作成する

まずは正当な方法を考えよう。

自分でQStyleクラスを継承して、QStyle::drawPrimitiveメソッドをオーバーライドする方法である。
私の環境はWindowsなので、QWindowsStyleを継承する。

class MyWindowsStyle(QWindowsStyle):
    def drawPrimitive(self, element, option, painter, widget):
        if element == QStyle.PE_IndicatorItemViewItemDrop:
            pen = QPen(Qt.red)
            pen.setWidth(2)

            painter.setPen(pen)
            if not option.rect.isNull():
                painter.drawRect(option.rect)
            return

        super(MyWindowsStyle, self).drawPrimitive(element, option, painter, widget)

上の例ではDrop Indicatorを赤にし枠を太くしている。
このスタイルクラスをQApplication::setStyleで設定する。

my_style = MyWindowsStyle(parent_widget)

QApplication.setStyle(my_style)

2016-12-28_23h35_29.png

うまくいったようだが、見た目が全体的に古臭くなった。

Windows版のPySide(Qt)では、標準のスタイルはQWindowStyleではなくQWindowsVistaStyleが適用される。
しかし、QWindowsVistaStyleは動的に読み込まれるPluginスタイルであるため、クラス継承ができないのだ。
また特定のスタイルクラスを継承してしまうため、MacやLinuxでも同一のルック&フィールになってしまう。
これらのデメリットが気にならないのであれば、この方法をお勧めする。

それでは、黒魔術を使ってQWindowsVistaStyleの描画を横取りしてみよう。

QWindowsVistaStyleのインスタンスを生成

まずはQWindowsVistaStyleインスタンスの生成である。
スタイルの生成にはQStyleFactoryクラスを使う。

vista_style = QStyleFactory.create("windowsvista")

これでQWindowsVistaStyleインスタンスが生成された。

ちなみに各環境の標準のスタイルクラスとキー名(QStyleFactory.createに渡す文字列)を調べるのは以下のようにする。

QApplication.style().metaObject().className()
>>> QWindowsVistaStyle

QApplication.style().objectName()
>>> windowsvista

インスタンスのProxyクラスを作る

スタイルインスタンスを包括するProxyクラスを作成する。

class ProxyStyle(QCommonStyle):
    TranslateMethods = [
        'drawComplexControl', 'drawControl', 'drawItemText', 'generatedIconPixmap', 
        'hitTestComplexControl', 'pixelMetric', 'polish', 'sizeFromContents', 
        'sizeFromContents', 'standardPixmap', 'styleHint', 'styleHint',
        'subControlRect', 'subElementRect', 'unpolish'
    ]

    def __init__(self, style, parent=None):
        super(ProxyStyle, self).__init__(parent)
        self._style = style

        for method_name in self.TranslateMethods:
            setattr(self, method_name, getattr(self._style, method_name))

    def drawPrimitive(self, element, option, painter, widget):
        if element == QStyle.PE_IndicatorItemViewItemDrop:
            pen = QPen(Qt.red)
            pen.setWidth(2)

            painter.setPen(pen)
            if not option.rect.isNull():
                painter.drawRect(option.rect)
            return

        self._style.drawPrimitive(element, option, painter, widget)

見てもらえれば分かるが、強引かつ泥臭い方法で委譲をしている。
ちなみにPySideオブジェクトは__getattr__による委譲トリックは使えない。
強引な黒魔術でQProxyStyleを実装している。

このクラスでスタイルインスタンスを動的にProxyして、QApplicationにスタイルとして定義する。

proxy_style = ProxyStyle(vista_style)

QApplication.setStyle(proxy_style)

2016-12-29_02h01_19.png

うまくいったようだ。
以下のようにStyleSheetを併用して背景を灰色にしても問題なく動作している。

tree_view.setStyleSheet("QTreeView { background-color: gray }")

2016-12-29_02h07_40.png

まとめ

この方法は全くエレガントではない。

ViewのpaintEventをオーバーライドして、自前ですべて描画する方法もあるが、Viewの種類ごとに描画を変える必要があり、非常に面倒である。
できればスタイルの設定で乗り切りたいところである。

もっと良い方法があったら、是非ご教授願いたい。

5
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
5