原文連接:Qt實現表格控件-支持多級列表頭、多級行表頭、單元格合併、字體設置等app
最近在研究QTableView支持多級表頭的事情,百度了下網上資料仍是挺多的。實現的方式總的來講有2種,效果都還不錯,最主要是搞懂其中的原理,作到以不變應萬變。框架
實現多級表頭的方式有如下兩種方案ide
以上兩種方式均可以實現多級表頭,各有利弊,而且已經有人投入項目使用。函數
我我的仍是比較偏向於第二種方式,由於這樣咱們才能夠更好的瞭解Qt的底層,瞭解Qt的繪圖機制,而且這樣實現的效率也是比較高的,並且合理一些,比較可控(我的理解)。學習
後來我在網上找到了一個哥們寫的控件,項目名字叫作RbTableHeaderView,挺不錯的,能夠實現咱們要的功能,可是效果仍是差一些,若是須要更友好的交互效果,那麼還須要在繼續完善這個demo。字體
今天閒來無事,找到了一個開源的網站,上邊好多Qt的庫,雖然有一些是很早之前的東西,可是也很值得咱們去學習。爲何會提到這個網站呢?由於這個網站上就有咱們要的這個多級表頭事例,和上邊提到的那個哥們的事例不謀而合。網站
想要學習更多開源事例的能夠到openDesktop上去看看。還有我本身收錄的牛逼哄哄的Qt庫this
下面咱們就來說解這個多節表頭的實現方式,代碼比較簡單,主要是你們理解下這個實現方式,能夠加以擴展。spa
後續的文章中我會在寫一篇關於樹控件多級表頭的事例,這裏先把文章名稱掛載這裏,後續發佈後就能夠看到--Qt實現表格樹控件-支持多級表頭
多級表頭的效果下圖所示,很糙粗的一個demo,你們將就着看吧。
定製表頭咱們主要是要重寫2個東西,分別是數據源QAbstractTableModel和表頭QHeaderView
數據源就是爲視圖提供數據的model,咱們的全部顯示的內容數據都來自這個model。
對於外部程序填充數據時和往常使用一樣的方式
for (int i = 0; i < 10; i++) { QList<QStandardItem*> items; for (int j = 0; j < 8; j++) { items.append(new QStandardItem(QString("item(%1, %2)").arg(i).arg(j))); } dataModel->appendRow(items); }
重寫了這個數據源後,咱們主要是爲了完成data的返回數據過程,View最關心的就是這個接口
class RbTableHeaderModel : public QAbstractTableModel { Q_OBJECT public: // override virtual QVariant data(const QModelIndex &index, int role) const; private: // properties int row_count_prop; int column_count_prop; // inherent features RbTableHeaderItem* root_item; };
下面就是data的函數實現,是否是大失所望,全部的額操做好像被封裝到RbTableHeaderItem這個節點中去了。
QVariant RbTableHeaderModel::data(const QModelIndex & index, int role) const { if (!index.isValid()) return QVariant(); if (index.row() >= row_count_prop || index.row() < 0 || index.column() >= column_count_prop || index.column() < 0) return QVariant(); RbTableHeaderItem * item = static_cast<RbTableHeaderItem *>(index.internalPointer()); return item->data(role); }
RbTableHeaderItem結構表示了一個單元格,並且他還維護了全部的表格cell子節點。
注意看下面index的構造,把index和RbTableHeaderItem這個結構綁定在了一塊兒。
index中的不少數據也都存儲在了RbTableHeaderItem這個結構中,後續咱們在講視圖的時候你們就會發現了。
Model也就這麼多內動了,View纔是咱們的重頭戲。
QModelIndex RbTableHeaderModel::index(int row, int column, const QModelIndex & parent) const { RbTableHeaderItem * parentItem; if (!parent.isValid()) parentItem = root_item; // parent item is always the root_item on table model else parentItem = static_cast<RbTableHeaderItem*>(parent.internalPointer()); // no effect RbTableHeaderItem * childItem = parentItem->child(row, column); if (!childItem) childItem = parentItem->insertChild(row, column); return createIndex(row, column, childItem); return QModelIndex(); }
重寫表頭時,公有接口用於設置單元格行高、列寬、背景色和前景色的,單元格合併等。
保護接口都是重寫父類的方法,在合適的實際會被框架進行調用。
inherent features
註釋下的方法是本身封裝的方法,方便其餘函數調用。
class RbTableHeaderView : public QHeaderView { void setRowHeight(int row, int rowHeight); void setColumnWidth(int col, int colWidth); void setSpan(int row, int column, int rowSpanCount, int columnSpanCount); void setCellBackgroundColor(const QModelIndex & index, const QColor &); void setCellForegroundColor(const QModelIndex & index, const QColor &); protected: // override virtual void mousePressEvent(QMouseEvent * event); virtual void paintSection(QPainter * painter, const QRect & rect, int logicalIndex) const; protected Q_SLOTS: void onSectionResized(int logicalIdx, int oldSize, int newSize); Q_SIGNALS: void sectionPressed(int from, int to); };
下面咱們分析幾個比較重要的函數
一、mousePressEvent鼠標按下
當鼠標按下時mousePressEvent函數被觸發,而後咱們須要去計算那個單元格被按下了,並通知視圖,讓視圖去選擇某些cell集合。
這個函數的處理邏輯會比較負責一些,這個dmeo作的有問題,這裏我就不按照demo中的代碼來說解了。
首先咱們仍是得根據本身的需求來實現這個鼠標按下事件,對於大多數的程序來講,可能都是鼠標按下時,選中視圖中的單元格集合,那麼咱們這裏也就按照這個思路來分析。
以下圖所示,咱們在程序加載過程當中,給表頭頭設置了合併屬性,對於合併了的表格項,他們對象的index中都是存儲了紅色文字信息的。
當咱們點擊了某一個item時,程序就須要去判斷是否點擊了這個大的合併sell,而後去選擇tableview視圖上的cell集合。
就是這麼簡單,可是實現起來仍是有必定難度的。
思路就到這裏了,具體邏輯你們能夠去思考下。
二、paintSection繪製函數
UI上真正的繪製函數其實就是paintSection函數,當這個函數回調的時候,咱們只須要在程序給定的區域內繪製上文本便可,那麼問題來了,這個區域是這麼計算出來的,既然咱們要合併了列和行,那麼每個區域的大小應該都是不同的。
分析的一點都沒錯,這個區域的大小Qt已經幫咱們留好了接口--sectionSizeFromContents
咱們只須要重寫這個函數便可,根據咱們以前保存的index上合併列和行的數據進行計算,計算出一個合適的區域,而後把值返回便可。
QSize RbTableHeaderView::sectionSizeFromContents(int logicalIndex) const { const RbTableHeaderModel * tblModel = qobject_cast<const RbTableHeaderModel*>(this->model()); const int OTN = orientation(); const int LEVEL_CNT = (OTN == Qt::Horizontal) ? tblModel->rowCount() : tblModel->columnCount(); QSize siz = QHeaderView::sectionSizeFromContents(logicalIndex); for (int i = 0; i < LEVEL_CNT; ++i) { QModelIndex cellIndex = (OTN == Qt::Horizontal) ? tblModel->index(i, logicalIndex) : tblModel->index(logicalIndex, i); QModelIndex colSpanIdx = columnSpanIndex(cellIndex); QModelIndex rowSpanIdx = rowSpanIndex(cellIndex); siz = cellIndex.data(Qt::SizeHintRole).toSize(); if (colSpanIdx.isValid()) { int colSpanFrom = colSpanIdx.column(); int colSpanCnt = colSpanIdx.data(COLUMN_SPAN_ROLE).toInt(); int colSpanTo = colSpanFrom + colSpanCnt - 1; siz.setWidth(columnSpanSize(colSpanIdx.row(), colSpanFrom, colSpanCnt)); if (OTN == Qt::Vertical) i = colSpanTo; } if (rowSpanIdx.isValid()) { int rowSpanFrom = rowSpanIdx.row(); int rowSpanCnt = rowSpanIdx.data(ROW_SPAN_ROLE).toInt(); int rowSpanTo = rowSpanFrom + rowSpanCnt - 1; siz.setHeight(rowSpanSize(rowSpanIdx.column(), rowSpanFrom, rowSpanCnt)); if (OTN == Qt::Horizontal) i = rowSpanTo; } } return siz; }
三、列大小改變
當手動拖拽列帶下時,onSectionResized槽函數會被調用,而後咱們須要在這個函數中把相鄰的列頭大小進行從新設置。
void RbTableHeaderView::onSectionResized(int logicalIndex, int oldSize, int newSize) { for (int i = 0; i < LEVEL_CNT; ++i) { QSize cellSize = cellIndex.data(Qt::SizeHintRole).toSize(); // set position of cell if (OTN == Qt::Horizontal) { sectionRect.setTop(rowSpanSize(logicalIndex, 0, i)); cellSize.setWidth(newSize); } else { sectionRect.setLeft(columnSpanSize(logicalIndex, 0, i)); cellSize.setHeight(newSize); } tblModel->setData(cellIndex, cellSize, Qt::SizeHintRole); QRect rToUpdate(sectionRect); rToUpdate.setWidth(viewport()->width() - sectionRect.left()); rToUpdate.setHeight(viewport()->height() - sectionRect.top()); viewport()->update(rToUpdate.normalized()); } }
大體的實現思路就是這樣的,因爲核心實現代碼邏輯比較長,大多數的代碼我只保留了關鍵的執行步驟。
下面這一大堆代碼看似很長,其實很好理解,就是調用咱們封裝好的控件進行設置
hHead->setSpan(0, 0, 3, 0); hHead->setSpan(0, 1, 2, 2); hHead->setSpan(1, 3, 2, 0); hModel->setData(hModel->index(0, 0), QStringLiteral("一級表頭"), Qt::DisplayRole); hModel->setData(hModel->index(0, 1), QStringLiteral("一級表頭"), Qt::DisplayRole); hModel->setData(hModel->index(2, 1), QStringLiteral("二級表頭"), Qt::DisplayRole); hModel->setData(hModel->index(2, 2), QStringLiteral("二級表頭"), Qt::DisplayRole); hModel->setData(hModel->index(0, 3), QStringLiteral("一級表頭"), Qt::DisplayRole); hModel->setData(hModel->index(1, 3), QStringLiteral("二級表頭"), Qt::DisplayRole); vHead->setSpan(0, 0, 0, 3); vHead->setSpan(1, 0, 3, 0); vHead->setSpan(1, 1, 2, 0); vModel->setData(vModel->index(0, 0), QStringLiteral("一級表頭"), Qt::DisplayRole); vModel->setData(vModel->index(1, 0), QStringLiteral("一級表頭"), Qt::DisplayRole); vModel->setData(vModel->index(1, 1), QStringLiteral("二級表頭"), Qt::DisplayRole); vModel->setData(vModel->index(3, 1), QStringLiteral("二級表頭"), Qt::DisplayRole); vModel->setData(vModel->index(1, 2), QStringLiteral("三級表頭"), Qt::DisplayRole); vModel->setData(vModel->index(2, 2), QStringLiteral("三級表頭"), Qt::DisplayRole); vModel->setData(vModel->index(3, 2), QStringLiteral("三級表頭"), Qt::DisplayRole); hHead->setRowHeight(0, 30); hHead->setRowHeight(1, 30); hHead->setRowHeight(2, 30); vHead->setRowHeight(0, 30); vHead->setRowHeight(1, 30); vHead->setRowHeight(2, 30); vHead->setColumnWidth(0, 50); vHead->setColumnWidth(1, 50); vHead->setColumnWidth(2, 50); hHead->setSectionsClickable(true); vHead->setSectionsClickable(true); hHead->setCellBackgroundColor(hModel->index(0, 0), 0xcfcfcf); hHead->setCellBackgroundColor(hModel->index(0, 1), 0xcfcfcf); vHead->setCellBackgroundColor(vModel->index(0, 0), Qt::cyan); vHead->setCellBackgroundColor(vModel->index(1, 0), 0xcfcfcf);
這個demo網上也能夠下載到,或者去我文章開頭留下的開源庫地址去拿也行。
如今的CSDN不敢恭維了,代碼也沒地方放了。代碼若是下載不到也能夠留言我來發。。。
很重要--轉載聲明