固定列のQTableView

2018年7月4日水曜日

C++ QT

t f B! P L
QTableView の列固定 (スクロール固定)

QTableView の列固定 (スクロール固定)

QT の QTableView で業務システム等でよく要望される、
左からN列目までは横スクロールしないように、固定化する方法です。

はじめに

標準の QTableView には、スクロール固定を行う機能がないため、
カスタムウェジットを作成します。
公式でも以下のページで固定化を実現方法が説明されています。
Frozen Column Example
http://doc.qt.io/qt-5/qtwidgets-itemviews-frozencolumn-example.html
このやり方でも、スクロール固定での表示自体はできますが、
固定列側のセルをクリックしても clicked シグナルが発生しないなど、
表示だけでなく、QTableView を操作・編集するようなアプリの場合に
色々機能が足りません。
今回は、それを改善した物を作ろうと思います。

実現方法

列固定は以下のイメージのように、メインの QTableVIew の上に、
スクロール固定列を表示する、もう一つの QTableView を重ねて配置し、
1つの Tableに見せかけます。
enter image description here

ソースコード

freezetableview.h
#ifndef FREEZETABLEVIEW_H
#define FREEZETABLEVIEW_H

#include <QtWidgets>
#include <QtGui>

class FreezeTableView : public QTableView
{
    Q_OBJECT
public:
    explicit FreezeTableView(QWidget *parent = 0);
    ~FreezeTableView();

    Q_PROPERTY(int freezeColumnCount READ freezeColumnCount WRITE setFreezeColumnCount NOTIFY freezeColumnCountChanged)
    void initFreezeColumn();
    int freezeColumnCount() const { return m_freezeColumnCount; }
    void setItemDelegateForColumn(int column, QAbstractItemDelegate *delegate);
    void edit(const QModelIndex &index);

signals:
    void freezeColumnCountChanged(int freezeColumnCount);

protected slots:
    void columnResized(int column, int, int newWidth);

protected:
    void resizeEvent(QResizeEvent *event) override;
    QModelIndex moveCursor(CursorAction cursorAction, Qt::KeyboardModifiers modifiers) override;
    void scrollTo (const QModelIndex & index, ScrollHint hint = EnsureVisible) override;

public slots:

    void setFreezeColumnCount(int freezeColumnCount) {
        if (m_freezeColumnCount == freezeColumnCount)
            return;
        m_freezeColumnCount = freezeColumnCount;
        emit freezeColumnCountChanged(freezeColumnCount);
    }

    void hideColumn(int column);
    void showColumn(int column);
    void sortByColumn(int column);
    void sortByColumn(int column, Qt::SortOrder order);

private:

    QTableView *frozenTableView;
    int m_freezeColumnCount;
    bool freezenTableColumnResizing_;
    bool tableColumnResizing_;

    void updateFrozenTableGeometry();
    int frozenColumnWidth();

private slots:
    void updateSectionWidth(int logicalIndex, int oldSize, int newSize);
    void updateSectionHeight(int logicalIndex, int oldSize, int newSize);
    void updateFrozenTableSectionWidth(int logicalIndex, int, int newSize);
};

#endif // FREEZETABLEVIEW_H
freezetableview.cpp
#include <QtGlobal>
#ifdef Q_OS_WIN
    #pragma execution_character_set("utf-8")
#endif

#include <QScrollBar>
#include <QHeaderView>
#include "freezetableview.h"

FreezeTableView::FreezeTableView(QWidget *parent) :
    QTableView(parent)
{
    connect(this->horizontalHeader(),SIGNAL(sectionResized(int,int,int)),
            this,SLOT(columnResized(int,int,int)));

    // 固定列表示用のTableViewを初期化する
    frozenTableView = new QTableView(this);

    // 固定列数の変数初期化
    m_freezeColumnCount = 0;
    freezenTableColumnResizing_ = false;
    tableColumnResizing_ = false;
}

FreezeTableView::~FreezeTableView()
{
    delete frozenTableView;
}

void FreezeTableView::columnResized(int column, int /*oldWidth*/, int newWidth)
{
    if (newWidth > this->width() / 2)
    {
        this->setColumnWidth(column, this->width() / 2);
    }
}

