iT邦幫忙

2026 iThome 鐵人賽

DAY 21
0
Software Development

用 Python 打造最順手的良率統計工具:半導體工程師的模組化開發之道系列 第 21 篇

Day 21:別問 A 答 B - 使用 QStyledItemDelegate 限縮使用者在表格中的輸入內容

  • 分享至 

  • xImage
  •  

本篇目的:了解 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

https://ithelp.ithome.com.tw/upload/images/20261004/20182319V4NiUqoAia.png

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 三者的關係示意圖如下。

https://ithelp.ithome.com.tw/upload/images/20261004/20182319pO2SAWONgR.png

參考資料: https://doc.qt.io/qt-6/model-view-programming.html

應用例:一個 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,因此不可以改引數的名稱

從 model 寫入資料到物件的設定:setEditorData()

setEditorData() 的工作是將 Model 目前儲存的資料載入 QSpinBox。因此資料流方向是:

Model → QSpinBox Editor

首先先對 model 取值,使用 index.data(取值規則) 就可以直接對對應 model 取值了。取值的優先順位如下

  1. 取得 model 內的原始值 (EditRole)

  2. 取得顯示值 (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)

將編輯完成資料寫入到 model 的設定: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 套用到 QTableWidget 中

將 delegate class 以 table.setItemDelegateFor…… 方式指定到 table 內即可

# index: 欄號或列號
table.setItemDelegateForColumn(index, delegate class 物件) #以欄方式設定委派
table.setItemDelegateForRow(index, delegate class 物件) #以列方式設定委派

To be continued →

今天我們了解了 QTableWidget 內資料與顯示的關係,並且設計了一個 QSpinBox delegate class 讓表格內指定欄位只能輸入整數。

明天 day 22 的文章我們會探討其他種類的 delegate class 如何設計,讓套用委派的格子可以支援鍵盤上下左右移動,並且最後設計一個 function 讓我們可以只用一行就完成所有行列的委派


上一篇
Day 20:複製貼上才不簡單 - 掌控 QTableWidget 的鍵盤事件來快速貼上設定
系列文
用 Python 打造最順手的良率統計工具:半導體工程師的模組化開發之道 共 21 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言