Qt カスタム描画入門:QTreeView drawRow() を使った行のカスタマイズ例

2025-05-27

QTreeView::drawRow() は、QTreeView クラス(およびその基底クラスである QAbstractItemView)の仮想関数の一つです。この関数は、ツリービュー内の特定の行を描画するために呼び出されます

より具体的に説明すると、以下のようになります。

  • 引数
    drawRow() 関数は通常、描画を行うための QPainter オブジェクト、描画先のオプションを含む QStyleOptionViewItem オブジェクト、そして描画するアイテムのモデルインデックス QModelIndex を引数として受け取ります。これらの引数を通じて、描画に必要な情報が提供されます。
  • 呼び出しのタイミング
    drawRow() は、ツリービューがアイテムを表示する必要がある際に、Qt の内部的な描画処理の過程で呼び出されます。これには、初期表示時、アイテムのデータが変更された時、スクロール時、選択状態が変更された時などが含まれます。
  • 仮想関数
    これは仮想関数であるため、QTreeView を継承したカスタムクラス内で再実装(オーバーライド)することで、行の描画処理をカスタマイズできます。例えば、特定の条件を満たす行の背景色を変えたり、特別なアイコンを表示したりといったことが可能です。
  • 役割
    drawRow() 関数は、モデル内のデータに基づいて、ツリービューの指定された行の表示をどのように行うかを決定し、実際に描画処理を行うための基盤を提供します。

カスタム実装の例

もし QTreeView を継承したクラスで drawRow() を再実装する場合、典型的な処理の流れは以下のようになります。

  1. 親クラスの drawRow() を呼び出す (オプション)
    基本的な描画処理を親クラスに任せたい場合は、最初に QTreeView::drawRow(painter, option, index); を呼び出します。
  2. 描画オプションのカスタマイズ
    option パラメータを通じて、背景色、フォント、選択状態など、デフォルトの描画オプションにアクセスできます。必要に応じてこれらのオプションを変更します。
  3. モデルデータの取得
    index パラメータを使って、描画するアイテムに対応するモデルのデータを取得します。例えば、index.data(Qt::DisplayRole) で表示テキストを取得できます。
  4. QPainter を使った描画
    取得したデータやカスタマイズしたオプションに基づいて、painter オブジェクトのメソッド(fillRect(), drawText(), drawPixmap() など)を使って、行の各要素を描画します。


一般的なエラーとトラブルシューティング

    • 原因
      • QPainter の設定が不適切(例:ブラシやペンが設定されていない、色が透明になっている)。
      • 描画範囲の計算ミス(座標やサイズが間違っている)。
      • モデルから取得したデータが期待する形式ではない。
      • 親クラスの drawRow() を適切に呼び出していない(必要なデフォルト描画が行われていない)。
    • トラブルシューティング
      • QPainter の設定(ブラシ、ペン、フォントなど)を明示的に行う。
      • 描画処理を行う前に、描画範囲(option.rect など)をよく確認する。
      • モデルから取得したデータの型と内容をデバッグ出力などで確認する。
      • 基本的な描画処理を維持したい場合は、最初に QTreeView::drawRow(painter, option, index); を呼び出すことを検討する。
  1. パフォーマンスの問題 (描画が遅い)

    • 原因
      • drawRow() 内で複雑な計算や重い処理を行っている。
      • 頻繁にスタイルシートの再評価や複雑な描画処理を繰り返している。
      • 不要な描画処理を行っている(例:画面に表示されていない部分も描画しようとしている)。
    • トラブルシューティング
      • drawRow() 内の処理をできる限り軽量化する。複雑な処理は事前に計算しておくか、別のスレッドで行うことを検討する。
      • スタイルシートの利用を適切に行い、不必要な再評価を避ける。
      • 描画が必要な範囲のみを描画するように最適化する(通常は option.rect 内)。
  2. 選択状態やフォーカス状態の描画が不正

    • 原因
      • option.state を適切に評価していない(選択されているか、フォーカスがあるかなど)。
      • 選択色やフォーカスインジケータの描画処理が正しくない。
      • デフォルトの選択/フォーカス描画を上書きしてしまい、必要な情報が表示されなくなっている。
    • トラブルシューティング
      • option.state のフラグ(QStyle::State_Selected, QStyle::State_HasFocus など)を適切にチェックし、状態に応じた描画を行う。
      • QStyle クラスのメソッド(proxy()->drawControl(), proxy()->drawPrimitive() など)を利用して、プラットフォームに依存しない標準的な選択/フォーカス表示を行うことを検討する。
  3. アイテムの高さが正しく計算されない

    • 原因
      • カスタム描画によって、アイテムの論理的な高さがデフォルトと異なるにもかかわらず、sizeHint() を再実装していない。
      • drawRow() 内での描画内容が、アイテムの高さに影響を与えている。
    • トラブルシューティング
      • カスタム描画に合わせて、QAbstractItemDelegate を使用している場合はその sizeHint() 関数を、QTreeView を直接サブクラス化している場合は rowHeight() 関数を再実装し、正しい高さを返すようにする。
  4. デリゲートとの連携の問題 (カスタムデリゲートを使用している場合)

    • 原因
      • drawRow() とカスタムデリゲートの paint() 関数で描画処理が競合している、または矛盾している。
      • デリゲートが提供する描画オプションを drawRow() で無視している。
    • トラブルシューティング
      • カスタムデリゲートを使用している場合は、drawRow() ではなく、デリゲートの paint() 関数内で描画処理を実装することを推奨する。
      • もし drawRow() で追加の描画を行いたい場合は、デリゲートの描画結果を尊重し、その上に重ねるように描画する。

