如何優化QTableView的性能?
最近在做數據處理的項目,需要將處理後的數據通過表示形式進行展現,業務情況大致如下:
1、無需實時更新數據,而是通過用戶操作(如切換列表選項)觸發更新。2、更新時需重置整張表格數據,數據量非常龐大,常見情況為約1-20萬行,10列到數百列不等。3、每個單元格的數據內容都是單純的整形或者浮點型。4、數據不連貫,可能有的單元格沒有數據目前遇到問題是:重置表格數據時,表格首次重繪奇卡無比,耗時巨大,且耗時和表格列數有明顯正相關目前實現方式如下:Step 1:通過用戶操作,確認數據範圍,發送請求到後台線程。Step 2:後台生產者線程,根據數據範圍,從數據源提取數據,一邊提取一邊發送至消費者線程。Step 3:消費者線程接收原始數據,對其根據用戶指令進行一定處理和變換。Step 4:處理完畢的數據,整合後統一推送至自定義的TableModel。Step 5:TableModel接收到數據後,通過beginResetModel()方法通知界面準備更新,然後將數據保存在本地,並重置HeaderText等輔助界面運作的數據結構。然後通過endResetModel()方法刷新界面。 Step 6:數據容器為兩層嵌套的QVector,內層每個Vector為一個column。針對數據空格,做了一個row*column大小的雙層vector,用來保存實際的數據索引。TableModel的rowCount()、columnCount()均為輸出索引表對應維度的長度。data()方法內直接通過row、column兩個下標查詢索引表,索引存在則根據索引取出對應數據,無索引則返回空值。
現在遇到的問題是:1、處理大約1GB的原始數據,生成數據大約幾百萬個值,耗時僅需幾秒。2、生成的數據傳遞至TableModel後,由於QVector內部隱式共享,TableModel接收數據,並重置內部數據結構耗時可以忽略。3、TableModel調用endResetModel()後,界面的QTableView開始遍歷model,重繪表格,這個過程的耗時比數據處理還要長,大約10-20秒量級……4、相比於行數的增加,耗時對列數更加敏感。從2-3萬行增加至10-20萬行時,表格重繪耗時增加很少。而從10-20列增加至100列乃至1000列時,表格重繪耗時急劇提升。PS:受部署環境所限,沒辦法用QML的TableView。PS2:生成的數據量,無法根據輸入數據量和數據範圍計算確定,需要通過消費者進行解析處理後,才會生成不定數量的數據,故無法提前分配表格空間。
PS3:自行寫了個最粗暴的demo,數據用QVector&&>,數據長度每次reset()都隨機生成,行數10w上下,列數1k上下,數據值隨機。但生成+刷新耗時都不超過三秒,這特么就詭異了……PSV:需要Windows/Linux雙平台運行,為避免編譯器差異(如不定參宏格式等),現在用的是GCC和MinGW編譯器。
我是題主,今天剛解決了此問題,來回答下我的解決方案
正如題目里所說,我自己寫的一個輕量級的demo,數據刷新幾乎無延遲,和實際程序中嚴重不符。
經過各種profile,最終定位了問題原因——VerticalHeader的resize。實際開發的這個程序,由於數據每次請求都是不同的,header的內容也會不同,所以更新數據後需要對其進行resize。
而單純的把resizeMode設置為resizeToContents效果並不好,只會在第一次更新數據時生效。之後更新數據後,不管是調用endResetModel()還是headerDataChanged(),QTableView都不會刷新verticalHeader的寬度,只會刷新其中的數據。如果要刷新verticalHeader的寬度,要麼重設model,要麼把QTableView給hide()再show()出來,調用update()或者repaint()都不行。
然而,只要resizeMode是QHeaderView::ResizeToContents,那麼採用上述兩種方法觸發全局重繪時,QTableView都會請求表頭中每一個section的數據,並且用QFontMetrics計算寬度,故而數據量龐大時,這個計算耗時長到無法容忍。所以,在數據量非常龐大時,使用自適應的resize是不可行的。
——————————————————————————
於是我們嘗試了以下兩種解決方案
1、手動設置定長的表頭數據。比如表頭可能出現的數據是1到100000,那麼我們就以最長的為標準,不夠長度的補空格。
這樣不用需要設置自適應,表格刷新和切換可以瞬間完成。但代價是表頭很醜,在數據小時有大片空白2、使用當前最大的表頭數據,僅對該數據用QFontMetrics計算寬度,然後對錶頭設置fixedWidth()。這樣理論可行,然而實際很醜陋,調整寬度後多出來的部分只有QWidget底色,文本信息並沒有繪製上去,導致表頭文本顯示不完全,且表頭效果繪製錯誤。然後嘗試搜索了"QTableView VerticalHeader width"關鍵字之後,在Qt論壇搜到一篇帖子,裡面給了一個腦洞清奇的解法——設置表頭寬度後,將首列寬度設置為0,再重置,即代碼如下:ui-&>tableView-&>verticalHeader()-&>setFixedWidth(width);
width = ui-&>tableView-&>columnWidth(0);
ui-&>tableView-&>setColumnWidth(0, 0);
ui-&>tableView-&>setColumnWidth(0, width);
上述代碼,會導致表格布局改變,從而觸發表格的全局重繪,在這次重繪里,重設寬度過後的表頭也能正常顯示了。
由於所有column和表頭都沒有設置自適應,所以這個重繪效率很高。最後profile結果如下:
- 在設置自適應後,表格刷新耗時10ms內。但若要重新適配表頭,則需要耗時10s左右。 - 關閉自適應,通過手動設置fixedWidth方式適配表頭,數據刷新加適配表頭,總體耗時不超過20ms。——————————————————————————
結論
resizeToContents這個尺寸自適應模式,不管是對於column,還是對於header,都慎用。該函數會遍歷該contents內所有數據,逐一通過QFontMetrics計算尺寸,這個耗時在數據量到達一定量級的情況下,高到無法接受。
本來,使用view-model-delegate機制就是為了規避性能問題,在這套機制下,動態更新數據非常靈活,並且UI只會繪製顯示的那一部分數據,所以理論上不管數據量有多大,界面耗時都是常量級的。但也有一些例外,這些情況下依舊會遍歷所有數據,典型的就是resizeToContents。真如題主這樣,遇到非要自適應不可的場景,也最好根據實際場合中的數據特點,手動進行適配贊一下。我做過類似的表格,用的就是QML的TableView,不過數據量不是太大,遇到的主要都是功能問題。在這總結分享一下,做的不好輕噴。
先上個效果圖,好讓大家有個直觀的感受:
我的需求:
1、表格可以導入、導出xml/json格式的數據。
2、表格的每一列是定製的(數據類型和用來顯示的控制項都不同)
3、表格編輯功能,包括增、刪、改、查,撤銷、恢復,以及特定規則的內容校驗。
我的實現:
1、顯然要做xml/json的解析,Qt對xml/json的支持已經很好了。這裡的難點是,把json/xml轉換成TableView需要的數據ListModel,和把ListModel轉換回json/xml數據。qml中有JSON.parse和JSON.stringify可以完成這個工作,實際開發中要注意字元編碼、js中的Object 源信息過濾等問題。
2、TableView按照列定製:我是把所有可能的列對應的控制項都寫出來,然後根據實際數據類型,顯示相應的控制項。這種方法在表格列有限的情況下是可行的。實際開發的時候,會研究的問題是,行號和列號怎麼拿到,滑鼠點擊高亮,數據怎麼綁定(比如TextField的初始數據怎麼設置和修改後怎麼設置回ListModel中,才比較合理,而且不造成循環綁定)等等。這些問題大部分都能在TableView的幫助文檔里找到,極少問題需要搜索引擎/請教老司機(我就碰到了一個,headerDelegate裡面拿不到滑鼠懸浮事件,公司一圈人都問了一遍,兩天都搞不定,最後去問公司最老的一個Qt程序員(他在Qt公司工作過),他看了一分鐘就搞定了,設置z軸,別人根本想不到啊!!!)。
3、增、刪、改、查和內容校驗都是對ListModel的操作了。ListModel本身提供了添加、刪除、修改的操作,以及遍歷每個Object的get方法,夠用了。撤銷、恢復是對整個表格增、刪、改的操作,不是只對一個TextEdit操作的右鍵菜單那種。我用C++做了一個撤銷、恢復的數據結構,存的數據就是QString,C++ 層面不關心數據內容,只實現撤銷、恢復的邏輯。然後在qml裡面去用這個數據結構,每次增刪改操作的時候,記錄操作,即用特定JSON格式的字元串往這個數據結構裡面存,撤銷、恢復的時候再從數據結構裡面拿數據,去操作ListModel。
我的代碼:
之後會放在github上。
----------------------------分割線-------------------------------
補上github的地址:wentaojia2014/TableEdit
----------------------------分割線-------------------------------
補一個關於z軸的,看代碼
import QtQuick 2.6
import QtQuick.Controls 2.1
import QtQuick.Controls 1.4 as QC14
ApplicationWindow {
visible: true
width: 640
height: 480
title: qsTr("Hello World")
QC14.TableView {
width: parent.width
height: parent.height
QC14.TableViewColumn {
role: "title"
title: "Title"
width: 100
}
QC14.TableViewColumn {
role: "author"
title: "Author"
width: 200
}
model: ListModel {
id: libraryModel
ListElement {
title: "A Masterpiece"
author: "Gabriel"
}
ListElement {
title: "Brilliance"
author: "Jens"
}
}
headerDelegate: Rectangle {
id: headerDelegate
implicitWidth: 100
height: 40
color: "#2d2d2d"
border.width: 1
border.color: "#838383"
Rectangle {
anchors.right: parent.right
width: 40
height: 40
color: "red"
MouseArea {
anchors.fill: parent
hoverEnabled: true
focus: true
onHoveredChanged: {
//這裡的HoveredChanged信號,默認情況下是拿不到的,滑鼠單擊的時候才能拿到,與普通的MouseArea不一致。
console.log("containMouse", containsMouse)
}
}
}
//這幾行是老司機加的代碼,加上之後就能正常拿到hoveredChanged信號了
// Component.onCompleted: {
// parent.z = 10
// }
}
}
}
model要動態載入,顯示多少載入多少,不要加多餘的,這樣多少條都行
自己繪製QTableView,屏幕顯示的數據量是一定的,將不需要展示的數據丟掉,只繪製要顯示的數據
謝邀(其實沒人邀請我)。
說實話第一次見到這麼巨大的表格。
如果沒有複雜的交互需求,僅僅是展示數據的話,我一般不用任何第三方控制項(Control、Component),那麼怎麼做呢?自己畫!速度一流!!
推薦閱讀:
※你見過哪些令你瞠目結舌的C/C++代碼技巧?
※gcc為何有時會在call前push eax,默認的調用約定不是cdecl?
※現代C/C++編譯器有多智能?能做出什麼厲害的優化?
※參加2017年在新浪舉辦的北京場「前端體驗大會」是個什麼樣的體驗?