void FreezeTableView::initFreezeColumn() {

    /// 固定列表示用の QTableVIew にプロパティをコピー
    frozenTableView->setModel(model());
    frozenTableView->setFocusPolicy(Qt::NoFocus);
    frozenTableView->verticalHeader()->hide();
    frozenTableView->setSelectionBehavior(selectionBehavior());
    frozenTableView->setSelectionMode(selectionMode());
    frozenTableView->horizontalHeader()->setMinimumHeight(this->horizontalHeader()->minimumHeight());
    frozenTableView->setEditTriggers(this->editTriggers());
    viewport()->stackUnder(frozenTableView);
    frozenTableView->setSelectionModel(selectionModel());
    frozenTableView->setContextMenuPolicy(contextMenuPolicy());

    /// 固定列表示用の QTableView は固定列のみを画面に表示し、それ以外を非表示にする
    for (int col = freezeColumnCount(); col < model()->columnCount(); ++col)
        frozenTableView->setColumnHidden(col, true);

    /// 列幅同期
    for (int col = 0; col < freezeColumnCount(); ++col)
        frozenTableView->setColumnWidth(col, columnWidth(col));

    /// 固定列表示用の QTableView はスクロールバーを表示しない
    frozenTableView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
    frozenTableView->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
    frozenTableView->show();

    /// 固定列表示用の QTableView の表示位置設定
    updateFrozenTableGeometry();

    /// スクロールの設定を行う。
    setHorizontalScrollMode(ScrollPerPixel);
    setVerticalScrollMode(ScrollPerPixel);
    frozenTableView->setVerticalScrollMode(ScrollPerPixel);

    const QColor hlClr = Qt::red; // highlight color to set
    const QColor txtClr = Qt::white; // highlighted text color to set
    QPalette p = palette();
    p.setColor(QPalette::Highlight, hlClr);
    p.setColor(QPalette::HighlightedText, txtClr);

    /// シグナル・スロット接続
    /// - ヘッダのリサイズ(幅)
    connect(horizontalHeader(),&QHeaderView::sectionResized, this,
          &FreezeTableView::updateSectionWidth);

    /// - ヘッダのリサイズ(幅) 固定列表示のテーブル
    connect(frozenTableView->horizontalHeader(),&QHeaderView::sectionResized, this,
          &FreezeTableView::updateFrozenTableSectionWidth);

    /// - ヘッダのリサイズ(高さ)
    connect(verticalHeader(),&QHeaderView::sectionResized, this,
          &FreezeTableView::updateSectionHeight);

    /// - 固定列表示用の QTableView 縦スクロール
    connect(frozenTableView->verticalScrollBar(), &QAbstractSlider::valueChanged,
          verticalScrollBar(), &QAbstractSlider::setValue);

    /// - スクロール列表示用の QTableView 縦スクロール
    connect(verticalScrollBar(), &QAbstractSlider::valueChanged,
          frozenTableView->verticalScrollBar(), &QAbstractSlider::setValue);

    /// - clicked シグナルの連動
    QObject::connect(frozenTableView, &QTableView::clicked, [&](const QModelIndex &index){
        emit clicked(index);
    });

    /// - doubleClicked シグナルの連動
    QObject::connect(frozenTableView, &QTableView::doubleClicked, [&](const QModelIndex &index){
        emit doubleClicked(index);
    });

    /// sectionClicked シグナルの連動
    QObject::connect(frozenTableView->horizontalHeader(), &QHeaderView::sectionClicked, [&](int logicalIndex){
        emit horizontalHeader()->sectionClicked(logicalIndex);
    });

    /// - doubleClicked シグナルの連動
    QObject::connect(frozenTableView, &QTableView::customContextMenuRequested, [&](const QPoint &pos){
        emit customContextMenuRequested(pos);
    });

}

void FreezeTableView::setItemDelegateForColumn(int column, QAbstractItemDelegate *delegate)
{
    QTableView::setItemDelegateForColumn(column, delegate);

    if (column < freezeColumnCount()) {
        frozenTableView->setItemDelegateForColumn(column, delegate);
    }
}

void FreezeTableView::edit(const QModelIndex &index)
{
    if (index.column() < freezeColumnCount()) {
        frozenTableView->edit(index);
    } else {
       QTableView::edit(index);
    }
    updateFrozenTableGeometry();
}

void FreezeTableView::hideColumn(int column) {

    QTableView::hideColumn(column);
    frozenTableView->hideColumn(column);

    if (freezeColumnCount() > 0) {
        /// 固定列表示用の QTableView は固定列のみを画面に表示し、それ以外を非表示にする
        for (int col = freezeColumnCount(); col < model()->columnCount(); ++col) {
            tableColumnResizing_ = true;
            frozenTableView->setColumnHidden(col, true);
            tableColumnResizing_ = false;
        }

        updateFrozenTableGeometry();
    }
}