トラブルシューティングの一般的なヒント

  • シンプルな描画から始める
    まずは基本的な描画(単色の背景、シンプルなテキスト表示など)を実装し、徐々に複雑な描画を追加していく。
  • デバッグ出力を活用する
    描画処理の中で、変数の値(座標、サイズ、データなど)を qDebug() などで出力し、意図した値になっているか確認する。


基本的な例: 行の背景色を変更する

この例では、特定の条件を満たす行の背景色をカスタムで設定します。

#include <QApplication>
#include <QTreeView>
#include <QStandardItemModel>
#include <QPainter>
#include <QStyleOptionViewItem>

class CustomTreeView : public QTreeView {
public:
    CustomTreeView(QWidget *parent = nullptr) : QTreeView(parent) {}

protected:
    void drawRow(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override {
        QStyleOptionViewItem customOption = option;

        // 特定の条件(例:0列目のアイテムのテキストが "Error" の場合)
        QVariant data = index.model()->data(index.siblingAtColumn(0), Qt::DisplayRole);
        if (data.toString() == "Error") {
            customOption.backgroundBrush = QBrush(Qt::red);
        } else if (index.row() % 2 == 0) {
            // 偶数行の背景色を少し薄いグレーにする
            customOption.backgroundBrush = QBrush(QColor(240, 240, 240));
        }

        // 親クラスの drawRow を呼び出し、基本的な描画を行う
        QTreeView::drawRow(painter, customOption, index);
    }
};

int main(int argc, char *argv[]) {
    QApplication a(argc, argv);

    QStandardItemModel model;
    model.setHorizontalHeaderLabels({"Name", "Value"});
    model.appendRow({new QStandardItem("Item 1"), new QStandardItem("10")});
    model.appendRow({new QStandardItem("Error"), new QStandardItem("20")});
    model.appendRow({new QStandardItem("Item 3"), new QStandardItem("30")});
    model.appendRow({new QStandardItem("Error"), new QStandardItem("40")});
    model.appendRow({new QStandardItem("Item 5"), new QStandardItem("50")});

    CustomTreeView treeView;
    treeView.setModel(&model);
    treeView.show();

    return a.exec();
}

この例では、CustomTreeView クラスが QTreeView を継承し、drawRow() をオーバーライドしています。drawRow() 内で、描画する行のモデルインデックス (index) を使って、0列目のテキストを取得し、それが "Error" であれば背景色を赤に設定しています。また、偶数行の背景色を薄いグレーにしています。最後に、変更したオプション (customOption) とともに親クラスの drawRow() を呼び出し、実際の描画を行っています。

応用的な例: アイコンとテキストをカスタム描画する

この例では、各行にアイコンを表示し、テキストの色をデータに基づいて変更します。

#include <QApplication>
#include <QTreeView>
#include <QStandardItemModel>
#include <QPainter>
#include <QStyleOptionViewItem>
#include <QIcon>
#include <QColor>

class CustomTreeViewWithIcon : public QTreeView {
public:
    CustomTreeViewWithIcon(QWidget *parent = nullptr) : QTreeView(parent) {}

protected:
    void drawRow(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override {
        QStyleOptionViewItem customOption = option;

        // 親クラスの drawRow を最初に呼び出し、基本的な要素(背景、選択など)を描画
        QTreeView::drawRow(painter, customOption, index);

        // アイコンを描画
        QIcon icon(":/images/qt_logo.png"); // リソースファイルに "qt_logo.png" があると仮定
        if (!icon.isNull()) {
            QRect iconRect = option.rect;
            iconRect.setWidth(20); // アイコンの幅
            iconRect.setHeight(20); // アイコンの高さ
            iconRect.moveCenter(QPoint(option.rect.left() + 15, option.rect.center().y()));
            icon.paint(painter, iconRect);
            customOption.rect.adjust(30, 0, 0, 0); // テキストの描画位置をアイコンの幅だけ右にずらす
        }

        // テキストの色をデータに基づいて変更
        QVariant valueData = index.model()->data(index.siblingAtColumn(1), Qt::DisplayRole);
        bool ok;
        int value = valueData.toInt(&ok);
        if (ok && value > 30) {
            painter->setPen(Qt::blue);
        } else {
            painter->setPen(Qt::black);
        }

        // テキストを描画 (アイコンの描画位置を考慮)
        QString text = index.model()->data(index, Qt::DisplayRole).toString();
        painter->drawText(customOption.rect, option.displayAlignment, text);
    }
};

int main(int argc, char *argv[]) {
    QApplication a(argc, argv);

    QStandardItemModel model;
    model.setHorizontalHeaderLabels({"Name", "Value"});
    model.appendRow({new QStandardItem("Item A"), new QStandardItem("25")});
    model.appendRow({new QStandardItem("Item B"), new QStandardItem("40")});
    model.appendRow({new QStandardItem("Item C"), new QStandardItem("15")});

    CustomTreeViewWithIcon treeView;
    treeView.setModel(&model);
    treeView.show();

    return a.exec();
}

#include "main.moc" // moc ファイル (必要に応じて)

この例では、まず親クラスの drawRow() を呼び出して基本的な描画を行います。その後、アイコンを描画するために QIcon を使用し、描画位置を計算しています。テキストの描画位置はアイコンの幅だけ右に調整しています。さらに、2列目の値が 30 より大きい場合はテキストの色を青に、それ以外の場合は黒に設定しています。最後に、調整された描画領域 (customOption.rect) を使ってテキストを描画しています。

