本篇目的:了解 delegate class 內部各 method 的意義,藉此設計可用於
QSpinBox的 delegate class
好的 UI 設計要能引導 user 正確使用程式,該輸入整數的地方就該用 QSpinBox、固定選項的輸入就該用 QComboBox 等。但 user 在 QTableWidget 輸入時沒有任何的限制,導致 user 可能會像下圖一樣,有整數欄位輸入字串、輸入內容前有空格等各種神奇異常。
這樣子的設計不只容易讓使用者錯誤輸入,在注重資料正確性的產線環境更是難以使用 (OP 在過貨壓力下,他們得一個一個按鍵輸入 SPEC,還要避免輸入錯誤?)。因此若我們要在生產環境中使用,必須修改 QTableWidget 的輸入方式。
當 user 使用空 QTableWidget 時,預設的的輸入方式是 QLineEdit,也因此難以限制輸入內容。今明兩天的文章便會說明如何使用委派(delegate)方式,將 QSpinBox 、 QComboBox 帶入到 QTableWidget 中,相關內容如下:
Day 21: 說明 delegate 內部的參數意義以及建構 delegate class 方法,並以 QSpinBox 做示範
Day 22: 說明如何做出不同的 delegate class,並且與方向鍵事件結合,最後做出一個只要帶入 table 與指定欄位即可套用 delegate 的 function

QTableWidget 內模型與視圖、與委派間的關聯QTableWidget 是一個 model/ view architecture 的物件,裡面有儲存資料的 model 、顯示資料的 view,並且以 delegate 管理 view 怎麼被編輯、model 如何被寫入。三個物件的詳細說明如下:
Model (模型): 儲存資料的物件,可以想像成資料庫。其中各儲存格的資料會用 day 19 提到的 QTableWidgetItem 儲存
View (視圖): 顯示資料的物件。View 就只是個顯示器,顯示內容由 model 決定、而顯示方式由 delegate 決定
Delegate (委派): Model 與 view 之間的橋樑。Delegate 會在使用者編輯 view 的某格時,設定使用者的輸入方式;當使用者設定完成後,delegate 會將輸入資料傳入 model、並將輸入資料以設定方式(如顯示小數數量/ 底色等)顯示於 view 中
我們今天便是要透過修改指定 QTableWidget 的 delegate,強制儲存格編輯時載入 QSpinBox 、 QComboBox 等編輯器。Model/ view/ delegate 三者的關係示意圖如下。

QSpinBox delegate class 的實作一個可以修改儲存格輸入方式之 delegate class,會設定以下 3 種 method
編輯方式的設定:createEditor()
從 model 寫入資料到物件的設定:setEditorData()
將編輯完成資料寫入到 model 的設定:setModelData()
以下我們會以實作一個可以帶入 QSpinBox (限定使用者輸入整數) 的 delegate class,來說明這 3 個 method 的設定方式
QSpinBox 的相關參數設定實作 delegate class 的方式是先繼承 QStyledItemDelegate,再去自訂 mothod 覆蓋原先的 createEditor() 、 setEditorData() 、 setModelData()。 QStyledItemDelegate 是 QTableWidget 預設的 delegate class。
可以看到 \__init_\_ 中有一個引數 parent。 __init__ 內必須有 parent 此引數,但它可設定可不設定。parent 通常會設定成使用的 QTableWidget ,用途是管理這個物件的狀態(生命週期)。若有設定 parent 且 parent 被清理掉,這個 delegate 物件也會被清理掉並釋放出記憶體。
QSpinBox 可以設定最大最小值、step 等,這些東西可以拉出來到 \__init_\_ 中,讓我們後續可以設定。
from PySide6.QtCore import Qt
from PySide6.QtWidgets import (
QAbstractSpinBox,
QSpinBox,
QStyledItemDelegate,
)
class IntSpinDelegate(QStyledItemDelegate):
"""在表格中使用整數 SpinBox。"""
def __init__(
self,
minv=-9999,
maxv=9999,
step=1,
parent=None,
):
super().__init__(parent)
self.minv = minv
self.maxv = maxv
self.step = step
createEditor()createEditor() 會在 user 開始編輯儲存格時建立 editor (我們要用的 QSpinBox)。並針對該 QSpinBox 做最大最小值、step 等相關設定
另外我們會一直在 delegate class 看見以下引數(就算我們在這 method 中沒有使用),這些引數的意義如下:
parent:編輯器的父元件(通常是表格的 viewport)。傳給 QSpinBox(parent) 可以確保editor 正確浮動顯示在儲存格位置上,並在編輯結束時由 Qt 自動管理記憶體回收。
option (QStyleOptionViewItem):紀錄該儲存格當前的「外觀狀態與風格資訊」之 class,例如文字對齊、字型、背景色、是否處於選取狀態等。
另外我們能透過 option.widget 取得目前使用的 QTableWidget ,這個功能會在明天的文章中用到
index (QModelIndex):代表目前被點擊、正在編輯的 ”儲存格座標與資料索引”。它提供了 index.row() 與 index.column(),我們可以用這個 index 從 model 中取值
def createEditor(
self,
parent,
option,
index,
):
"""建立表格儲存格的 QSpinBox 編輯器。"""
editor = QSpinBox(parent)
editor.setRange(
self.minv,
self.maxv,
)
editor.setSingleStep(
self.step,
)
# 隱藏上下調整按鈕,保留表格內簡潔的輸入介面
editor.setButtonSymbols(
QAbstractSpinBox.ButtonSymbols.NoButtons,
)
return editor
因為是覆蓋人家的 method,因此不可以改引數的名稱
setEditorData()setEditorData() 的工作是將 Model 目前儲存的資料載入 QSpinBox。因此資料流方向是:
Model → QSpinBox Editor
首先先對 model 取值,使用 index.data(取值規則) 就可以直接對對應 model 取值了。取值的優先順位如下
取得 model 內的原始值 (EditRole)
取得顯示值 (DisplayRole),顯示值為格式化的資料,例如只顯示小數後兩位的 float
接者再將值寫入 editor 中,若不能寫入 editor 則設定 editor 為最小值
def setEditorData(
self,
editor,
index,
):
"""將 Model 中的資料載入 QSpinBox。"""
value = (
index.data(Qt.ItemDataRole.EditRole)
or index.data(Qt.ItemDataRole.DisplayRole)
or 0
)
try:
editor.setValue(int(value))
except (TypeError, ValueError):
editor.setValue(self.minv)
setModelData()setModelData() 會取得 user 在 QSpinBox 中設定的的整數值,並將它寫入 model 的對應位置。因此資料流方向是:
QSpinBox Editor → Model
def setModelData(
self,
editor,
model,
index,
):
"""將 QSpinBox 的資料寫回 Model。"""
model.setData(
index,
editor.value(),
Qt.ItemDataRole.EditRole,
) # 寫入原始值
將 delegate class 以 table.setItemDelegateFor…… 方式指定到 table 內即可
# index: 欄號或列號
table.setItemDelegateForColumn(index, delegate class 物件) #以欄方式設定委派
table.setItemDelegateForRow(index, delegate class 物件) #以列方式設定委派
今天我們了解了 QTableWidget 內資料與顯示的關係,並且設計了一個 QSpinBox delegate class 讓表格內指定欄位只能輸入整數。
明天 day 22 的文章我們會探討其他種類的 delegate class 如何設計,讓套用委派的格子可以支援鍵盤上下左右移動,並且最後設計一個 function 讓我們可以只用一行就完成所有行列的委派