void FreezeTableView::showColumn(int column) {
    QTableView::showColumn(column);

    if (column < freezeColumnCount()) {
        bool isHidden = frozenTableView->isColumnHidden(column);
        frozenTableView->showColumn(column);
        if (isHidden) {
            updateFrozenTableGeometry();
        }
    }
}

void FreezeTableView::sortByColumn(int column)
{
    if (column < freezeColumnCount()) {
        frozenTableView->sortByColumn(column);
    } else {
        QTableView::sortByColumn(column);
    }
}

void FreezeTableView::sortByColumn(int column, Qt::SortOrder order)
{
    if (column < freezeColumnCount()) {
        frozenTableView->sortByColumn(column, order);
    } else {
       QTableView::sortByColumn(column, order);
    }
}

void FreezeTableView::updateSectionWidth(int logicalIndex, int /* oldSize */, int newSize) {
    if (freezenTableColumnResizing_) {
        return;
    }
    if (logicalIndex <= freezeColumnCount() - 1){
        tableColumnResizing_ = true;
        frozenTableView->setColumnWidth(logicalIndex, newSize);
        tableColumnResizing_ = false;
        updateFrozenTableGeometry();
    }
}

void FreezeTableView::updateFrozenTableSectionWidth(int logicalIndex, int /* oldSize */, int newSize) {
    if (tableColumnResizing_) {
        return;
    }
    freezenTableColumnResizing_ = true;
    setColumnWidth(logicalIndex, newSize);
    freezenTableColumnResizing_ = false;
    updateFrozenTableGeometry();
}

void FreezeTableView::updateSectionHeight(int logicalIndex, int /* oldSize */, int newSize) {
    frozenTableView->setRowHeight(logicalIndex, newSize);
}

void FreezeTableView::resizeEvent(QResizeEvent * event) {
    /// QTableView のサイズが変われば、固定列表示用の QTableView にも反映する
    QTableView::resizeEvent(event);
    updateFrozenTableGeometry();
}

QModelIndex FreezeTableView::moveCursor(CursorAction cursorAction, Qt::KeyboardModifiers modifiers) {
    QModelIndex current = QTableView::moveCursor(cursorAction, modifiers);

    int width = frozenColumnWidth();
    if (cursorAction == MoveLeft && current.column() > (freezeColumnCount() - 1)
          && visualRect(current).topLeft().x() < width ) {
        const int newValue = horizontalScrollBar()->value() + visualRect(current).topLeft().x() - width;
        horizontalScrollBar()->setValue(newValue);
    }
    return current;
}

void FreezeTableView::scrollTo (const QModelIndex & index, ScrollHint hint) {
    if (index.column() > freezeColumnCount() - 1)
        QTableView::scrollTo(index, hint);
}

void FreezeTableView::updateFrozenTableGeometry() {

    //モデルの初期化が未 or 列数が0の場合は処理スキップ
    if (model() == NULL || model()->columnCount() == 0) {
        frozenTableView->setGeometry(0, 0, 0, 0);
        return;
    }

    //固定する列数が未設定の場合、処理スキップ
    if (freezeColumnCount() == 0) {
        frozenTableView->setGeometry(0, 0, 0, 0);
        return;
    }

    int width = frozenColumnWidth();

    //固定表示用の TableWidget のサイズ調整
    frozenTableView->setGeometry(verticalHeader()->width(),
                               0 , width ,
                               viewport()->height()+horizontalHeader()->height());

}

int FreezeTableView::frozenColumnWidth() {
    int width = 0;
    for (int i = 0; i < freezeColumnCount(); i++) {
        if (!isColumnHidden(i)) {
            width += columnWidth(i);
        }
    }
    return width;
}

使用方法

  1. freezetableview.h、freezetableview.cpp の2つを自分のプロジェクトに取り込んでください。
  2. デザイナを開いてスクロール固定したい QTableVIew を選択します。
  3. 右クリックメニューより [格上げ先を指定]を選択
  4. 以下のイメージの通り、格上げ先を指定します
enter image description here
enter image description here
  1. 初期化用のコードを記述します
// 2列目までを固定する設定で、カスタムウェジットを初期化
ui->tableView->initFreezeColumn(2);
スポンサーリンク

QooQ