  • リソースファイル (.qrc) を使用する場合は、適切に設定し、アイコンなどのリソースがアプリケーションからアクセスできる状態になっている必要があります。
  • アイテムのサイズヒント (sizeHint()) がカスタム描画の内容と一致していることを確認してください。そうでない場合、アイテムが正しく表示されないことがあります。必要に応じて、QAbstractItemDelegate を使用し、その中で sizeHint() を実装することを検討してください。
  • カスタム描画を行う際は、選択状態やフォーカス状態など、標準的な UI の挙動を考慮する必要があります。option.state を確認し、適切に描画を切り替えることが重要です。
  • drawRow() 内では、できるだけ軽量な処理に留めるべきです。複雑な計算や I/O 処理を行うと、描画のパフォーマンスに影響が出る可能性があります。


デリゲート (Delegates) の使用 (QAbstractItemDelegate およびそのサブクラス)

  • デメリット
    • 複雑性
      デリゲートの作成は、drawRow() のオーバーライドよりも一般的にコード量が多く、理解が難しい場合があります。
    • ビューとの連携
      デリゲートとビューの間で信号とスロットを使った連携が必要になる場合があります。
  • paint() 関数
    デリゲートの paint() 関数は、drawRow() と同様に、QPainterQStyleOptionViewItemQModelIndex を引数として受け取り、アイテムの描画処理を行います。

例 (簡単な背景色の変更)

#include <QApplication>
#include <QTreeView>
#include <QStandardItemModel>
#include <QPainter>
#include <QStyleOptionViewItem>
#include <QStyledItemDelegate>
#include <QColor>

class CustomDelegate : public QStyledItemDelegate {
public:
    CustomDelegate(QObject *parent = nullptr) : QStyledItemDelegate(parent) {}

    void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override {
        QStyleOptionViewItem customOption = option;

        QVariant data = index.model()->data(index.siblingAtColumn(0), Qt::DisplayRole);
        if (data.toString() == "Special") {
            customOption.backgroundBrush = QBrush(QColor(200, 255, 200)); // 明るい緑
        }

        QStyledItemDelegate::paint(painter, customOption, index);
    }
};

int main(int argc, char *argv[]) {
    QApplication a(argc, argv);

    QStandardItemModel model;
    model.setHorizontalHeaderLabels({"Name", "Value"});
    model.appendRow({new QStandardItem("Item 1"), new QStandardItem("10")});
    model.appendRow({new QStandardItem("Special"), new QStandardItem("20")});
    model.appendRow({new QStandardItem("Item 3"), new QStandardItem("30")});

    QTreeView treeView;
    treeView.setModel(&model);

    CustomDelegate *delegate = new CustomDelegate(&treeView);
    treeView.setItemDelegate(delegate);

    treeView.show();

    return a.exec();
}

スタイルシート (Style Sheets)

