Customizing QTableView grid style

Viewed 640

I'm wondering several things. I have subclassed QTableView to make a custom table. I'd like to be able to do several things.

First of all, I wanted the selected cells not to all have the "selected" color (blue by default) but instead have a frame around the selected cells (just like in Excel). To do this, I've used the following (in my custom QItemDelegate):

void MyDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
{
    QModelIndex upIndex = index.sibling(index.row() - 1, index.column());
    QModelIndex downIndex = index.sibling(index.row() + 1, index.column());
    QModelIndex rightIndex = index.sibling(index.row(), index.column() + 1);
    QModelIndex leftIndex = index.sibling(index.row(), index.column() - 1);

    auto newOption = option;
    if (option.state.testFlag(QStyle::State_Selected))
    {
        painter->save();

        auto selIndexes = selM->selectedIndexes().toSet();
        painter->setPen(QPen(Qt::red, 5));
        if (!selIndexes.contains(rightIndex))
            painter->drawLine(option.rect.topRight(), option.rect.bottomRight());
        if (!selIndexes.contains(upIndex))
            painter->drawLine(option.rect.topLeft(), option.rect.topRight());
        if (!selIndexes.contains(downIndex))
            painter->drawLine(option.rect.bottomLeft(), option.rect.bottomRight());
        if (!selIndexes.contains(leftIndex))
            painter->drawLine(option.rect.topLeft(), option.rect.bottomLeft());

        painter->restore();
        // newOption.palette.setBrush(QPalette::Normal, QPalette::Highlight, index.data(Qt::BackgroundRole).value<QColor>());
        newOption.palette.setBrush(QPalette::Normal, QPalette::Highlight, Qt::gray);
    }
    QStyledItemDelegate::paint(painter, newOption, index);
}

This is probably not optimal, but I'd like to have something that works, before anything else.

Now it seems to be working, but it unfortunately isn't automatically repainted. What happens when I select cells is the following:

Incorrect

Which is not what I'm looking for at all. I guess it's not being repainted because (1) The points inside the zone are still red and (2) If I resize my window, I get the following:

Kind of correct

Which is way closer to what I'm trying to achieve.

I've already tried to do this in my QTableView:

// selModel is my selection model
connect(selModel, &QItemSelectionModel::selectionChanged, [this]() {
    for(const auto & selected : selModel->selectedIndexes())
    {
        update(visualRect(selected));
        repaint(visualRect(selected));
    }
}

(Before, I actually used setDirtyRegion but it didn't work either, so I figured I'd do something more... brutal.)

Finally, I have another question: why do I get those weird red "small lines" in my cell corners? Even on the "kind of" correct screenshot I get these lines that I can't explain:

Small lines

Please suggest if you have any ideas for any of the issues.

1 Answers

The problem can be easily explained as follows.

Assume that the cell (0,0) is selected. Now the user selects the additional cells (0,1), (1,0) and (1,1). Qt correctly repaints the additional 3 cells, that became selected. But, it doesn't repaint the cell (0,0), as it was neither selected nor deselected. Of course for your desired behavior you still need to redraw this cell.

This can easily be achieved by just redrawing all indices of your current selection.

MyDelegate.h

#pragma once

#include <QStyledItemDelegate>
#include <QItemSelectionModel>

class MyDelegate : public QStyledItemDelegate {

public:
    virtual void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override;

    void setSelectionModel(QItemSelectionModel* selectionModel);
private:
    QItemSelectionModel* mSelectionModel{ nullptr };
};

MyDelegate.cpp

#include "MyDelegate.h"
#include <QPainter>
#include <QDebug>

void MyDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
{
    if (!mSelectionModel) return;
    auto newOption = option;
    auto normalText = newOption.palette.brush(QPalette::ColorGroup::Normal, QPalette::ColorRole::Text);
    newOption.palette.setBrush(QPalette::ColorGroup::Normal, QPalette::ColorRole::Highlight, QBrush(Qt::GlobalColor::blue, Qt::BrushStyle::NoBrush));
    newOption.palette.setBrush(QPalette::ColorGroup::Normal, QPalette::ColorRole::HighlightedText, normalText);
    QStyledItemDelegate::paint(painter, newOption, index);
    QModelIndex upIndex = index.sibling(index.row() - 1, index.column());
    QModelIndex downIndex = index.sibling(index.row() + 1, index.column());
    QModelIndex rightIndex = index.sibling(index.row(), index.column() + 1);
    QModelIndex leftIndex = index.sibling(index.row(), index.column() - 1);
    //auto newOption = option;
    //newOption.palette.setBrush(QPalette::Normal, QPalette::Highlight, Qt::transparent);
    if (option.state.testFlag(QStyle::State_Selected))
    {
        painter->save();
        painter->setClipRect(option.rect);
        auto selIndexes = mSelectionModel->selectedIndexes().toSet();
        painter->setPen(QPen(Qt::red, 5));
        if (!selIndexes.contains(rightIndex))
            painter->drawLine(option.rect.topRight(), option.rect.bottomRight());
        if (!selIndexes.contains(upIndex))
            painter->drawLine(option.rect.topLeft(), option.rect.topRight());
        if (!selIndexes.contains(downIndex))
            painter->drawLine(option.rect.bottomLeft(), option.rect.bottomRight());
        if (!selIndexes.contains(leftIndex))
            painter->drawLine(option.rect.topLeft(), option.rect.bottomLeft());

        painter->restore();
    }
}

void MyDelegate::setSelectionModel(QItemSelectionModel* selectionModel)
{
    mSelectionModel=selectionModel;
}

main.cpp

#include <QApplication>
#include <QDebug>
#include <QStandardItemModel>
#include <QTableWidget>
#include "MyDelegate.h"

int main(int argc, char** args) {
    QApplication app(argc, args);
    auto widget = new QTableView;
    QStandardItemModel model;
    model.setRowCount(10);
    model.setColumnCount(10);
    for (auto i = 0; i < 10; i++) {
        for (auto j = 0; j < 10; j++) {
            model.setItem(i, j, new QStandardItem("Test"));
        }
    }
    auto selModel = new QItemSelectionModel;
    selModel->setModel(&model);
    widget->setModel(&model);
    widget->setSelectionModel(selModel);

    auto delegate = new MyDelegate;
    delegate->setSelectionModel(selModel);
    widget->setItemDelegate(delegate);
    // Ensures that also items are repainted, that where neither selected nor deselect, but will stay selected
    // This solutions eventually paints elements twice
    QObject::connect(selModel, &QItemSelectionModel::selectionChanged, [widget,selModel](auto &selected, auto& deselected) {
        for (auto item : selModel->selectedIndexes()) {
            widget->update(item); 
        }
    });
    widget->show();
    app.exec();
}

As for the strange red line artifacts, you should paint the red line inside the allowed rectangle I guess. This is easily achieved by clipping to the item boundaries.

Related