I want to separate specific columns/rows in a QTableWidget
using a bold colored line.
I could easily achieve a separation by drawing lines on both sides of the the cells adjacent to the column separation (by implementing a custom QItemDelegate
):
However, I do not want to draw inside the cells, but rather in the area between them.
Furthermore, an extension of the line into the header would be the ideal appearance for my application:
Is there a way to achieve this look?
We override paintEvent
in a subclass of QTableView
(I let you replace with QTableWidget
if you need) and another (private) subclass of QHeaderView
.
If the model has fewer records than the view can display at once, you can choose whether to draw the line all the way down to the bottom (or the scrollbar), or if you prefer the line to stop on the last row of the model. In the latter case, uncomment lines I marked.
#include <QtWidgets/QHeaderView>
#include <QtWidgets/QTableView>;
#include <QtGui/QPaintEvent>
#include <QtGui/QPainter>
// Tableview that draws a rectangle between 2 columns, called split rectangle.
// The split rectangle is drawn over the right part of a chosen column.
// A big part of the code consists in tweaking some behavior to hide the fact that the column actually extends under the split rectangle. Among other things:
// The header and column width are increased to leave enough space for the rectangle.
// Nothing will happen for item selection if the click takes place over the rectangle.
class MyTableView : public QTableView {
private:
class MyHeader : public QHeaderView {
public:
MyHeader(int splitAfterColumn, int lineThickness, MyTableView* parent) :
QHeaderView(Qt::Horizontal, parent),
splitColumn(splitAfterColumn),
thickness(lineThickness)
{}
protected:
void paintSection(QPainter* painter, const QRect& rect, int logicalIndex) const override
{
if (logicalIndex + 1 == splitColumn)
QHeaderView::paintSection(painter, rect.adjusted(0, 0, -thickness, 0), logicalIndex);
else
QHeaderView::paintSection(painter, rect, logicalIndex);
}
void paintEvent(QPaintEvent* event) override
{
QHeaderView::paintEvent(event);
int x = sectionViewportPosition(3);
QPainter p(viewport());
p.setBrush(Qt::black);
if (auto maxAvailableSize = sectionSize(splitColumn-1); maxAvailableSize < thickness)
p.fillRect(x - maxAvailableSize, 0, maxAvailableSize, height(), Qt::BrushStyle::SolidPattern);
else
p.fillRect(x - thickness, 0, thickness, height(), Qt::BrushStyle::SolidPattern);
}
QSize sectionSizeFromContents(int logicalIndex) const override {
if (logicalIndex + 1 == splitColumn) {
QSize contentSize = QHeaderView::sectionSizeFromContents(logicalIndex);
if (contentSize.width() < minimumSectionSize())
contentSize.setWidth(minimumSectionSize());
return contentSize + QSize(thickness, 0);
}
else
return QHeaderView::sectionSizeFromContents(logicalIndex);
}
private:
int splitColumn, thickness;
};
// The delegate exists only to draw a correct dotted rectangle around the index that has the focus.
// A (now unused) inDelegate attribute can be leveraged
public:
MyTableView(int splitAfterColumn, int lineThickness, QWidget* parent = nullptr) :
QTableView(parent),
splitColumn(splitAfterColumn),
thickness(lineThickness)
{
setHorizontalHeader(new MyHeader(splitColumn, thickness, this));
}
QModelIndex indexAt(const QPoint& pos) const
{
QModelIndex index = QTableView::indexAt(pos);
if (index.column() + 1 == splitColumn) {
if (visualRect(index).contains(pos))
return index;
else
return QModelIndex();
}
else
return index;
}
QRect visualRect(const QModelIndex& index) const
{
QRect rect = QTableView::visualRect(index);
if (rect.isValid() && ! rect.isNull() && index.column() + 1 == splitColumn)
return rect.adjusted(0, 0, -thickness, 0);
else return rect;
}
QRegion visualRegionForSelection(const QItemSelection& selection) const
{
//Adjust the region covered by a selection.
QRegion region = QTableView::visualRegionForSelection(selection);
return region.subtracted(QRegion(separatorArea()));
}
protected:
void paintEvent(QPaintEvent* event) override
{
QTableView::paintEvent(event);
if (model() && splitColumn > 0) {
QPainter p(viewport());
p.setBrush(Qt::black);
QRect splitRect = separatorArea();
p.fillRect(splitRect, Qt::BrushStyle::SolidPattern);
QStyleOptionViewItem option;
option.initFrom(this);
const QColor gridColor = static_cast<QRgb>(style()->styleHint(QStyle::SH_Table_GridLineColor, &option, this));
p.setPen(gridColor);
p.drawLine(splitRect.topLeft() - QPoint(1, 0), splitRect.bottomLeft() - QPoint(1, 0));
}
}
int sizeHintForColumn(int column) const override
{
return QTableView::sizeHintForColumn(column) + (column + 1 == splitColumn ? thickness : 0);
}
private:
QRect separatorArea() const
{
int x = columnViewportPosition(3);
//Replace h by the commented lines below if the line should stop on the last model row.
int h = height();
//auto findHeight = [this]() -> int {
// //Dichotomic search of the table height (if smaller than its viewport)
// int hTop = 0, hBottom = height();
// while (hTop < hBottom - 1) {
// if (auto hMiddle = (hTop + hBottom) >> 1; indexAt(QPoint(0, hMiddle)).isValid())
// hTop = hMiddle;
// else
// hBottom = hMiddle;
// }
// return indexAt(QPoint(0, hBottom)).isValid() ? hBottom : hTop;
//};
//int h = findHeight();
if (auto maxAvailableSize = columnWidth(splitColumn - 1); maxAvailableSize < thickness)
return QRect(x - maxAvailableSize, 0, maxAvailableSize, h);
else
return QRect(x - thickness, 0, thickness, h);
}
int splitColumn, thickness;
};
I let you optimize MyTableView::paintEvent
by detecting cases where calls to the findHeight
can be skipped and/or keeping the value of findHeight in memory as long as the model does not change.
You will have to detect changes of the view height, the model attached to the view, the rows being added/removed to the model and the model being reset (I think but have not checked this should be it).
You can use the main function below to test the class:
int main(int argc, char** arga)
{
QApplication a(argc, arga);
QStandardItemModel model;
for (int r = 1; r <= 10; ++r) {
QList<QStandardItem*> items;
for (int c = 1; c <= 10; ++c)
items.append(new QStandardItem(QString::number(r) + ',' + QString::number(c)));
model.appendRow(items);
}
MyTableView view(3, 20);
view.setHorizontalScrollMode(QAbstractItemView::ScrollMode::ScrollPerPixel);
view.setMinimumSize(640, 480);
view.setModel(&model);
view.resizeColumnsToContents();
view.show();
return a.exec();
}
Edit:
There remains a small issue with the outline that appears on the index that has the focus. It can be solved with a proxy delegate, whose code is not finished.
#include <QtWidgets/QStyledItemDelegate>
class MyProxyDelegate : public QStyledItemDelegate
{
public:
MyProxyDelegate (int lineThickness, QObject* parent = nullptr) :
QStyledItemDelegate(parent),
thickness(lineThickness),
inDelegate(nullptr)
{}
protected:
void paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const
{
QStyleOptionViewItem opt = option;
if (index.column() + 1 == 3)
opt.rect.adjust(0, 0, -thickness, 0);
if (inDelegate)
inDelegate->paint(painter, opt, index);
else
QStyledItemDelegate::paint(painter, opt, index);
}
private:
int thickness;
QAbstractItemDelegate* inDelegate;
friend class MyTableView;
};