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

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

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

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

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

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

## 正当な方法：独自のスタイルを作成する
まずは正当な方法を考えよう。

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

```python
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で設定する。

```python
my_style = MyWindowsStyle(parent_widget)

QApplication.setStyle(my_style)
```
<img width="129" alt="2016-12-28_23h35_29.png" src="https://qiita-image-store.s3.amazonaws.com/0/140075/47203af9-7e36-9b97-21d8-ef8cf73ab908.png">

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

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

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


## QWindowsVistaStyleのインスタンスを生成

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

```python
vista_style = QStyleFactory.create("windowsvista")
```

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

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

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

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

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

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

```python
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にスタイルとして定義する。

```python
proxy_style = ProxyStyle(vista_style)

QApplication.setStyle(proxy_style)
```

<img width="129" alt="2016-12-29_02h01_19.png" src="https://qiita-image-store.s3.amazonaws.com/0/140075/c59c1fa4-6b7e-57ce-52ce-e3ce414b32b3.png">

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

```python
tree_view.setStyleSheet("QTreeView { background-color: gray }")
```
<img width="129" alt="2016-12-29_02h07_40.png" src="https://qiita-image-store.s3.amazonaws.com/0/140075/b7ceefa1-17f4-cdcb-fbe9-b25ef2c5ad7f.png">

## まとめ

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

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

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