You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何将QTableView修改为类似列表的显示样式?

实现QTableView的分组列表样式

你要的是分组标题+条目的列表样式(比如按首字母分组,每组顶部带背景标题行),QSortFilterProxyModel仅负责排序过滤,要实现这种样式需结合自定义代理/模型,或直接用更适配层级结构的QTreeView,以下是Python实现方案:

方案1:用QTreeView替代(最简方案)

如果数据本身有层级分组,QTreeView + QStandardItemModel天然支持分组结构,无需额外绘制:

from PyQt5.QtWidgets import QApplication, QTreeView, QStandardItemModel, QStandardItem
from PyQt5.QtCore import Qt
import sys

app = QApplication(sys.argv)
model = QStandardItemModel()

# 添加A分组及条目
group_a = QStandardItem("A")
group_a.setFlags(group_a.flags() ^ Qt.ItemIsSelectable)  # 分组标题设为不可选
group_a.appendRow(QStandardItem("Alice"))
group_a.appendRow(QStandardItem("Andy"))
model.appendRow(group_a)

# 添加B分组及条目
group_b = QStandardItem("B")
group_b.setFlags(group_b.flags() ^ Qt.ItemIsSelectable)
group_b.appendRow(QStandardItem("Bob"))
model.appendRow(group_b)

tree = QTreeView()
tree.setModel(model)
tree.expandAll()  # 默认展开所有分组
tree.show()
sys.exit(app.exec_())

方案2:QTableView + 自定义委托+ProxyModel(模拟分组)

若必须用QTableView,需通过自定义QStyledItemDelegate绘制分组标题,配合QSortFilterProxyModel完成排序:

步骤1:准备带分组标记的模型

给原始模型添加自定义角色,标记该行是否为分组标题:

from PyQt5.QtCore import QAbstractTableModel, Qt, QModelIndex
from PyQt5.QtWidgets import QStyledItemDelegate, QTableView, QApplication
from PyQt5.QtGui import QPainter, QColor
import sys

class CustomTableModel(QAbstractTableModel):
    def __init__(self, data):
        super().__init__()
        self._data = data  # 数据格式:[(是否为分组标题, 内容), ...]
        self.header = ["联系人"]

    def rowCount(self, parent=QModelIndex()):
        return len(self._data)

    def columnCount(self, parent=QModelIndex()):
        return len(self.header)

    def data(self, index, role=Qt.DisplayRole):
        if not index.isValid():
            return None
        row = index.row()
        if role == Qt.DisplayRole:
            return self._data[row][1]
        # 自定义角色:标记是否为分组行
        elif role == Qt.UserRole + 1:
            return self._data[row][0]
        return None

    def headerData(self, section, orientation, role=Qt.DisplayRole):
        if orientation == Qt.Horizontal and role == Qt.DisplayRole:
            return self.header[section]
        return None

步骤2:自定义委托绘制分组样式

重写paint方法,区分分组行和普通行的绘制逻辑:

class GroupDelegate(QStyledItemDelegate):
    def paint(self, painter, option, index):
        is_group = index.data(Qt.UserRole + 1)
        if is_group:
            # 绘制分组标题背景
            painter.fillRect(option.rect, QColor("#f0f0f0"))
            # 设置加粗字体
            option.font.setBold(True)
            painter.setFont(option.font)
            painter.drawText(option.rect, Qt.AlignLeft | Qt.AlignVCenter, index.data())
        else:
            # 正常绘制普通条目
            super().paint(painter, option, index)

    def sizeHint(self, option, index):
        hint = super().sizeHint(option, index)
        if index.data(Qt.UserRole + 1):
            hint.setHeight(hint.height() + 8)  # 分组行增高
        return hint

步骤3:结合QSortFilterProxyModel排序

if __name__ == "__main__":
    app = QApplication(sys.argv)
    # 模拟数据
    raw_data = [
        (True, "A"),
        (False, "Alice"),
        (False, "Andy"),
        (True, "B"),
        (False, "Bob"),
        (False, "Ben"),
        (True, "C"),
        (False, "Cindy"),
    ]
    model = CustomTableModel(raw_data)
    
    # 用ProxyModel实现排序
    from PyQt5.QtCore import QSortFilterProxyModel
    proxy = QSortFilterProxyModel()
    proxy.setSourceModel(model)
    proxy.sort(0)  # 按第一列排序

    table = QTableView()
    table.setModel(proxy)
    table.setItemDelegate(GroupDelegate())
    table.setShowGrid(False)  # 隐藏网格线
    table.verticalHeader().hide()  # 隐藏行号
    table.resize(300, 400)
    table.show()
    sys.exit(app.exec_())

关键说明

  • QSortFilterProxyModel仅负责排序/过滤,无法直接实现分组样式,必须配合自定义委托绘制分组行
  • 若数据动态生成,可重写ProxyModel的lessThan方法,自定义排序逻辑确保分组与对应条目关联
  • 优先推荐QTreeView,原生支持层级结构,无需额外绘制逻辑

内容的提问来源于stack exchange,提问作者IKNCODE

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.24 21:18:17