如何重写QItemDelegate实现QTableWidget单元格绘制方块与文本
QItemDelegate自定义绘制实现单元格内方框+多行文本方案
问题背景
我正在尝试重写QItemDelegate,实现在每个单元格内绘制带边框的方框以及多行文本。目前我已经掌握通过QTableWidget.setCellWidget(row, column, QWidget())实现该效果的方法,但我认为改用委托手动绘制图形和文本的方案,相比同时在界面加载近300个独立控件的实现,运行效率和可扩展性都会更高——毕竟我并没有用到这些控件的大部分功能,仅用到了它们的绘制表现。
当前使用setCellWidget的实现代码如下:
class DesignTable(QTableWidget): def __init__(self, parent=None): super().__init__(parent) self.setRowCount(15) self.setColumnCount(6) self.horizontalHeader().hide() self.verticalHeader().hide() for r in range(15): for c in range(6): self.setCellWidget(r, c, MyTableCell()) self.resizeColumnsToContents() self.resizeRowsToContents() class MyTableCell(QWidget): def __init__(self, parent=None): super().__init__(parent) self.layout = QGridLayout() self.setStyleSheet('QFrame {border: 3px solid black;}') self.setLayout(self.layout) box = QLabel() box.setMinimumSize(50, 50) area_1 = QLineEdit() area_1.setText('line of text written in code') area_2 = QTextEdit() self.layout.addWidget(box, 0, 0) self.layout.addWidget(area_1, 0, 1) self.layout.addWidget(area_2, 1, 1) if __name__ == '__main__': q = QApplication() w = QMainWindow(None) d = DesignTable() w.setCentralWidget(d) w.show() sys.exit(q.exec_())
运行上述代码得到的基础界面效果如下:
目前进展
我目前还没写出可用的自定义委托实现,已经花了数天研究QStyledItemDelegate的用法,参考Qt官方StarDelegate示例写出的测试代码如下:
class TestBench(QStyledItemDelegate): def __init__(self, parent=None): super().__init__(parent) def paint(self, painter, option, index): if index.row() == 1: if option.state & QStyle.State_Selected: painter.fillRect(option.rect, painter.brush()) title = index.data(TitleRole) super().paint(painter, option, index)
目前我只理解到:行号为1时可以应用自定义绘制逻辑,其余情况走默认绘制流程。我现在的困惑是如何修改代码,通过委托实现在表格项中绘制方框和文本,目前很难找到清晰说明自定义委托支持哪些自定义修改的Python侧入门资料。
核心诉求
希望获得一份基础示例代码,演示如何结合Qt的模型数据机制使用QItemDelegate,在单元格中绘制一个正方形和两行文本;如果无法提供完整代码,给出大致实现方向也可以。
实现方案
优先选择继承QStyledItemDelegate而非QItemDelegate,前者默认支持Qt样式表渲染,自定义绘制的逻辑全部在paint方法中实现即可,不需要额外创建控件。核心实现逻辑分三步:
- 先处理单元格基础状态(选中、悬停、默认背景)
- 按照布局需求拆分
option.rect的可用区域,分别计算方框、两行文本的绘制矩形范围 - 调用QPainter的原生绘制接口完成边框、填充、文本的绘制,文本绘制使用
drawText即可自动支持换行。
可直接运行的完整示例代码如下:
import sys from PyQt5.QtCore import Qt, QRect, QSize from PyQt5.QtGui import QPainter, QPen, QBrush, QFont from PyQt5.QtWidgets import (QApplication, QMainWindow, QTableWidget, QStyledItemDelegate, QStyle, QTableWidgetItem) # 自定义角色,用于从模型取不同字段的数据 FirstLineTextRole = Qt.UserRole + 1 SecondLineTextRole = Qt.UserRole + 2 class CustomCellDelegate(QStyledItemDelegate): def __init__(self, parent=None): super().__init__(parent) # 固定方框尺寸,和原实现保持一致 self.box_size = 50 self.border_width = 3 self.text_padding = 5 def paint(self, painter, option, index): # 绘制前保存painter状态,避免污染其他单元格绘制 painter.save() # 先绘制单元格背景和选中状态 if option.state & QStyle.State_Selected: painter.fillRect(option.rect, option.palette.highlight()) text_pen_color = option.palette.highlightedText().color() else: painter.fillRect(option.rect, option.palette.base()) text_pen_color = option.palette.text().color() # 拆分绘制区域 cell_rect = option.rect # 计算左侧方框位置,垂直居中 box_rect = QRect( cell_rect.left() + self.text_padding, cell_rect.top() + (cell_rect.height() - self.box_size)//2, self.box_size, self.box_size ) # 计算右侧文本总区域 text_area_rect = QRect( box_rect.right() + self.text_padding*2, cell_rect.top() + self.text_padding, cell_rect.right() - box_rect.right() - self.text_padding*3, cell_rect.height() - self.text_padding*2 ) # 拆分右侧为上下两行文本区域 first_line_rect = QRect( text_area_rect.left(), text_area_rect.top(), text_area_rect.width(), text_area_rect.height()//2 ) second_line_rect = QRect( text_area_rect.left(), first_line_rect.bottom(), text_area_rect.width(), text_area_rect.height()//2 ) # 绘制黑色边框方框 painter.setPen(QPen(Qt.black, self.border_width)) painter.setBrush(QBrush(Qt.white)) painter.drawRect(box_rect) # 绘制文本 painter.setPen(QPen(text_pen_color)) # 从模型读取对应字段数据 first_text = index.data(FirstLineTextRole) or "第一行默认文本" second_text = index.data(SecondLineTextRole) or "第二行默认文本,支持自动换行显示,内容过长时会自动折行不会超出单元格范围" # 第一行文本用加粗字体 bold_font = QFont() bold_font.setBold(True) painter.setFont(bold_font) painter.drawText(first_line_rect, Qt.AlignLeft|Qt.AlignVCenter|Qt.TextWordWrap, first_text) # 第二行文本用常规字体 normal_font = QFont() painter.setFont(normal_font) painter.drawText(second_line_rect, Qt.AlignLeft|Qt.AlignVCenter|Qt.TextWordWrap, second_text) # 恢复painter状态 painter.restore() def sizeHint(self, option, index): # 固定单元格最小尺寸,和原setCellWidget效果一致 return QSize(220, 70) class DesignTable(QTableWidget): def __init__(self, parent=None): super().__init__(parent) self.setRowCount(15) self.setColumnCount(6) self.horizontalHeader().hide() self.verticalHeader().hide() # 给表格设置自定义委托 delegate = CustomCellDelegate(self) self.setItemDelegate(delegate) # 给单元格填充测试数据 for r in range(15): for c in range(6): item = QTableWidgetItem() self.setItem(r,c,item) # 用自定义角色存储不同字段数据 item.setData(FirstLineTextRole, f"第{r}行{c}列 第一行标题") item.setData(SecondLineTextRole, f"这是第二行多行文本内容,单元格复用机制下哪怕几千行也不会卡顿,性能远高于setCellWidget方案") self.resizeColumnsToContents() self.resizeRowsToContents() if __name__ == '__main__': q = QApplication(sys.argv) w = QMainWindow(None) d = DesignTable() w.setCentralWidget(d) w.show() sys.exit(q.exec_())
关键注意点
- 全量自定义绘制时不需要调用
super().paint(),跳过默认实现可以避免重复绘制的问题 - 如果需要支持单元格编辑功能,额外重写委托的
createEditor、setEditorData、setModelData三个方法即可,纯展示场景不需要实现 - 委托基于表格的单元格复用机制,同时间只会创建和可见单元格数量相当的绘制上下文,哪怕上万行数据性能也不会有明显下降,完全替代几百个独立控件的方案没有问题。
内容的提问来源于stack exchange,提问作者Alexander WJ
相关产品推荐
相关产品推荐

