PySide Model/View【第4弾】QStyledItemDelegateでプログレスバー表示&条件付き色分け

\ 迷ったらまずTechAcademyの無料カウンセリング! /

QTableView や QTreeView を使ってデータを表示する方法は「ただの文字だけの表」だと、情報が多いときに 直感的に把握しにくい ことがあります。

そこで今回は QStyledItemDelegate を使い、セルの中に プログレスバーを描画したり、条件によってセルの背景色を変える方法 を紹介します。
これを使えば、タスク進捗やステータスをひと目で理解できる かっこいいダッシュボード風のUI を作れます。

サンプルコードはそのままコピーして動かせるようにしてあります。実務的で「ダッシュボード映え」する例も載せています。

目次

QStyledItemDelegateとは

QStyledItemDelegate は、QtのModel/Viewフレームワークで,QTableView / QListView / QTreeView 上の「セルの見た目」や「編集方法」をカスタマイズするためのクラスです。

  • 表示:paint() をオーバーライドして自由に描画できます(プログレスバーの埋め込み、色分け、アイコン合成など)。
  • 編集:createEditor() / setEditorData() / setModelData() で、セルをクリックしたときに表示される編集ウィジェット(例:QComboBox, QSpinBox)を制御できます。

これにより「ただの文字」が「進捗バー」「色付きラベル」「コンボで編集できるセル」などに変身し、ダッシュボードや管理画面の視認性を大きく向上させます。

基本コード

実用サンプルです。コピーして動かしてみてください
解説は全体解説詳細解説をご覧ください。

  • 1列目:タスク名(文字列)
  • 2列目:進捗(数値)→ ProgressDelegate でプログレスバー表示
  • 3列目:ステータス(文字列)→ StatusDelegate で背景色をつけ、QComboBox で編集可能
from PySide6.QtWidgets import (
    QApplication, QTableView, QStyledItemDelegate, QStyleOptionProgressBar, QStyle,
    QComboBox
)
from PySide6.QtGui import QStandardItemModel, QStandardItem, QColor
from PySide6.QtCore import Qt, QSize
import sys

class ProgressDelegate(QStyledItemDelegate):
    def paint(self, painter, option, index):
        # 数値をパーセントとして描画する(整数保存想定)
        try:
            value = int(index.data(Qt.DisplayRole))
        except Exception:
            super().paint(painter, option, index)
            return

        opt = QStyleOptionProgressBar()
        opt.rect = option.rect.adjusted(6, 8, -6, -8)
        opt.minimum = 0
        opt.maximum = 100
        opt.progress = max(0, min(100, value))
        opt.text = f"{opt.progress}%"
        opt.textVisible = True
        QApplication.style().drawControl(QStyle.CE_ProgressBar, opt, painter)

    def sizeHint(self, option, index):
        s = super().sizeHint(option, index)
        return QSize(s.width(), s.height() + 8)


class StatusDelegate(QStyledItemDelegate):
    def paint(self, painter, option, index):
        text = index.data(Qt.DisplayRole)
        # 背景色を条件で変える
        if isinstance(text, str):
            if "エラー" in text or "Error" in text:
                painter.save()
                painter.fillRect(option.rect, QColor(255, 200, 200))
                painter.restore()
            elif "完了" in text or "Done" in text:
                painter.save()
                painter.fillRect(option.rect, QColor(200, 255, 200))
                painter.restore()
        # 標準表示(テキスト描画)
        super().paint(painter, option, index)

    def createEditor(self, parent, option, index):
        cb = QComboBox(parent)
        cb.addItems(["未着手", "処理中", "完了", "エラー"])
        # 値確定と閉じる通知
        cb.activated.connect(lambda _idx, ed=cb: self.commitData.emit(ed))
        cb.activated.connect(lambda _idx, ed=cb: self.closeEditor.emit(ed))
        return cb

    def setEditorData(self, editor, index):
        val = index.data(Qt.DisplayRole)
        i = editor.findText(val)
        if i >= 0:
            editor.setCurrentIndex(i)

    def setModelData(self, editor, model, index):
        model.setData(index, editor.currentText(), Qt.EditRole)

    def updateEditorGeometry(self, editor, option, index):
        editor.setGeometry(option.rect.adjusted(2, 2, -2, -2))