  • デメリット
    • 描画ロジックの制限
      スタイルシートは主に外観のカスタマイズに特化しており、複雑なカスタム描画ロジック(例:特定の形状を描画する、アニメーションを行うなど)には向きません。
    • パフォーマンス
      複雑なスタイルシートは、パフォーマンスに影響を与える可能性があります。
  • メリット
    • 宣言的
      外観の定義がコードから分離され、より宣言的な方法で記述できます。
    • 柔軟性
      実行時にスタイルを動的に変更できます。
    • テーマの適用
      アプリケーション全体で一貫した外観を簡単に実現できます。
  • カスタマイズ範囲
    背景色、文字色、フォント、ボーダー、アイコンなど、多くの視覚的な属性をスタイルシートで制御できます。
  • 適用
    スタイルシートは、アプリケーション全体、特定のウィジェット、またはその子ウィジェットに適用できます。

例 (特定のテキストを持つアイテムの背景色を変更)

#include <QApplication>
#include <QTreeView>
#include <QStandardItemModel>

int main(int argc, char *argv[]) {
    QApplication a(argc, argv);

    QStandardItemModel model;
    model.setHorizontalHeaderLabels({"Name", "Value"});
    model.appendRow({new QStandardItem("Item 1"), new QStandardItem("10")});
    model.appendRow({new QStandardItem("Special"), new QStandardItem("20")});
    model.appendRow({new QStandardItem("Item 3"), new QStandardItem("30")});

    QTreeView treeView;
    treeView.setModel(&model);
    treeView.setStyleSheet("QTreeView::item[column=\"0\"][data=\"Special\"] { background-color: lightgreen; }");
    treeView.show();

    return a.exec();
}

この例では、スタイルシートを使用して、0列目のデータが "Special" であるアイテムの背景色を明るい緑に設定しています。

モデルのデータロールの活用 (Qt::DecorationRole, Qt::TextColorRole, Qt::BackgroundRole など)

  • デメリット
    • カスタマイズの限界
      より複雑なカスタム描画には向きません。提供されているロールの範囲内でのカスタマイズとなります。
  • メリット
    • 簡潔性
      簡単な外観の変更であれば、drawRow() やデリゲートを直接操作するよりも簡潔に実装できます。
    • モデルとの連携
      外観のロジックをモデルに含めることができるため、データの意味合いに基づいて表示を制御できます。
  • 実装
    モデルの data() 関数をオーバーライドし、特定のロールに対して適切な値を返します。ビューはこれらのロールに基づいてアイテムを描画します。

例 (アイコンと背景色をモデルのデータロールで設定)

#include <QApplication>
#include <QTreeView>
#include <QStandardItemModel>
#include <QIcon>
#include <QColor>

class CustomModel : public QStandardItemModel {
public:
    CustomModel(int rows, int columns, QObject *parent = nullptr) : QStandardItemModel(rows, columns, parent) {}

    QVariant data(const QModelIndex &index, int role) const override {
        if (role == Qt::DecorationRole && index.column() == 0) {
            if (QStandardItemModel::data(index, Qt::DisplayRole).toString().startsWith("Item")) {
                return QIcon(":/images/info.png"); // リソースファイルに "info.png" があると仮定
            }
        } else if (role == Qt::BackgroundRole && index.column() == 1) {
            bool ok;
            int value = QStandardItemModel::data(index, Qt::DisplayRole).toInt(&ok);
            if (ok && value > 30) {
                return QBrush(QColor(255, 230, 230)); // 薄い赤
            }
        }
        return QStandardItemModel::data(index, role);
    }
};

int main(int argc, char *argv[]) {
    QApplication a(argc, argv);

    CustomModel model(3, 2);
    model.setHorizontalHeaderLabels({"Name", "Value"});
    model.setItem(0, 0, new QStandardItem("Item A"));
    model.setItem(0, 1, new QStandardItem("25"));
    model.setItem(1, 0, new QStandardItem("Item B"));
    model.setItem(1, 1, new QStandardItem("40"));
    model.setItem(2, 0, new QStandardItem("Other"));
    model.setItem(2, 1, new QStandardItem("15"));

    QTreeView treeView;
    treeView.setModel(&model);
    treeView.show();

    return a.exec();
}

#include "main.moc" // moc ファイル (必要に応じて)

どの方法を選ぶべきか?

  • ビューの特定の行に対してのみカスタム描画を行いたい場合 (ただし、再利用性は低い)
    drawRow() のオーバーライドが比較的簡単です。
  • 個々のアイテムの複雑なカスタム描画
    デリゲートの使用が最も柔軟性が高く、推奨されます。
  • 簡単な外観の変更 (色、フォントなど)
    スタイルシートやモデルのデータロールの活用が適しています。

多くの場合、これらの方法を組み合わせて使用することも可能です。例えば、基本的な外観はスタイルシートで設定し、特定のアイテムに対してはデリゲートでより詳細な描画を行うといった使い方が考えられます。