メインコンテンツまでスキップ

PySideのTableViewでDelegateを使う(1)

PySide の View 関連で一番わかりにくいのが「Delegate」と呼ばれる部分になります。
ので今回は、TableView をベースにその使い方をまとめてみます。

サンプルに使用している UI ファイルは
https://snippets.cacher.io/snippet/9110e33ece017819b0fb
こちら。
全コードは長いので、
https://snippets.cacher.io/snippet/6d1bf656edbf26752bdc
全部まとめて見たい場合はこちらを参照してください。

TableModel について

まず、PySide で View を扱う場合は、View に表示するデータのアクセスをコントロールする「Model」
とセットで扱います。

Model については、 に書いてますが、
今回はその内容に+して TableModel 用に拡張した内容の補足。

    def headerData(self, col, orientation, role):

HEADER = ['名前', 'チェック', '進行状況']

if orientation == QtCore.Qt.Horizontal and role == QtCore.Qt.DisplayRole:
return HEADER[col]

if orientation == QtCore.Qt.Vertical and role == QtCore.Qt.DisplayRole:
return str(col + 1).zfill(3)

TableView の場合は、列・行それぞれに Header が表示できます。
ので、Header に何を表示するかは orientation を使用することで指定することができます。

flags

    def flags(self, index):

return QtCore.Qt.ItemIsEditable | QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled

flags では、そのセルごとの編集フラグを指定できます。
今回はセルごとに編集できるようにしたいので Editable,Selectable,Enabled を指定しておきます。
ここを指定しなければ、編集不可状態になってしまいます。

セルの順番を変える

    def upData(self, index):
"""
引数のIndexを1つ上に移動する
"""
row = index.row()
if row > 0:
buff = self.items.pop(row)
self.items.insert(row - 1, buff)
self.layoutChanged.emit()

def downData(self, index):
"""
引数のIndexを一つ下に移動する
"""
row = index.row()
if row < len(self.items):
buff = self.items.pop(row)
self.items.insert(row + 1, buff)
self.layoutChanged.emit()

今回はボタンで行を入れ替えられるようにしたかったので、関数を2つ追加。
Model の行は List で持っているので
配列を入れ替えれば、並び順を入れ替えられるようになります。

最後は layoutChanged で View を更新します。

Delegate について

ユーザーの操作を受け付ける View、View に表示するための Data をコントロースするのが Model
では Delegate はなにかというと、
現在の処理を行うセルごとの処理を個別に処理をする部分 を受け持ちます。

Cell ごとになにを行うかというと

  • セルの編集
  • セルの描画

の、大きく分けて 2 つになります。

セルの描画

Delegate がない場合は、Model 内の data で DisplayRole で return した値が描画されるだけでした。
しかし、Delegate を使用した場合はセルごとに QPainter を使用してよりカスタムした表示をおこなうことができます。

    def paint(self, painter, option, index):
"""
Cellの中の描画を行う
"""
if index.column() == 0:
painter.drawText(option.rect, QtCore.Qt.AlignCenter | QtCore.Qt.TextWordWrap, index.data())

if index.column() == 1:
btn = QtWidgets.QStyleOptionButton()
btn.state |= QtWidgets.QStyle.State_Enabled
if index.data() == True:
btn.state |= QtWidgets.QStyle.State_On
else:
btn.state |= QtWidgets.QStyle.State_Off

btn.rect = self.checkBoxRect(option)
btn.text = "hogehoge"
QtWidgets.QApplication.style().drawControl(QtWidgets.QStyle.CE_CheckBox, btn, painter)

if index.column() == 2:

bar = QtWidgets.QStyleOptionProgressBar()
bar.rect = option.rect
bar.rect.setHeight(option.rect.height() - 1)
bar.rect.setTop(option.rect.top() + 1)
bar.minimum = 0
bar.maximum = 100
bar.progress = int(index.data())
bar.textVisible = True
bar.text = str(index.data()) + '%'
bar.textAlignment = QtCore.Qt.AlignCenter
QtWidgets.QApplication.style().drawControl(QtWidgets.QStyle.CE_ProgressBar, bar, painter)

描画部分は paint 関数をオーバーライドすることで作成します。
TableView の場合は、列ごとに何を表示するかを変える必要があります。

各セルに「表示したい」データというのは、Model クラスの data(QtCore.Qt.DataRole) で指定した方法で受け取ります。

index.data()

この場合 QtCore.Qt.DisplayRole で指定した値になります。
値を取得できたら、実際の描画をします。

painter.drawText(option.rect, QtCore.Qt.AlignCenter | QtCore.Qt.TextWordWrap, index.data())

