Entroduction
Developing spreadsheet-like functionality within Qt applications often requires custom widget compositions, particularly when features such as frozen panes, auto-resizing rows, and cell merging are needed. While standard QTableView provides robust data display capabilities, it lacks native support for locking specific rows or columns during scrolling. This guide details the architecture and implementation of a advanced table widget that mimics Excel behavior by leveraging multiple synchronized view instances.
Architectural Overview
The core challenge in implementing frozen panes is maintaining visual consistency while allowing independent scrolling for the non-frozen areas. The solution involves overlaying multiple QTableView instances. A naive approach might suggest using two views (one for frozen columns, one for the rest), but handling the intersection of frozen rows and columns simultaneously requires a quadrant-based strategy.
To achieve full functionality, four view instances are managed:
- Top-Left: Displays frozen rows and frozen columns.
- Top-Right: Displays frozen rows only.
- Bottom-Left: Displays frozen columns only.
- Bottom-Right (Main): Displays the scrollable content area.
These views share the same model but maintain independent headers and scrollbars that are programmatically synchronized.
Implemantation Details
1. Synchronizing Views and Scrollbars
The primary mechanism involves connecting the header resizing signals and scrollbar value changes across all four views. When the main view scrolls, the frozen views must update their corresponding scroll positions to ensure alignment. Conversely, header adjustments in any view must propagate to the others to maintain column width consistency.
Below is a refactored initialization sequence demonstrating how the views are linked:
void AdvancedTableWidget::initializeFrozenPanes()
{
// Initialize the four quadrant views
m_topLeftView = new QTableView(this);
m_topRightView = new QTableView(this);
m_bottomLeftView = new QTableView(this);
// Configure view properties (hide scrollbars where not needed)
m_topLeftView->verticalScrollBar()->hide();
m_topLeftView->horizontalScrollBar()->hide();
m_topRightView->verticalScrollBar()->hide();
m_bottomLeftView->horizontalScrollBar()->hide();
// Sync header resizing
connect(mainView->horizontalHeader(), &QHeaderView::sectionResized,
this, &AdvancedTableWidget::propagateColumnWidth);
connect(mainView->verticalHeader(), &QHeaderView::sectionResized,
this, &AdvancedTableWidget::propagateRowHeight);
// Sync vertical scrolling between main and left views
connect(mainView->verticalScrollBar(), &QAbstractSlider::valueChanged,
m_bottomLeftView->verticalScrollBar(), &QAbstractSlider::setValue);
connect(m_bottomLeftView->verticalScrollBar(), &QAbstractSlider::valueChanged,
mainView->verticalScrollBar(), &QAbstractSlider::setValue);
// Sync horizontal scrolling between main and top views
connect(mainView->horizontalScrollBar(), &QAbstractSlider::valueChanged,
m_topRightView->horizontalScrollBar(), &QAbstractSlider::setValue);
connect(m_topRightView->horizontalScrollBar(), &QAbstractSlider::valueChanged,
mainView->horizontalScrollBar(), &QAbstractSlider::setValue);
// Sync selection models across all quadrants
syncSelectionModels();
}
2. Managing Cell Selection
Since four separate views exist, selection states must be unified. When a user clicks a cell in one quadrant, the selection in the other three views must be cleared or updated to reflect the same logical index. This prevents visual artifacts where multiple cells appear selected simultaneously across the split views.
The following logic handles selection synchronization based on the quadrant clicked:
void AdvancedTableWidget::handleSelectionChange(const QItemSelection &selected)
{
if (selected.isEmpty()) return;
QModelIndex currentIndex = selected.indexes().first();
int r = currentIndex.row();
int c = currentIndex.column();
// Determine which quadrant was interacted with and clear others
if (r < m_frozenRowCount && c < m_frozenColumnCount) {
// Top-Left clicked
clearSelectionsExcept(m_topLeftView);
} else if (r < m_frozenRowCount && c >= m_frozenColumnCount) {
// Top-Right clicked
clearSelectionsExcept(m_topRightView);
} else if (r >= m_frozenRowCount && c < m_frozenColumnCount) {
// Bottom-Left clicked
clearSelectionsExcept(m_bottomLeftView);
} else {
// Main view clicked
clearSelectionsExcept(mainView);
}
}
3. Auto-Resizing Row Heights
To mimic Excel's content adaptation, row heights should adjust dynamically based on the text content. This is achieved by connecting the model's data change signals to the view's resizeRowToContents method. This feature can be toggled via a public interface.
void AdvancedTableWidget::enableDynamicRowHeight(bool active)
{
if (active) {
connect(dataModel, &QStandardItemModel::itemChanged,
this, [this](QStandardItem *item) {
mainView->resizeRowToContents(item->row());
// Propagate height change to frozen views
updateFrozenRowHeights(item->row());
}, Qt::UniqueConnection);
} else {
disconnect(dataModel, &QStandardItemModel::itemChanged, this, nullptr);
}
}
4. Custom Selection Delegate (Ant-Line)
Standard Qt selection borders are often solid rectangles. To achieve a more distinct "marquee" or animated border effect (often called an ant-line), a custom QStyledItemDelegate is required. This delegate overrides the paint method to draw a custom border around the selected item.
Key considerations for the delegate:
- Ensure the base class
paintmethod is called to retain standard styles. - Use a timer or offset variable to animate the dash pattern if motion is desired.
class MarqueeDelegate : public QStyledItemDelegate
{
Q_OBJECT
public:
explicit MarqueeDelegate(QObject *parent = nullptr);
void setAnimated(bool enabled);
void setBorderColor(const QColor &color);
protected:
void paint(QPainter *painter, const QStyleOptionViewItem &option,
const QModelIndex &index) const override;
private:
void renderCustomBorder(QPainter *painter, const QRect &rect) const;
bool m_animated;
int m_dashOffset;
QColor m_borderColor;
};
Usage Examples
The following snippets demonstrate how to configure and populate the custom table widget.
Populating Data
Data can be loaded from various sources. Here is an example of parsing a CSV-like structure into the widget:
void loadDataset(AdvancedTableWidget *table, const QString &filePath)
{
QFile source(filePath);
if (!source.open(QIODevice::ReadOnly)) return;
QTextStream stream(&source);
QStringList headers = stream.readLine().split(',');
table->setHorizontalHeaders(headers);
while (!stream.atEnd()) {
QString line = stream.readLine();
if (line.startsWith('#') || line.isEmpty()) continue;
QStringList fields = line.split(',');
int rowIdx = table->rowCount();
table->insertRow(rowIdx);
for (int col = 0; col < fields.size(); ++col) {
table->setItemData(rowIdx, col, fields.at(col).trimmed());
}
}
}
Configuring Frozen Panes
Freezing specific rows and columns is exposed via a simple API:
// Freeze the first 2 rows and first 2 columns
tableWidget->setFrozenAreas(2, 2);
Styling Cells
Individual cells can be styled regarding background, foreground, alignment, and font properties:
// Set text color for specific range
for (int col = 0; col < 5; ++col) {
tableWidget->setCellForegroundColor(0, col, Qt::red);
}
// Set background color
for (int col = 0; col < 5; ++col) {
tableWidget->setCellBackgroundColor(1, col, Qt::lightGray);
}
// Apply custom font
QFont customFont("Arial", 12, QFont::Bold);
customFont.setItalic(true);
tableWidget->setCellFont(3, 0, customFont);
Merging and Adjustments
Cell merging and dynamic sizing can be controled programmatically:
// Merge a 2x2 block starting at row 5, column 5
tableWidget->mergeCells(5, 5, 2, 2);
// Enable auto-resize for content changes
tableWidget->enableDynamicRowHeight(true);
// Customize selection border
tableWidget->setSelectionBorderColor(Qt::blue);
tableWidget->setSelectionBorderStyle(Style::Dashed);