def build_model():
    model = QStandardItemModel(5, 3)
    model.setHorizontalHeaderLabels(["Task", "Progress", "Status"])
    rows = [
        ("ダウンロード", 10, "処理中"),
        ("解析", 40, "処理中"),
        ("レポート作成", 70, "処理中"),
        ("バックアップ", 100, "完了"),
        ("外部接続", 20, "エラー"),
    ]
    for r, (task, prog, status) in enumerate(rows):
        model.setItem(r, 0, QStandardItem(task))
        # Progress を数値として保持(重要)
        model.setData(model.index(r, 1), prog, Qt.DisplayRole)
        model.setItem(r, 2, QStandardItem(status))
    return model

if __name__ == "__main__":
    app = QApplication(sys.argv)
    view = QTableView()
    model = build_model()
    view.setModel(model)
    view.setItemDelegateForColumn(1, ProgressDelegate(view))
    view.setItemDelegateForColumn(2, StatusDelegate(view))
    view.resize(640, 300)
    view.show()
    sys.exit(app.exec())
  • model.setData(model.index(r, 1), prog, Qt.DisplayRole) のように進捗は数値で保持すると Delegate 側で int() が使えて簡単です。
  • ステータス列は StatusDelegate で描画と編集(QComboBox)を同一デリゲートで実装しています。列ごとに別のデリゲートを割り当てられるため柔軟です。

主なプロパティとメソッド一覧(全体解説)

Noメソッド / プロパティ説明
1paint(self, painter, option, index)セルの描画処理を定義します。QPainter を使って自由に描画可能。
2sizeHint(self, option, index)セル(エディタ)の推奨サイズを返します。行高さや列幅の目安に。
3createEditor(self, parent, option, index)セル編集用ウィジェット(エディタ)を生成して返します(例:QComboBox)。
4setEditorData(self, editor, index)モデルの値をエディタにセットします(編集開始時)。
5setModelData(self, editor, model, index)エディタの値をモデルに保存します(編集終了時)。
6updateEditorGeometry(self, editor, option, index)エディタの位置とサイズを調整します(通常 editor.setGeometry(option.rect))。
7editorEvent(self, event, model, option, index)マウスやキーボードのイベントをセル単位で処理できます(チェックボックスの切替等)。
8displayText(self, value, locale)値を文字列に整形して表示する際に使います(フォーマット処理に便利)。
9helpEvent(self, event, view, option, index)ツールチップ等のヘルプ表示を制御できます。
10initStyleOption(self, option, index)描画オプション(QStyleOptionViewItem)を初期化する補助メソッド。既定のスタイル情報を取得可能。
11commitData (シグナル)編集ウィジェットからデータ確定を通知するために使います(self.commitData.emit(editor))。
12closeEditor (シグナル)編集ウィジェットを閉じるよう通知するときに使います(self.closeEditor.emit(editor))。

各プロパティ・メソッド 詳細解説

【1】セルの描画

概要:
セルの描画を行うメソッドです。QPainter を受け取り、矩形(option.rect)領域にテキストや図形、アイコン、プログレスバー等を描画します。既存の表示(標準表示)を使いたい場合は super().paint(...) を呼べます。

メソッド:
paint(self, painter, option, index)

  • 描画前に self.initStyleOption(option, index) を呼ぶと、フォントや選択状態などの既定情報が option に入り便利です。
  • 描画は painter.save() / painter.restore() で囲むと安全です(状態を戻せる)。

使い方:簡単なプログレスバー描画

from PySide6.QtWidgets import QStyledItemDelegate, QStyleOptionProgressBar, QApplication, QStyle
from PySide6.QtCore import Qt

class ProgressDelegate(QStyledItemDelegate):
    def paint(self, painter, option, index):
        # 表示値を取得(数値を想定)
        value = index.data(Qt.DisplayRole)
        try:
            progress = int(value)
        except Exception:
            return super().paint(painter, option, index)

        opt = QStyleOptionProgressBar()
        opt.rect = option.rect.adjusted(4, 8, -4, -8)  # 少し余白
        opt.minimum = 0
        opt.maximum = 100
        opt.progress = progress
        opt.text = f"{progress}%"
        opt.textVisible = True

        QApplication.style().drawControl(QStyle.CE_ProgressBar, opt, painter)

【2】セルやエディタの推奨サイズを返す

概要:
セルやエディタの推奨サイズを返します。行の高さやエディタのサイズ調整に使われます。通常は super().sizeHint(option, index) を使いつつ、必要なら高さを増やして見栄え良くします。

メソッド:
sizeHint(self, option, index)

使い方:行を高めにとる

from PySide6.QtCore import QSize