その値を文字列として表示したい場合は QPainter の drawText()で描画すれば OK です。

セルに対して Widgets を配置したい

drawRect などを使用して描画することもできますが、よくある Widgets はデフォルトで用意されていて
それをセルの中に配置することができます。
その表示用のウィジェットが「QStyleOption####」クラスです。

サンプルでは、チェックボックスとプログレスバーを配置しています。
それ以外にも ComboBox など、よく使うものが用意されています。

セルの編集

各セルをクリックしたときや、ダブルクリックをしたときには
そのセルの実体となっているオブジェクトの値を編集して、UI の表示をアップデートを行う必要があります。
Delegate は、そのあたりを細かくカスタマイズすることができます。

具体的には、4 つの Virtual 関数をオーバーライドすることで
望んだ動作を作成します。

クリックしたときに値を変更する

まずは、クリックしたときになにかする場合。
例えばセルに対してチェックボックスを表示させて、そのチェックをクリックしたら
中の値を変えるなど。

    def editorEvent(self, event, model, option, index):
"""
TableのCellに対してなにかしらのイベント(クリックした等)が発生したときに呼ばれる。
"""

if index.column() == 1:
if self.checkBoxRect(option).contains(event.pos().x(), event.pos().y()):
if event.type() == QtCore.QEvent.MouseButtonPress:
currentValue = model.items[index.row()].data(index.column())
model.items[index.row()].setData(index.column(), not currentValue)
model.layoutChanged.emit()
return True
return False

まず、処理は列ごとに処理を分けたいので index.column() ごとに判定を行います。
まず、チェックボックのセルの列だった場合、かつ指定の範囲内だった場合は
指定の「書き換え処理」を行えるようにします。
イベントは、今回は「マウスをクリックした」ですが、それ以外にもイベントを取得して
何かしらの処理を行う場合はこの editorEvent で処理を作成します。

特に何も指定しないと、どこでもクリックできてしまいます。
ので、「現在のセル内」でのみ判定できるように
セルの矩形を取得して、現在のマウス位置が矩形内かを判定します。

範囲内の場合は指定のモデルに値をセットして、表示をアップデートします。

ダブルクリックで Widgets を表示して編集する

次に、ダブルクリックしたときに指定の Widgets を配置させて
編集をする機能を付けたい場合。
その場合は、

  • createEditor
  • setEditorData
  • setModelData

この3つの関数を使用します。

createEditor

    def createEditor(self, parent, option, index):
"""
編集したいCellに対して、編集用のWidgetsを作成する
"""
if index.column() == 0:
return QtWidgets.QLineEdit(parent)

if index.column() == 2:
spin = QtWidgets.QSpinBox(parent)
spin.setMinimum(0)
spin.setMaximum(100)
return spin

まず、ダブルクリックしたときに表示する Widgets を、createEditor で返すようにします。
配置する Widgets は「parent」を指定することでそのセル位置に Widgets を配置できます。
(入れないとセル位置ではなく View 全体に配置されます)

setEditor

    def setEditorData(self, editor, index):
"""
createEditorで作成したWidgetsを受け取って、
Editorにデフォルト値を設定する。
今回の場合、元々のCellに表示されていた内容を受け取り、
QLineEditに対してデフォルト値をセットしている
"""

if index.column() == 0:
value = index.model().data(index, QtCore.Qt.DisplayRole)
editor.setText(value)

if index.column() == 2:
value = index.model().data(index, QtCore.Qt.DisplayRole)
editor.setValue(value)

createEditor で作成した Widgets に対して、「現在の値」をセットするのが setEditorData。
いわゆるここが「編集する一歩手前」状態になります。

setModelData

    def setModelData(self, editor, model, index):
"""
編集した値を、Modelを経由して実体のオブジェクトに対してセットする
"""

value = None

if index.column() == 0:
value = editor.text()

if index.column() == 2:
value = editor.value()

if value is not None:
model.items[index.row()].setData(index.column(), value)

最後に Widgets への編集状態が解除されたタイミングで、Widgets の値を受け取り、
Model に値をセットします。
この setModelData の editor は、createEditor で作成した Widgets のオブジェクトになります。

こんなかんじで...

Delegate を使用することで各セルごとの挙動を細かくカスタムできるようになりました。

複数のデータを編集したりするような UI を作る場合
FormLayout で Widgets を配置していくのも手ですが、 View と Model と Delegate を合わせてつかうことで
データ本体は Model で管理し、データの入出力や表示は列ごとに Delegate でコントロール。
そして表示は View で行えるようになります。