class ProgressDelegate(QStyledItemDelegate):
    def sizeHint(self, option, index):
        sz = super().sizeHint(option, index)
        # 高さを 8 px 増やす例
        return QSize(sz.width(), sz.height() + 8)

【3】ウィジェット(エディタ)を生成

概要:
セルを編集するためのウィジェット(エディタ)を生成して返します。たとえば文字列は QLineEdit、選択肢は QComboBox、数値は QSpinBox などを返します。

メソッド:
createEditor(self, parent, option, index)

  • createEditor() で作ったウィジェットは、setEditorData() → ユーザー操作 → setModelData() の順で値の受け渡しが行われます。

使い方:QComboBox を返す

from PySide6.QtWidgets import QComboBox

class StatusDelegate(QStyledItemDelegate):
    def createEditor(self, parent, option, index):
        cb = QComboBox(parent)
        cb.addItems(["未着手", "処理中", "完了", "エラー"])
        # 値が確定したらモデルへ反映するための接続
        cb.activated.connect(lambda _idx, ed=cb: self.commitData.emit(ed))
        cb.activated.connect(lambda _idx, ed=cb: self.closeEditor.emit(ed))
        return cb

【4】モデルの現在値をセット

概要:
createEditor() で作ったエディタに対して、モデルの現在値をセットします(編集開始時)。エディタの種別に応じて適切に値を入れてください。

メソッド:
setEditorData(self, editor, index)

使い方:QComboBox に現在値をセット

class StatusDelegate(QStyledItemDelegate):
    def setEditorData(self, editor, index):
        value = index.data(Qt.DisplayRole)
        i = editor.findText(value)
        if i >= 0:
            editor.setCurrentIndex(i)

【5】編集終了時にエディタの値をモデルへ書き戻す

概要:
編集終了時にエディタの値をモデルへ書き戻す処理を実装します。model.setData(index, value, Qt.EditRole) を使うのが一般的です。

メソッド:
setModelData(self, editor, model, index)

使い方:QComboBox の選択をモデルに反映

class StatusDelegate(QStyledItemDelegate):
    def setModelData(self, editor, model, index):
        text = editor.currentText()
        model.setData(index, text, Qt.EditRole)

【6】エディタの表示位置やサイズ

概要:
エディタの表示位置やサイズを設定します。ほとんどの場合 editor.setGeometry(option.rect) で問題ありませんが、余白を取りたいときなどに調整します。

メソッド:
updateEditorGeometry(self, editor, option, index)

使い方:

class StatusDelegate(QStyledItemDelegate):
    def updateEditorGeometry(self, editor, option, index):
        editor.setGeometry(option.rect.adjusted(2, 2, -2, -2))

【7】マウスやキーボードイベントを操作

概要:
セル上でのマウスやキーボードイベントをハンドルできます。主にチェックボックスの切替や、セル内部のクリック判定(アイコン押下)などに使います。戻り値は True でイベントを消費したことを示します。

メソッド:
editorEvent(self, event, model, option, index)

使い方:チェックボックス風にクリックでトグル

from PySide6.QtCore import QEvent, Qt

class CheckboxDelegate(QStyledItemDelegate):
    def editorEvent(self, event, model, option, index):
        if event.type() == QEvent.MouseButtonRelease:
            # boolean 値をトグルする例
            current = index.data(Qt.DisplayRole)
            new = not bool(current)
            model.setData(index, new, Qt.EditRole)
            return True
        return False

【8】文字列に変換して表示

概要:
モデルの値を文字列に変換して表示する際に呼ばれます(paint() で標準表示を使う場合に便利)。例えば数値に % を付ける、日付を整形するなどの用途です。

メソッド:
displayText(self, value, locale)

使い方:

class PercentDelegate(QStyledItemDelegate):
    def displayText(self, value, locale):
        try:
            v = int(value)
            return f"{v}%"
        except Exception:
            return super().displayText(value, locale)

【9】ツールチップやヘルプ表示の動作

概要:
ツールチップやヘルプ表示の動作をカスタマイズできます。QToolTip.showText() を使えば自由に表示できます。

メソッド:
helpEvent(self, event, view, option, index)

使い方:ツールチップ表示

from PySide6.QtWidgets import QToolTip

class HelpDelegate(QStyledItemDelegate):
    def helpEvent(self, event, view, option, index):
        text = f"詳細: {index.data(Qt.DisplayRole)}"
        QToolTip.showText(event.globalPos(), text)
        return True

【10】初期化するユーティリティ

概要:
QStyleOptionViewItem を初期化するユーティリティ。呼べば option にフォント、ペインティング状態、選択状態などの情報が入ります。paint() の先頭で呼ぶと標準のスタイル情報を利用できます。

メソッド:
initStyleOption(self, option, index)

使い方:

class MyDelegate(QStyledItemDelegate):
    def paint(self, painter, option, index):
        self.initStyleOption(option, index)
        # option に既定の情報が入っているので、これを基に描画できる
        super().paint(painter, option, index)

【11】ビュー/モデルへの通知シグナル

概要:
編集ウィジェットから「データをコミットして欲しい」とビュー/モデルに通知するためのシグナルです。エディタでの内部イベント(例:editingFinishedactivated)に接続して self.commitData.emit(editor) を呼びます。これによりフレームワークが setModelData() を呼びます。

メソッド:
commitData(シグナル)

使い方:

cb = QComboBox(parent)
cb.activated.connect(lambda _idx, ed=cb: self.commitData.emit(ed))

【12】編集ウィジェットを閉じる

概要:
編集ウィジェットを閉じたい(編集モードを終了したい)ときに self.closeEditor.emit(editor) を使って通知します。closeEditor は第2引数にヒントを渡せる(QAbstractItemDelegate.EndEditHint)ことがありますが、通常はエディタだけ渡して OK です。

メソッド:
closeEditor(シグナル)

使い方:

cb.activated.connect(lambda _idx, ed=cb: self.closeEditor.emit(ed))

よくある質問(FAQ)

paint() はいつ呼ばれる?super().paint() と何が違う?

表示更新の都度(スクロール、再描画、行の追加など)呼ばれます。super().paint() を呼ぶと Qt の既定の表示(テキスト、アイコン、選択ハイライト等)を使えます。カスタム描画と既定描画を組み合わせるときに便利です。

編集した値がモデルに反映されない(setModelData() が呼ばれない)

commitData シグナルを発行しているか確認してください(例:editor.activated.connect(lambda: self.commitData.emit(editor)))。また setModelData() 内で model.setData(index, value, Qt.EditRole) を正しく呼んでいるかもチェックします。

同じ列で描画用と編集用の別デリゲートを使いたい

QTableView では列ごとに 1 つのデリゲートしか設定できないため、描画と編集の両方を同一クラス内で実装する(paint()createEditor() を同じ QStyledItemDelegate に実装する)のが普通です。

セルにボタンやウィジェットを直接置ける?

描画時にウィジェットを直接置くのは推奨されません(パフォーマンス問題)。編集時に一時的に表示するエディタウィジェット(createEditor())はOKです。常時ウィジェットを置きたいなら setIndexWidget() が使えますが、大量アイテムだと重くなります。

大量データでも高速に動かしたい(パフォーマンス)

paint() 内の処理は軽くしてください。重い処理(画像生成・ファイルI/O等)は避け、可能ならキャッシュを使うか、表示する値そのものを単純化します。QStyledItemDelegate は大量アイテムでも高速に動きますが、setIndexWidget() は避けましょう。

editorEvent()createEditor() の違いは?

editorEvent() は編集ウィジェットを作らずにマウス操作でデータを変えたい場合(チェックボックスの切り替え等)に使います。一方 createEditor() は「エディットモード」にしてウィジェットを実際に表示して編集させたいときに使います。

まとめ

QStyledItemDelegate を覚えると、単なるテーブルが「情報を直感的に伝えるダッシュボード」に早変わりします。最初は次のようなステップで学ぶのがおすすめです:

  • まずは paint() をオーバーライドして簡単な色付けや背景ハイライトから試す。
  • 次に sizeHint() で見た目(行高さ)を整える。
  • createEditor() / setEditorData() / setModelData() を実装して、QComboBoxQSpinBox で編集できるセルを作る。
  • 必要なら editorEvent() でセルクリックの即時反応(チェックのトグル等)を実装する。

この記事のサンプル(プログレスバー + ステータスのコンボ)を動かしてみて、「どのメソッドがいつ呼ばれるか」を体感してください。慣れると、プロダクトで本当に役立つ“かっこいいダッシュボード”が作れます。次はこの Delegate と QAbstractTableModel を組み合わせて リアルタイム更新(センサー値や API) に繋げると実践的です。

シェアしてくださると嬉しいです!
  • URLをコピーしました!

コメント

コメントする

目次