第 14 章

列表模型与视图

本章共 8 个小节 · PySide6 Basic Tutorial
本章要点:
  1. 常见的列表模型类
  2. QStringListModel 和 QStandardItemModel
  3. QFileSystemModel
  4. QListWidget
  5. QTableWidget
  6. QTreeWidget
14.1

模型的抽象基类

列表模型(ItemModel)是应用数据与界面视图之间的桥梁,掌控着数据的呈现方式。列表模型采用二维表结构,可通过行号、列号来索引数据,如图14-1所示。

行、列编号从0开始,例如图14-1中B的行号为1,列号为2。由于每个数据项都包含父节点,因此对于顶层数据来说,它也包含一个隐藏的根节点。

如果数据是一维列表(如array、list),那么它的模型布局只有一列,每个元素为一行,如图14-2所示。

另一种比较复杂的模型是树形结构,数据项存在“父子”关系,如图14-3所示。

图 14-1、图 14-2、图 14-3
图 14-1 二维表结构 图 14-2 一维数据模型 图 14-3 树形结构
图 14-1 二维表结构
图 14-2 一维数据模型
图 14-3 树形结构

A所在的行号为0,列号为0。由于B是A的子节点,它的编号要重新计算,因此B的行号和列号也是0。

QAbstractItemModel类是列表模型的抽象基类。作为公共接口,不能直接实例化,开发人员需要创建 QAbstractItemModel 的派生类,然后才能与视图组件一起使用。

对于只读模型(仅获取和显示数据,不可编辑),只要实现以下方法即可。

  1. columnCount:返回某个父节点所包含的列数。
  1. rowCount:返回某个父节点下包含的行数。
  1. parent:获取给定索引的父节点,返回父节点的索引。
  1. index:获取列表项的索引。
  1. data:返回给定索引处的基础数据(原始数据)。

如果要视图支持编辑功能,并且希望列表模型能够将已修改的数据更新到原始数据,那就要实现以下方法。

  1. setData:用新值更新原始数据。注意,当数据更新后,需要发出dataChanged信号。
  1. flags:返回的值中必须包含ItemIsEditable(Qt.ItemFlag枚举定义的成员)。

14.1.1 ItemDataRole

Qt.ItemDataRole枚举定义了一组成员,描述了数据项在列表模型中的用途,即视图组件在绘制各部分界面元素时所需要的数据类型,可称为“数据角色”。ItemDataRole枚举定义的成员如下。

  1. DisplayRole:显示在视图中的文本,字符串类型。
  1. DecorationRole:装饰元素,例如列表项前面的小图标,其类型可以是QIcon,也可以是QPixmap和 QColor。
  1. EditRole:数据处于编辑状态,例如在文本框中修改内容。
  1. ToolTipRole:返回字符串数据,显示在工具提示中。
  1. StatusTipRole:返回的文本将显示在状态栏上。
  1. WhatsThisRole:文本将显示在“这是什么”帮助信息中。
  1. SizeHintRole:返回QSize类型的数据,表示列表项要占用空间的大小(宽度和高度)。
  1. FontRole:应返回QFont类型的对象,用来绘制视图中的内容。
  1. TextAlignmentRole:返回Qt.AlignmentFlag枚举的值,用于设置文本的对齐方式。
  1. BackgroundRole:返回的数据类型为 QBrush,用来绘制列表项的背景。
  1. ForegroundRole:也是返回QBrush类型的数据,表示文本的颜色。
  1. CheckStateRole:返回CheckState枚举的值,用于指定复选框的状态。
  1. InitialSortOrderRole:列表头的排序方式,返回 SortOrder枚举的值,即升序或降序。
  1. AccessibleTextRole:返回字符串类型的数据,用于设置辅助提示的主文本,例如“屏幕朗读”功能。
  1. AccessibleDescriptionRole:返回辅助提示的描述信息。

实现 QAbstractItemModel 类的 data 和 setData 方法都需要对 ItemDataRole 的值进行分析,从而返回或设置对应类型的值。

14.1.2 示例:整数列表模型

本示例将自定义一个提供整数类型的列表模型。该模型所使用的原始数据是一个list对象,其元素结构为单个维度—只有一列,每行表示一个元素。然后通过该模型将整数列表呈现在QListView组件中。

具体实现步骤如下。

  1. 定义 CustItemModel类,它派生自 QAbstractItemModel。
class CustItemModel(QAbstractItemModel):
  1. 为CustItemModel类实现__init__方法。该方法有两个重载,包含data参数的可以直接指定原始数据(内部存储字段是_myData)。
def __init__(self, parent: QObject = None): super().__init__(parent) #引用数据源 self._myData = [] def __init__(self, data: list, parent: QObject = None): super().__init__(parent) self._myData = data
  1. 如果在构造类实例时未指定原始数据,可以通过 setSourceData 方法设置。其他代码可以通过sourceData方法获取原始数据。
def sourceData(self): return self._myData def setSourceData(self, data: list): self._myData = data
  1. 重写parent方法,返回指定索引的父级。
def parent(self, index: QModelIndex) -> QModelIndex: #此模型没有真正的父级,应返回无效的索 return QModelIndex()

本示例所处理的数据只有一个层级,数据项之间不存在父子关系。因此,不需要返回特定的索引,而应返回无效的索引,调用QModelIndex类的默认构造函数即可。无效索引的行号和列号都是-1。

  1. 重写index方法,返回指定行、列处的索引。
def index(self, row: int, column: int, parent: QModelIndex =QModelIndex ()) ->QModelIndex: return self.createIndex(row, column)

外部代码不能直接访问 QModelIndex 类的成员,但可以通过 createIndex方法来创建索引。本示例不需要考虑父级索引。

  1. 重写rowCount方法,返回数据的行数。在本示例中,行数就是列表的元素个数。
def rowCount(self, parent: QModelIndex = QModelIndex()) -> int: #由于该模型表示的是一维列表,列表项没有子级 #因此,如果parent是有效索引,就要返回0 if parent.isValid(): return 0 #返回数据源的元素个数 return len(self._myData)
  1. 重写columnCount方法,返回数据包含的列数。本示例所处理的数据是普通列表,只有一列。
def columnCount(self, parent: QModelIndex = QModelIndex()) -> int: if parent.isValid(): return 0 #列表始终只有一列 return 1
  1. 重写data方法,返回特定索引处的数据项。
def data(self, index: QModelIndex, role: int = Qt.ItemDataRole. DisplayRole) -> Any: if role == Qt.ItemDataRole.DisplayRole: #只需要获取行号 rowIndex = index.row() return self._myData[rowIndex] #返回默认值 return None

本示例只实现显示数据,未实现编辑(修改)数据,因此不需要重写setData方法。无论是获取还是设置数据项的值,都应注意role参数,毕竟所返回的数据类型不适用所有的数据角色。此处所返回的数据是用于呈现在用户界面上的,所以只有当role参数是DisplayRole时才返回数据,其他情况一律返回 None。

  1. 实例化一个视图组件,本示例使用的是QListView。该组件适用于简单的列表项。
lv = QListView()
  1. 初始化原始数据(一个整数序列)。
intList = [105, 17, 915, 400, 3020, 840]
  1. 实例化自定义模型类。
model = CustItemModel(intList, lv)

调用CustItemModel类的构造函数时传递原始数据。如果未传递原始数据,可以稍后调用setSourceData方法设置,例如:

model.setSourceData(intList)
  1. 调用视图组件的 setModel方法关联列表模型。
lv.setModel(model)

示例的运行效果如图14-4所示。

图 14-4
图14-4 呈现整数列表
图14-4 呈现整数列表

14.1.3 推荐的基础模型

尽管直接从 QAbstractItemModel类派生可以实现自定义模型,但由于该类过于抽象,派生时需要重写的成员较多,开发效率不高。如果原始数据的结构比较明确,建议继承QAbstractListModel类或者QAbstractTableModel 类。

QAbstractListModel表示一维列表模型,如一维数组。其处理的数据由多行单列构成。因此,只需要重写 rowCount 方法即可,不需要重写 columnCount 方法。QabstractTableModel 模型则是面向二维表结构的数据,包含多行多列。由于二维表由行、列组成,因此继承 QabstractTableModel类时需要实现rowCount 和 columnCount 方法。

从 QAbstractListModel、QAbstractTableModel 类派生时都要重写 data方法以返回指定的数据项,或重写 setData方法以支持数据编辑。

14.1.4 示例:学生表

本示例将创建一个二维表模型,视图结构为5行4列。通过该模型和QTableView组件显示一组学生信息。具体实现步骤如下。

  1. 构建原始数据,稍后用于填充模型。
init_data = [ ["小许", 19,"13205445202","[email protected]"], ["小赵", 21, "18927274113", "[email protected]"], ["小马",25,"13609210500","[email protected]"], ["小易",27,"15530048951","[email protected]"], ["小何",30,"47300125336","[email protected]"]

上述数据样本是一个嵌套列表。列表中的顶层元素为学生记录,一条学生记录包含4个元素,代表姓名、年龄、联系电话和电子邮箱。

  1. 定义模型类 StudentItemModel,基类是 QabstractTableModel。
class StudentItemModel(QAbstractTableModel):
  1. 在__init__方法中传递基础数据(initData参数)。
def __init__(self, initData, parent: QObject = None): super().__init__(parent) self._theData = initData
  1. 重写 rowCount 方法,返回数据表的行数。
def rowCount(self, parent: QModelIndex = QModelIndex()) -> int: if parent.isValid(): return 0 return len(self._theData)

二维表结构中的行一般没有父/子节点,所以如果parent参数是有效索引,应当返回0,如果索引是无效值,表明正在访问的是顶层数据,此时应返回原始数据的元素个数。

  1. 重写columnCount方法,返回二维表的列数,本示例是固定值4。
def columnCount(self, parent: QModelIndex = QModelIndex()) -> int: if parent.isValid(): return 0 return 4
  1. 重写data方法,返回指定索引处的数据项。
def data(self, index: QModelIndex, role: int = Qt.ItemDataRole. DisplayRole) -> Any: if role == Qt.ItemDataRole.DisplayRole: #获取行号和列号 rowIdx = index.row() colIdx = index.column() #返回数据项 return self._theData[rowIdx][colIdx] #其他情况返回None return None

处理 data 方法时一定要注意 role参数,当其值为 DisplayRole时才返回用于视图呈现的数据。

  1. 重写headerData方法,返回显示在视图中的列标题。
def headerData(self, section: int, orientation: Qt.Orientation, role: int = Qt.ItemDataRole.DisplayRole) -> Any: #列表头用于界面显示 if role == Qt.ItemDataRole.DisplayRole: #如果是水平方向,返回列标题 if orientation == Qt.Orientation.Horizontal: if section == 0: return "姓名" if section == 1: return "年龄" if section == 2: return "联系电话" if section == 3: return"电子邮箱" #如果是垂直方向,返回行编号 if orientation == Qt.Orientation.Vertical: return section + 1 #其他情况返回 None return None

orientation参数表示的是排列方向,水平方向代表的是列标题,垂直方向则是行标题。在上述代码中,列标题为“姓名”“年龄”等,行标题显示行编号。

section参数表示从0开始的编号。如果方向是水平,那么 section=0表示第一列,section=2表示第三列;如果方向是垂直,那么section=0是第一行,section=1是第二行。由于 section是从0开始计算的,为了兼顾人们的阅读习惯,返回行号时使用section+1。这样可以确保第一行的标头显示“1”,第二行的标头显示“2”。

  1. 二维表数据应使用 QTableView组件来显示。下面代码初始化 QTableView 组件和 StudentItemModel模型。
view = QTableView() view.setWindowTitle("学生信息") #创建模型实例 model = StudentItemModel(init data, view) #为视图组件设置模型 view.setModel(model) #显示组件 view.show()

示例运行效果如图14-5所示。

图 14-5
图 14-5 表格视图
图 14-5 表格视图

14.1.5 示例:突出显示销量高于平均值的行

本示例模拟一个月度销售表,在视图呈现时,如果某行数据的销量高于平均值,该行文本就显示为蓝色。

在重写 data方法时,当列表项需要绘制显示文本时,role参数的值是 DisplayRole;当需要根据数据来获取绘制文本的颜色时,role参数的值为ForegroundRole。将当前记录的销量与整体销量的平均值比较,若销量大于或等于平均值,就返回QColor("blue")。

具体实现步骤如下。

  1. 准备示例数据。
sourceData = [ ['F302011', '热缩管', 1823], ['L281055', '元器件', 890], ['Q490084', '面包板', 442], ['T348192', '二极管', 2058], ['E419001', '导线', 1679], ['G780357', '三极管', 1315], ['K853692', 'LED灯珠', 4089], ['U160928', '热敏电阻', 2146] ]
  1. 调用 mean 函数求得销量的平均值。
evg = mean([x[2] for x in sourceData])

示例数据是嵌套的列表类型,子项也是列表。其中,子项的第三个元素为销量(索引是2)。

  1. 定义项目模型类 CustItemModel,派生自QabstractTableModel类。
class CustItemModel(QAbstractTableModel):
  1. 在__init__方法中通过 data参数传递示例数据。
def __init__(self, data, parent: QWidget = None): super().__init__(parent) self._data = data
  1. 重写 rowCount 方法,返回数据表的行数。
def rowCount(self, parent = QModelIndex) -> int: if parent.isValid(): return 0 return len(self._data)
  1. 重写 columnCount 方法,返回数据表的列数。
def columnCount(self, parent = QModelIndex()) -> int: if parent.isValid(): return 0 return len(self._data[0])
  1. 重写headerData方法,返回列标题和行序号。
def headerData(self, section: int, orientation: Qt.Orientation, role: int = Qt.ItemDataRole.DisplayRole) -> Any: if role == Qt.ItemDataRole.DisplayRole: #水平方向返回列标题 if orientation == Qt.Orientation.Horizontal: if section == 0: return "分类码" if section == 1: return "品名" if section == 2: return "销量" #否则返回行号 return section + 1 return None
  1. 重写data方法,返回指定索引处的数据。
def data(self, index: QModelIndex, role: int = Qt.ItemDataRole. DisplayRole) -> Any: #如果数据用于视图显示 if role == Qt.ItemDataRole.DisplayRole: #行索引 rowIndex = index.row() #列索引 colIndex = index.column() #返回数据 return self._data[rowIndex][colIndex] #如果数据用于文本颜色 if role == Qt.ItemDataRole.ForegroundRole: #获取销售量 q = self._data[index.row()][2] #如果销量在平均数之上,显示为蓝色 if q >= evg: return QColor("blue") #其他情况返回None return None
  1. 实例化 QTableView 组件。
table = QTableView() table.setWindowTitle("月度销售额")
  1. 为QTableView 组件设置模型。
#实例化模型 model = CustItemModel(sourceData, table) #为视图设置模型 table.setModel(model) #显示组件 table.show()

运行示例程序,效果如图14-6所示。

图 14-6
图 14-6 模拟销售表
图 14-6 模拟销售表
14.2

QStringListModel

QStringListModel派生自QAbstractListModel类,是 Qt 提供的便捷模型类。该模型以字符串列表为基础数据。该类可直接使用,开发人员不需要派生新类。

在实例化 QStringListModel类时,可以通过构造函数参数传递字符串列表,例如:

#字符串列表 strs = ["夏", "商", "周", "秦", "汉"] #实例化模型并传递字符串列表 myModel = QStringListModel(strs)

或者先创建 QStringListModel 实例,再通过 setStringList 方法设置字符串列表,例如:

#字符串列表 strs = ["夏", "商", "周", "秦", "汉"] #实例化模型 myModel = QStringListModel() #设置基础数据 myModel.setStringList(strs)

QStringListModel模型类实例化后可以直接用于视图组件。演示代码如下:

#字符串列表 strList = ["桃子", "梨子", "李子", "柚子", "瓜子"] #实例化模型 model = QStringListModel() #设置基础数据 model.setStringList(strList) ..... listview = QListView() #设置模型 listview.setModel(model) #显示视图组件 listview.show()

上述代码的执行结果如图14-7所示。

图 14-7
图 14-7 字符串列表视图
图 14-7 字符串列表视图
14.3

QStandardItemModel

QStandardItemModel也是Qt提供的便捷模型类。该类可通用于各种数据源,简单一维列表、二维列表、树形列表等数据模型均适用。

列表项由 QStandardItem类表示。每个 QStandarItem 对象都拥有一个二维表结构的子项列表,并且支持构建多层次结构的数据模型。调用setChild方法可以将子项设置到指定的行和列中。需要注意的是,只有QTreeView组件才支持树形结构的数据,QTableView、QListView等组件不会显示子项。

要为 QStandarItem 设置数据可以调用 setData 方法,如果数据是字符串类型,调用 setText 方法会更简单。setText方法内部调用了setData方法。

QStandardItemModel 类通过 item 和 setItem 方法在指定行和列获取/设置 QStandardItem 对象。如果模型用于QTableView组件,还可以用 setHorizontalHeaderLabels方法设置列标题,用 setVerticalHeaderLabels方法设置行标题。或者使用 setHorizontalHeaderItem 和 setVerticalHeaderItem 方法,这两个方法可以针对某一行或某一列来设置标题。

14.3.1 示例:员工信息表

本示例将演示 QStandardItemModel与QStandardItem类的基本用法。示例将构建一个4列3行的员工信息表。具体步骤如下。

  1. 实例化 QTableView 和 QStandardItemModel类,代码如下:
#创建视图组件实例 view = QTableView() view.setWindowTitle("员工信息") #创建模型实例 stdModel = QStandardItemModel(view)
  1. 调用 setHorizontalHeaderLabels 方法设置列标题。
stdModel.setHorizontalHeaderLabels(["员工ID","姓名","年龄","部门"])
  1. 向模型添加数据。
# ------ 第一行 ------ stdModel.setItem( 0, 0, QStandardItem("1213") ) stdModel.setItem( 0, 1, QStandardItem("小张") ) stdModel.setItem( 0, 2, QStandardItem("27") ) stdModel.setItem( 0, 3, QStandardItem("法务部") ) # ------ 第二行 ------ stdModel.setItem( 1, 0, QStandardItem("1456") ) stdModel.setItem( 1, 1, QStandardItem("小李") ) stdModel.setItem( 1, 2, QStandardItem("30") ) stdModel.setItem( 1, 3, QStandardItem("财务部") ) # ------ 第三行 ------ stdModel.setItem( 2, 0, QStandardItem("6825") ) stdModel.setItem( 2, 1, QStandardItem("小陈") ) stdModel.setItem( 2, 2, QStandardItem("35") ) stdModel.setItem( 2, 3, QStandardItem("人力资源部") )
  1. 将模型对象设置到QTableView组件中,显示数据。
view.setModel(stdModel) #显示视图 view.show()

示例程序的运行效果如图14-8所示。

图 14-8
图 14-8员工信息视图
图 14-8员工信息视图

14.3.2 示例:显示图标

本示例将演示在标准列表模型中显示图标的方法。QStandardItem类存在可接受 QIcon 对象的构造函数:

def __init__(icon: Union[QIcon, QPixmap], text: str)

即在实例化 QStandardItem类时可以将已加载的图标资源传递给icon参数。另外,如果在实例化时未传递图标资源,也可以通过以下方法设置:

def setIcon(icon: Union[QIcon, QPixmap])

本示例将使用一维列表,向模型添加4个子项,每个子项都带有小图标。核心代码如下:

#初始化视图组件 theView = QListView() #创建标准模型实例 model = QStandardItemModel(theView) #一级列表只有一列 model.setColumnCount(1) #加载图标 icon1 = QIcon("01.png") icon2 = QIcon("02.png") icon3 = QIcon("03.png") icon4 = QIcon("04.png") #创建标准项 item1 = QStandardItem(icon1, "object 1") item2 = QStandardItem(icon2, "object 2") item3 = QStandardItem(icon3, "object 3") item4 = QStandardItem(icon4, "object 4") #向模型添加数据项 model.appendRow(item1) model.appendRow(item2) model.appendRow(item3) model.appendRow(item4) #为视图组件设置项目模型 theView.setModel(model) #显示组件 theView.show()

由于示例数据只有一列,可以调用setColumnCount方法设置总列数为1。列表视图的呈现效果如图 14-9所示。

图 14-9
图14-9 带小图标的列表项
图14-9 带小图标的列表项

14.3.3 示例:树形视图

要构造树形视图,QStandardItem对象需要调用 setChild方法添加子节点。该方法有两个重载:

def setChild(row: int, column: int, item: QStandardItem) def setChild(row: int, item: QStandardItem)

row、column参数指定在何处放置子节点(行号和列号),item参数是子节点的数据项。对于简单的树形结构,可以认为子节点均放在第一列,每个子节点占一行。

以下是本示例所构建的数据项。

#顶层节点 top1 = QStandardItem("文件") top2 = QStandardItem("编辑") top3 = QStandardItem("格式") #第一个顶层节点的子节点 topl.setChild(0, QStandardItem("打开")) top1.setChild(1, QStandardItem("新建")) top1.setChild(2, QStandardItem("关闭")) #第二个顶层节点的子节点 top2.setChild(0, QStandardItem("复制")) top2.setChild(1, QStandardItem("剪切")) top2.setChild(2, QStandardItem("粘贴")) top2.setChild(3, QStandardItem("查找")) #第三个顶层节点的子节点 top3.setChild(0,QStandardItem("增加缩进量")) top3.setChild(1,QStandardItem("减少缩进量")) #第二层子节点 subitem = QStandardItem("对齐") #添加第三层子节点 subitem.setChild(0, QStandardItem("左")) subitem.setChild(1, QStandardItem("中")) subitem.setChild(2, QStandardItem("右")) top3.setChild(2, subitem)

要显示树形结构的数据,需要用QTreeView组件。设置标准模型的代码如下:

treeview = QTreeView() #隐藏标头 treeview.setHeaderHidden(True) #实例化标准模型 model = QStandardItemModel(treeview) #添加数据项 model.appendRow(top1) model.appendRow(top2) model.appendRow(top3)

top1、top2 和 top3 属性顶层数据项,直接添加到 QStandardItemModel 对象中即可。由于本示例未设置列或行的标题,所以应当调用setHeaderHidden(True)来隐藏标头区域。呈现结果如图14-10所示。

图 14-10
图 14-10 树形视图
图 14-10 树形视图

14.3.4 示例:多列树形视图

本示例将实现具有多个列的树形数据表。用到的仍然是 QTreeView组件和 QStandardItemModel类。

一般树形视图的子节点只有一列,而实现多列视图的原理就是在调用setChild方法时同时指定行号和列号。不过,QTreeView组件只有第一列才支持树形结构,因此在建立列表项的层级关系时仅需考虑第一列即可。

本示例将构建一个值班人员表。顶层节点是工作人员所在的分组,子节点才是值班信息。具体实现步骤如下。

  1. QStandardItem类默认是允许编辑、check等操作。本示例所构建的数据模型是只读的,不允许修改。因此,从QStandardItem派生出MyStandarItem类,在初始化过程中默认将编辑、拖放等功能禁用。这样做可避免每次创建标准项都要手动禁用一次。
class MyStandarItem(QStandardItem): def __init__(self): super().__init__() self._set_init() def __init__(self, text: str): super().__init__(text) self._set_init() def_set_init(self): #禁用编辑 self.setEditable(False) #禁用拖放 self.setDragEnabled(False) self.setDropEnabled(False) #禁用check 功能 self.setCheckable(False)
  1. 创建标准列表模型实例。
model = QStandardItemModel() #设置列标题 model.setHorizontalHeaderLabels(["姓名","性别","值班时间"])
  1. 准备示例数据。
#添加列表项 group1 = MyStandarItem("新闻组") group1.setChild(0, 0, MyStandarItem("小徐")) groupl.setChild(0, 1, MyStandarItem("男")) group1.setChild(0, 2, MyStandarItem("上午")) group1.setChild(1, 0, MyStandarItem("小钱")) groupl.setChild(1, 1, MyStandarItem("男")) groupl.setChild(1, 2, MyStandarItem("下午")) group2 = MyStandarItem("漫画组") group2.setChild(0, 0, MyStandarItem("小罗")) group2.setChild(0, 1, MyStandarItem("女")) group2.setChild(0, 2, MyStandarItem("上午")) group2.setChild(1, 0, MyStandarItem("小高")) group2.setChild(1, 1, MyStandarItem("男")) group2.setChild(1, 2, MyStandarItem("晚上")) group3 = MyStandarItem("广播组") group3.setChild(0, 0, MyStandarItem("小曾")) group3.setChild(0, 1, MyStandarItem("男")) group3.setChild(0, 2, MyStandarItem("下午")) group3.setChild(1, 0, MyStandarItem("小郭")) group3.setChild(1, 1, MyStandarItem("女")) group3.setChild(1, 2, MyStandarItem("中午")) group3.setChild(2, 0, MyStandarItem("小付")) group3.setChild(2, 1, MyStandarItem("男")) group3.setChild(2, 2, MyStandarItem("下午")) model.setItem(0, 0, groupl) model.setItem(1, 0, group2) model.setItem(2, 0, group3)
  1. 初始化 QTreeView 组件。
treeview = QTreeView() treeview.setWindowTitle("值班安排")
  1. 为视图组件设置列表模型。
treeview.setModel(model)
  1. 显示组件。
图 14-11
图 14-11 分组的值班人员表
图 14-11 分组的值班人员表
14.4

QFileSystemModel

QFileSystemModel类是 Qt提供的列表模型,支持对本地文件系统的访问。如 QTreeView等视图组件可以借助该类显示文件和目录结构。另外,QFileSystemModel类还提供了以下方法,可以方便地进行一些常用操作。

  1. mkdir:创建新目录。
  1. rmdir:删除目录。
  1. remove:删除文件。
  1. setNameFilters:过滤文件名。
  1. setFilter:为目录设置过滤方式。

QFileSystemModel 模型类在实例化后,需要调用 setRootPath方法设置一个根目录。调用该方法并不意味着模型只提取根目录下的内容,而是为了激活后台线程上的文件扫描操作。如果不调用setRootPath方法,QFileSystemModel模型中将读取不到目录和文件信息。因此,调用 setRootPath 方法时也可以传递空白字符串,不会影响目录和文件信息的提取。

14.4.1 示例:显示目录和文件

QFileSystemModel 类通常与 QTreeView 组件一起使用,实现以树形结构显示目录和文件列表。本示例将显示C:\Windows目录下的目录和文件。核心代码如下:

#实例化视图组件 treeview = QTreeView() treeview.setWindowTitle("目录和文件") #实例化模型类 model = QFileSystemModel() model.setRootPath("") #为视图组件设置模型 treeview.setModel(model) #设置要显示的根目录 treeview.setRootIndex(model.index("C:\\Windows")) #显示窗口 treeview.show()

调用 QTreeView 组件的 setRootIndex方法后才会显示指定路径下的内容。若不调用 setRootIndex方法,默认会显示分区/磁盘列表(如C:、D:)。setRootIndex方法的参数要求传递列表项索引,可以通过QFileSystemModel类的 index方法获取。

上述代码的运行结果如图14-12所示。

图 14-12
图 14-12 目录与文件列表
图 14-12 目录与文件列表

14.4.2 示例:使用过滤器

调用下面的方法成员,可以为QFileSystemModel设置文件名过滤器(也称“筛选器”):

def setNameFilters(filters: Sequence[str])

filters参数是字符串序列,表明该方法可以设置多个过滤条件,例如:

setNameFilters(["*.doc", "*.vob"])

上述代码表示只有扩展名为.doc和.vob的文件有效。

本示例的窗口顶部有一个QLineEdit组件,可通过键盘输入过滤条件。单击“确定”按钮后应用过滤器。QTreeView组件用于显示文件列表。

自定义窗口DemoWindow类的完整代码如下:

class DemoWindow(QWidget): def __init__(self): super().__init__() #窗口顶部布局 _topLayout = QHBoxLayout() self._txtFilter = QLineEdit(self) _topLayout.addWidget(self._txtFilter, 1) btn = QPushButton("确定", self) _topLayout.addWidget(_btn) #连接 clicked 信号 _btn.clicked.connect(self.onClicked) #实例化模型类 self._model = QFileSystemModel(self) self._model.setRootPath("E:\\") #实例化视图组件 self._view = QTreeView(self) #设置模型引用 self._view.setModel(self._model) #设置根节点 self.view.setRootIndex(self.model.index("E:\\test")) #窗口整体布局 _layout = QVBoxLayout() self.setLayout(_layout) #将上述各对象添加到布局 layout.addLayout(_topLayout) _layout.addWidget(self._view, 1) def onClicked(self): #获取输入的过滤关键字 s = self._txtFilter.text() if len(s) == 0: return #拆分字符串 filters = s.split(';') #应用过滤 self._model.setNameFilters(filters)

上述代码设定要显示文件的目录为E:\test(读者可以根据实际情况改为其他路径),必须保证此目录已存在,且里面有相应的文件。如果目录不存在,QTreeView组件默认显示分区列表。

“确定”按钮的clicked信号连接到onClicked方法。本示例允许输入多个过滤条件,条件之间用分号(;)隔开。应用过滤器时通过 split将输入的字符串拆分为字符串列表,再传递给 setNameFilters方法。

示例运行结果如图14-13所示。

在文本框中输入“*.txt”,单击“确定”按钮。此时,扩展名为.txt以外的文件变为禁用状态,文本呈现为灰色,并且不能选中,如图14-14所示。

应用多个过滤器,例如输入“*.zip;*.mp3;*.tar”,单击“确定”按钮后,只有扩展名为.zip、.mp3、.tar的文件有效。

通常人们更习惯于将不符合过滤条件的文件隐藏,而不是变为禁可用状态。要实现此效果需要调用 setNameFilterDisables 方法,并向参数赋值 False。代码如下:

self._model.setNameFilterDisables(False)

修改后再次进行过滤,未符合条件的文件就不再显示了,如图14-15所示。

图 14-13、图 14-14、图 14-15
图14-13 未使用过滤器的文件列表 图14-14 应用单个过滤器后 图 14-15 只显示符合条件的文件
图14-13 未使用过滤器的文件列表
图14-14 应用单个过滤器后
图 14-15 只显示符合条件的文件

14.4.3 示例:创建和删除目录

本示例将使用mkdir方法创建新目录,使用 rmdir方法删除选中的目录。示例程序窗口使用QGridLayout布局,第一行放置 QTreeView 组件,第二行放置两个 QPushButton组件。具体代码如下:

#程序窗口 window = QWidget() #设置标题 window.setWindowTitle("创建和删除目录") #设置窗口大小 window.resize(325, 287) #布局 layout = QGridLayout() window.setLayout(layout) #实例化 QTreeView 组件 treeview = QTreeView(window) layout.addWidget(treeview, 0, 0, 1, 2) #创建两个按钮组件 btnMakeDir = QPushButton("创建目录", window) btnRemoveDir = QPushButton("删除目录", window) layout.addWidget(btnMakeDir, 1, 0) layout.addWidget(btnRemoveDir, 1, 1) layout.setRowStretch(0, 1)

下面的代码初始化 QFileSystemModel 对象:

#实例化 QFileSystemModel 类 theModel = QFileSystemModel(window) #当前目录 currIndex = theModel.setRootPath(QDir.currentPath()) #视图组件与模型关联 treeview.setModel(theModel) treeview.setRootIndex(currIndex)

QDir.currentPath静态方法返回当前目录的路径(默认是应用程序所在的目录)。setRootPath调用后会返回根目录在模型中的索引,随后可以将该索引用于 QTreeView组件的 setRootIndex方法。

连接“创建目录”“删除目录”按钮的clicked信号,处理代码如下:

def onMkDirClicked(): #让用户输入新目录的名称 newDirName, ok = QInputDialog.getText( window, "输入", "新目录的名称:" ) if ok: #创建目录 theModel.mkdir(currIndex, newDirName) def onRmDirClicked(): #获取当前选定的项 selIndex = treeview.currentIndex() if theModel.isDir(selIndex): #删除目录 theModel.rmdir(selIndex) btnMakeDir.clicked.connect(onMkDirClicked) btnRemoveDir.clicked.connect(onRmDirClicked)

一般情况下,currentIndex方法返回的是 QTreeView 中被选择的索引,然后要用 isDir方法判断被选项是否为目录(本示例仅删除目录)。如果选中的是目录,就用rmdir方法删除。

运行示例程序,单击“创建目录”按钮,弹出输入对话框。在对话框中输入新目录名称,例如“Winderful”,如图14-16所示。

确认输入后,Winderful目录被创建,QTreeView组件自动刷新,如图14-17所示。

图 14-16、图 14-17
图 14-16 输入新目录名 图 14-17 视图自动刷新
图 14-16 输入新目录名
图 14-17 视图自动刷新

在 QTreeView组件中选中刚才创建的 Winderful 目录,再单击“删除目录”按钮将其删除。

14.5

编辑功能

列表模型要实现编辑功能,需要重写setData方法。该方法声明如下:

def setData( index: QModelIndex, value: Any, role: int = ... ) -> bool

index参数代表正在被编辑的数据项索引。value参数传递的是数据项对应的值。role参数是Qt.ItemDataRole 枚举的值,通常为EditRole。setData 方法内需要完成将 value 传递到基础数据,并替换旧的值。如果此过程顺利完成则返回True,若失败则返回False。

另外,为了表明模型支持编辑,需要重写flags方法,返回的Qt.ItemFlag枚举值中必须包含ItemIsEditable。

若需要对数据列表进行追加、插入、删除等操作,则重写insertRows、insertColumns方法。

14.5.1 示例:可编辑列表

本示例将从QAbstractListModel类派生出自定义的模型类,实现一维列表的编辑功能。具体实现步骤如下。

  1. 定义 CustModel,基类是 QAbstractListModel。
class CustModel(QAbstractListModel):
  1. 在__init__方法中通过 data参数传递原始数据。
def __init__(self, data: list, parent: QObject = None): super().__init__(parent) self._data = data
  1. 定义两个方法,用于获取和设置原始(基础)数据。
def sourceList(self): return self._data def setSourceList(self, list: list): self._data = list
  1. 重写rowCount方法,返回列表中的元素数量。
def rowCount(self, parent: QModelIndex = QModelIndex()) -> int: if parent.isValid(): return 0 return len(self._data)
  1. 重写data方法,返回指定索引处的数据。
def data(self, index: QModelIndex, role: int = Qt.ItemDataRole. DisplayRole) -> Any: if role == Qt.ItemDataRole.DisplayRole or role == Qt.ItemDataRole. EditRole: #获取行号 i = index.row() #如果索引无效,返回None if self.hasIndex(i, 0) == False: return None #返回数据 return self._data[i] return None

上述代码满足 role参数为DisplayRole或EditRole时返回数据,保证在正常显示和正在编辑两种状态下都能呈现数据。如果仅在DisplayRole前提下返回数据,那么在用户界面上编辑数据时,输入文本框的初始内容就是空白字符串。这不太符合人们的使用习惯,通常在编辑数据时,文本框应默认显示原有的数据。

  1. 重写setData方法,实现在用户完成编辑后更新原始数据。
def setData(self, index: QModelIndex, value: Any, role: int = Qt. ItemDataRole.EditRole) -> bool: if role == Qt.ItemDataRole.EditRole: #获取索引 i = index.row() #设置数据 self._data[i] = value #重要:一定要发出 dataChanged 信号 self.dataChanged.emit(index, index, [role]) return True return False

当数据被更新后,必须发出dataChanged信号,使得连接该信号的视图组件能及时刷新显示。setData方法如果更改数据失败,要返回False。

  1. 重写 flags方法,返回包含ItemIsEditable的标志值。
def flags(self, index: QModelIndex) -> Qt.ItemFlag: #获取基类设置的标志 f = super().flags(index) #加上可编辑标志 f = f | Qt.ItemFlag.ItemIsEditable return f

ItemFlag枚举支持使用“或”运算符组合多个值。

  1. 构建测试数据。
srcList = ['白菜', '卷心菜', '芥菜', '萝卜', '绿花菜', '芹菜']
  1. 创建自定义的模型实例。
model = CustModel(srcList)
  1. 构建用户界面。
#程序窗口 win = QWidget() #布局 layout = QGridLayout() win.setLayout(layout) #按钮 btn = QPushButton("显示数据", win) layout.addWidget(btn, 1, 0) #标签 lb = QLabel(win) lb.setWordWrap(True) layout.addWidget(lb, 1, 1) layout.setColumnStretch(0, 0) layout.setColumnStretch(1, 1) #列表视图 listview = QListView(win) listview.setModel(model) layout.addWidget(listview, 0, 0, 1, 2)

程序窗口使用网格布局。第一行跨两列放一个QListView组件,用来显示数据。第二行放置一个按钮和一个标签组件。按钮被单击后会在标签上显示原始数据列表。此做法是为了验证数据是否被成功修改。

  1. 连接按钮的 clicked信号,显示原始数据。
def onBtnClicked(): s ='、'.join(model.sourceList()) 1b.setText(s) btn.clicked.connect(onBtnClicked)

运行示例程序后,先单击“显示数据”按钮,确认原始数据和视图呈现的数据一致,如图14-18所示。

在QListView组件上选择要编辑的项,双击或者按快捷键 F2,使其进入编辑状态。例如,将“绿花菜”改为“白花菜”,如图14-19所示。

按【Enter】键或在QListView组件的其他空白区域单击一下鼠标,结束编辑状态。再次单击“显示数据”按钮,可以看到,原始数据也被更新了,如图14-20所示。

图 14-18、图 14-19、图 14-20
图 14-18 初始数据 图14-19 输入“白花菜” 图14-20 原始数据已更新
图 14-18 初始数据
图14-19 输入“白花菜”
图14-20 原始数据已更新

14.5.2 插入和删除数据

让列表模型支持插入、删除操作需要重写以下方法。

def insertRows( row: int, count: int, parent: QModelIndex = ...) -> bool
def insertColumns( column: int, count: int, parent: QModelIndex = ...) -> bool
def removeRows( row: int, count: int, parent: QModelIndex = ...) -> bool def removeColumns( column: int, count: int, parent: QModelIndex = ...)

对于insert*方法,row、column参数指定新行(新列)的插入点索引,新的数据项会在指定索引之前插入。假设有A、B、C三列,在B的索引处插入D,B、C列将向后移一位,变成A、D、B、C。要把数据项插入列表的开头,可以指定row=0或column=0;若要把数据项追加到列表的末尾,可以指定row=rowCount()或 column=columnCount()。其中,rowCount方法返回当前列表的行数,columnCount方法返回当前列表的列数。parent参数指定新插入的行或列所属的父级节点,对于普通列表或表格视图,直接使用QModelIndex()即可。count参数指定要插入的行数(或列数)。

remove*方法的参数含义与insert*方法相同。无论是插入新数据项,还是删除现有数据项,如果操作成功,则方法应返回True,失败就返回False。

在重写 insert*方法时,插入新数据项前必须调用 beginInsertRows或 beginInsertColumns 方法。完成后还要调用 endInsertRows 或 endInsertColumns 方法。重写 remove*方法的原理相同,删除数据前要调用 beginRemoveRows 或 beginRemoveColumns 方法,处理完毕后要调用 endRemoveRows 或endRemoveColumns方法。这样做是为了能及时通知与模型关联的视图组件,以应对模型数据的变化。

14.5.3 示例:新增/删除图书信息

本示例通过自定义模型实现新增和删除图书信息的功能。模型类名为BookModel,派生自QabstractTableModel类,实现过程如下。

  1. 构造函数内初始化一个列表对象,用于存放图书信息。
def __init__(self, parent: QObject = None): super().__init__(parent) #内部数据,存放图书信息 self._books = []
  1. 重写columnCount方法,返回表格的列数。本示例直接返回2,表示表格有两列。
def columnCount(self, parent: QModelIndex = QModelIndex()) -> int: if parent.isValid(): return 0 #本模型只有两列:书名、作者 return 2
  1. 重写 rowCount 方法,返回表格的行数。
def rowCount(self, parent: QModelIndex = QModelIndex()) -> int: if parent.isValid(): return 0 return len(self._books)
  1. 重写flags方法,返回相关的标志位。本示例的模型不需要编辑功能,因此返回的值中不包含ItemIsEditable.
def flags(self, index: QModelIndex) -> Qt.ItemFlag: # ItemIsEnabled:可交互 # ItemIsSelectable:允许选择 #ItemNeverHasChildren:列表项无子项 f = Qt.ItemFlag.ItemIsEnabled | Qt.ItemFlag.ItemIsSelectable | Qt.ItemFlag.ItemNeverHasChildren return f
  1. 重写headerData方法,返回行、列标题。
def headerData(self, section: int, orientation: Qt.Orientation, role: int = Qt.ItemDataRole.DisplayRole) -> Any: if role == Qt.ItemDataRole.DisplayRole: if orientation == Qt.Orientation.Horizontal: #列标题 if section == 0: return "书名" if section == 1: return "作者" elif orientation == Qt.Orientation.Vertical: #行标题 return f'{section + 1}' return None
  1. 重写data方法,返回指定索引处的数据项。
def data(self, index: QModelIndex, role: int = Qt.ItemDataRole. DisplayRole) -> Any: if role == Qt.ItemDataRole.DisplayRole: #获取行号和列号 ri = index.row() ci = index.column() if not self.hasIndex(ri, ci): return None return self._books[ri][ci] return None
  1. 重写insertRows方法,向表格的指定索引处插入若干行。
def insertRows(self, row: int, count: int, parent: QModelIndex = QModelIndex()) -> bool: if parent.isValid(): return False if row < 0 or count < 1 or row > self.rowCount(): return False #统计已添加的数量 n = 0 #插入新项的索引 idx = row #开始写入 self.beginInsertRows(parent, row, row + count - 1) while n < count: item = ['<未知书名>', '<未知作者>'] #插入新项 self._books.insert(idx, item) #计数增加 n = n+ 1 #索引增加 idx = idx + 1 self.endInsertRows() return True

在处理添加新数据项操作前,必须调用 beginInsertRows方法,在完成新增数据项后必须调用endInsertRows 方法。beginInsertRows 方法的参数与 insertRows 方法不同。insertRows 方法的 row 参数是插入点索引,count参数是要插入的行数。而beginInsertRows方法的声明如下:

def beginInsertRows( parent: QModelIndex, first: int, last: int)

该方法是用first参数表示新行的开始索引,用last参数表示新行的结束索引。例如,这样调用inserRows 方法:

insertRows(2, 3)

它表示索引2处为插入点,连续插入3行。在调用 beginInsertRows方法时要转换一下,即 first=2,last=2+(3-1)=4。也就是说,插入后3个行的索引是2、3、4。

在 beginInsertRows与 endInsertRows之间是逻辑代码,实现向_books字段添加新元素。在本示例中,新添加的元素默认值为“<未知书名>”和“<未知作者>”。

由于本示例不需要在模型中插入新列,因此没有重写insertColumns方法。

  1. 重写 removeRows 方法,从索引 row 处开始,删除 count 行。
def removeRows(self, row: int, count: int, parent: QModelIndex = QModelIndex()) -> bool: if parent.isValid(): return False if row < 0 or row > self.rowCount() - 1 or count < 1: return False #此变量负责计数 n= 0 #要删除的项索引 idx = row #开始移除 self.beginRemoveRows(parent, row, row + count - 1) while n < count and len(self. books) > 0: #删除 del self._books[idx] #计数增加 n=n+ 1 #索引增加 idx = idx + 1 self.endRemoveRows() return True

removeRows 方法的实现与 inserRows 方法相似。在删除数据前,必须调用 beginRemoveRows 方法,删除数据后调用 endRemoveRows 方法。

insertRows、removeRows方法均返回 bool 值,如果成功就返回 True,否则返回 False。

  1. 定义appendBook方法,可以方便追加图书信息。
# bookName:书名 # author: 作者 def appendBook(self, bookName, author): #新行号 newRow = self.rowCount() if self.insertRow(newRow): #更新数据 item = self._books[newRow] item[0] = bookName item[1] = author

追加记录是把数据项插入列表的末尾,因此新的行号总是等于rowCount方法的返回值。

插入图书信息时仅设置了默认值,在插入成功后需要更新数据。此处直接修改books字段即可,不需要重写 setData 方法来更新数据,因为 BookModel 没有编辑功能。

  1. 定义removeBook方法,删除指定索引的行。
def removeBook(self, index: QModelIndex): self.removeRow(index.row())

完成模型类的编写后,将构建视图界面进行验证。CustWindow类表示应用程序窗口,里面包含显示数据用的 QTableView 组件,两个 QLineEdit 组件用来输入图书信息,最后是两个操作按钮——添加和删除图书信息。具体代码如下:

class CustWindow(QWidget): def __init__(self): super().__init__() #整体布局 self.rootLayout = QVBoxLayout() self.setLayout(self.rootLayout) #表格视图 self.tableView = QTableView(self) #只能选择一项 self.tableView.setSelectionMode(QAbstractItemView.SelectionMode. SingleSelection) #显示网格线 self.tableView.setShowGrid(True) #只能选择一整行 self.tableView.setSelectionBehavior(QAbstractItemView.SelectionBehavior. SelectRows) self.rootLayout.addWidget(self.tableView, 1) #子布局 self.subLayout = QFormLayout() #输入框 self.edtBookname = QLineEdit(self) self.edtAuthor = QLineEdit(self) #两个按钮 self.btnLayout = QHBoxLayout() self.btnNewItem = QPushButton("添加新书", self) self.btnDelItem = QPushButton("删除书籍", self) self.btnLayout.addWidget(self.btnNewItem) self.btnLayout.addWidget(self.btnDelItem) self.subLayout.addRow(self.btnLayout) self.subLayout.addRow("书名:", self.edtBookname) self.subLayout.addRow("作者:", self.edtAuthor) #添加到根布局 self.rootLayout.addLayout(self.subLayout) #连接按钮的 clicked 信号 self.btnNewItem.clicked.connect(self.onNewItem) self.btnDelItem.clicked.connect(self.onDelItem) #数据模型 self.model = BookModel(self) self.tableView.setModel(self.model) def onNewItem(self): #获取输入的内容 bookname = self.edtBookname.text() author = self.edtAuthor.text() if len(bookname) == 0 or len(author) == 0: return #调用快捷方法添加新项 self.model.appendBook(bookname, author) #添加新项后清除输入框 self.edtBookname.clear() self.edtAuthor.clear() def onDelItem(self): #获取表格视图中被选项的索引 indexes = self.tableView.selectedIndexes() if len(indexes) == 0: return #只需要获取一个索引即可 selIndex = indexes[0] #删除数据项 self.model.removeBook(selIndex)

图书信息是以行为单位来处理的,所以QTableView组件的 selectionBehavior属性应设置为SelectRows;selectionMode属性为 SingleSelection表示每次只能选择一项(单选模式)。

运行示例后,在文本框内输入书名和作者,单击“添加新书”按钮,即可将数据插入模型中;在QTableView组件中选中要删除的行,单击“删除书籍”按钮即可将数据从模型中删除,如图14-21所示。

图 14-21
图 14-21 添加或删除图书信息
图 14-21 添加或删除图书信息
14.6

QListWidget

QListWidget 类是 QListView 的派生类,它是 QListView 的便捷版本。QListWidget 自身维护着一个内部列表模型,使用时不需要创建列表模型。

列表项由QListWidgetItem类封装。在实例化时,可以通过构造函数设置文本和图标。QListWidgetItem类的构造函数声明如下:

def __init__( icon: Union[QIcon, QPixmap], text: str, listview: Optional[QListWidget] = ..., type: int = ...) def __init__( listview: Optional[QListWidget] = ..., type: int = ...) def __init__(other: QListWidgetItem) def __init__( text: str, listview: Optional[QListWidget] = ..., type: int = ...)

text参数指定要显示的文本,icon参数指定要显示的小图标,listview参数指定列表项的容器QListWidget 组件。创建 QListWidgetItem 实例后,需要通过 addItem 方法添加到 QListWidget 组件中。

为了使添加列表项变得更加简单,QListWidget组件还提供了以下方法成员:

#以文本形式直接添加 def addItem(label: str) #可以一次性添加多个子项 def addItems(labels: Sequence[str])

如果需要在某个索引处插入子项,还可以使用以下方法:

def insertItem(row: int, item: QListWidgetItem) def insertItem(row: int, label: str) def insertItems(row: int, labels: Sequence[str])

row参数指定要插入子项的索引,item、label参数都表示要插入的项。labels参数指定一个字符串序列,可以一次性插入多个子项。

要删除某个子项可以调用takeItem方法,或者调用clear方法清空列表。

14.6.1 示例:添加与删除列表项

本示例将演示addItem、takeItem和 clear方法的使用。示例窗口有3个按钮,“添加”按钮允许用户输入和添加列表项,“删除”按钮将删除被选择的项,“清空”按钮将清空整个列表。

具体的实现步骤如下。

  1. 实现窗口基本布局。
#应用程序窗口 window = QWidget() #设置窗口标题 window.setWindowTitle("Demo") #布局 rootLayout = QHBoxLayout(window) #列表组件 listwg = QListWidget(window) rootLayout.addWidget(listwg, 1) #3个按钮 btnAdd = QPushButton("添加", window) btnDel = QPushButton("删除", window) btnClear = QPushButton("清空", window) subLayout = QVBoxLayout() subLayout.addWidget(btnAdd) subLayout.addWidget(btnDel) subLayout.addWidget(btnClear) subLayout.addStretch(1) #将子布局添加到父布局中 rootLayout.addLayout(subLayout, 0)

窗口的根布局使用 QHBoxLayout,先添加 QListWidget 组件,随后添加 QVBoxLayout 布局。

QVBoxLayout 布局内添加 3 个按钮。

  1. 连接“添加”按钮的 clicked信号,通过 QInputDialog 组件获取用户输入的文本,然后将文本添加到 QListWidget 组件中。
def onAddClicked(): #通过输入对话框获取文本 text,ok = QInputDialog.getText(window,"输入","请输入列表项文本:") if ok: #添加列表项 listwg.addItem(text) btnAdd.clicked.connect(onAddClicked)
  1. 连接“删除”按钮的clicked信号,删除选中的子项。
def onDelClicked(): #获取当前选择的索引 currIndex = listwg.currentRow() #删除指定的项 item = listwg.takeItem(currIndex) if item is not None: QMessageBox.information(window, "提示", f"列表项【{item.text()}】已经删除") btnDel.clicked.connect(onDelClicked)

takeItem方法会返回被删除的项(QListWidgetItem)。上述代码使用 QMessageBox组件弹出消息框,显示已删除项的文本。

  1. 连接“清空”按钮的clicked信号,删除所有列表项。
def onClearClicked(): #如果列表是空的,就不需要清除 if listwg.count() == 0: return if QMessageBox.question( window, "询问", "确定要清空列表吗?", QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, QMessageBox.StandardButton.No) == QMessageBox.StandardButton.Yes: #清空列表 listwg.clear() btnClear.clicked.connect(onClearClicked)

运行示例程序,单击“添加”按钮,弹出输入对话框。在对话框中输入要添加的文本,确认之后新列表项将显示在QListWidget组件中,如图14-22所示。

选中“第三项”,然后单击“删除”按钮。随后返回主窗口,QListWidget组件已经看不到“第三项”了,如图14-23所示。

图 14-22、图 14-23
图 14-22 新添加的列表项 图 14-23“第三项”已删除
图 14-22 新添加的列表项
图 14-23“第三项”已删除

14.6.2 示例:排序

本示例演示 QListWidget 组件的排序功能,即 sortItems 方法的使用。排序方案由 Qt.SortOrder 枚举定义,只有两种排序法:升序(AscendingOrder)和降序(DescendingOrder)。

示例程序的界面布局如下:

#窗口 window = QWidget() window.setWindowTitle("排序") #布局 layout = QGridLayout() window.setLayout(layout) #按钮 btnSortAsc = QPushButton("升序", window) btnSortDes = QPushButton("降序", window) layout.addWidget(btnSortAsc, 0, 0) layout.addWidget(btnSortDes, 0, 1) # QListWidget listwq = QListWidget(window) layout.addWidget(listwg, 1, 0, 1, 2)

QListWidget组件用于显示数据,两个按钮用于操作排序方式。下面的代码向QListWidget组件添加8个列表项。

item1 = QListWidgetItem() iteml.setData(Qt.ItemDataRole.DisplayRole, 707) item2 = QListWidgetItem() item2.setData(Qt.ItemDataRole.DisplayRole, 58) item3 = QListWidgetItem() item3.setData(Qt.ItemDataRole.DisplayRole, 149) item4 = QListWidgetItem() item4.setData(Qt.ItemDataRole.DisplayRole, 365) item5 = QListWidgetItem() item5.setData(Qt.ItemDataRole.DisplayRole, 28) item6 = QListWidgetItem() item6.setData(Qt.ItemDataRole.DisplayRole, 931) item7 = QListWidgetItem() item7.setData(Qt.ItemDataRole.DisplayRole, 74) item8 = QListWidgetItem() item8.setData(Qt.ItemDataRole.DisplayRole, 168) listwg.addItem(iteml) listwg.addItem(item2) listwg.addItem(item3) listwg.addItem(item4) listwg.addItem(item5) listwg.addItem(item6) listwg.addItem(item7) listwg.addItem(item8)

分别连接两个按钮的 clicked 信号,调用 sortItems 方法进行排序。代码如下:

def onSortAsc(): listwg.sortItems(Qt.SortOrder.AscendingOrder) btnSortAsc.clicked.connect(onSortAsc) def onSortDes(): listwg.sortItems(Qt.SortOrder.DescendingOrder) btnSortDes.clicked.connect(onSortDes)

运行示例程序,初始数据列表如图14-24所示。

图 14-24
图 14-24 初始数据列表
图 14-24 初始数据列表
14.7

QTableWidget

QTableWidget类派生自QTableView,它是 QTableView类的便捷版本。该类用于在用户界面上呈现二维表格,每个单元格都可以用QTableWidgetItem类表示(包括行、列标题)。要设置某个单元格的内容可以调用 setItem 方法;要设置行标题,可以用 setHorizontalHeaderItem 或 setHorizontalHeaderLabels方法;设置列标题应调用 setVerticalHeaderItem 或 setVerticalHeaderLabels 方法。

14.7.1 示例:订单表

本示例将用QTableWidget类构建一张4行5列的二维表格。

QTableWidget组件初始化时需要先指定行数和列数,否则就算调用了setItem方法设置单元格内容也不会有任何显示。可以通过构造函数直接指定行数和列数:

tablewg = QTableWidget(4, 5)

如果在调用构造函数时未指定行、列数量,可以调用setRowCount、setColumnCount方法。例如:

tablewg.setRowCount(4) tablewg.setColumnCount(5)

调用 setHorizontalHeaderLabels 方法设置列标题。

tablewg.setHorizontalHeaderLabels(["编号","省","市","客户数","订单数"])

最后是往 QTableWidget 组件中添加内容。先创建 QTableWidgetItem 实例,再通过 setItem 方法把QTableWidgetItem 实例设置到指定的单元格中。

#第一行 cell00 = QTableWidgetItem("6001") cell01 = QTableWidgetItem("河南") cell02 = QTableWidgetItem("安阳") cell03 = QTableWidgetItem("28") cel104 = QTableWidgetItem("317") tablewg.setItem(0, 0, cell00) tablewq.setItem(0, 1, cell01) tablewg.setItem(0, 2, cell02) tablewg.setItem(0, 3, cell03) tablewg.setItem(0, 4, cell04) #第二行 cell10 = QTableWidgetItem("6002") cell11 = QTableWidgetItem("四川") cell12 = QTableWidgetItem("眉山") cell13 = QTableWidgetItem("3") cell14 = QTableWidgetItem("149") tablewg.setItem(1, 0, cell10) tablewg.setItem(1, 1, cell11) tablewg.setItem(1, 2, cell12) tablewg.setItem(1, 3, cell13) tablewg.setItem(1, 4, cell14) #第三行 cell20 = QTableWidgetItem("6003") cell21 = QTableWidgetItem("江西") cell22 = QTableWidgetItem("九江") cel123 = QTableWidgetItem("11") cel124 = QTableWidgetItem("326") tablewg.setItem(2, 0, cell20) tablewg.setItem(2, 1, cell21) tablewg.setItem(2, 2, cell22) tablewg.setItem(2, 3, cell23) tablewg.setItem(2, 4, cell24) #第四行 cell30 = QTableWidgetItem("6004") cell31 = QTableWidgetItem("浙江") cell32 = QTableWidgetItem("金华") cell33 = QTableWidgetItem("35") cell34 = QTableWidgetItem("403") tablewg.setItem(3, 0, cell30) tablewg.setItem(3, 1, cell31) tablewg.setItem(3, 2, cell32) tablewg.setItem(3, 3, cell33) tablewg.setItem(3, 4, cell34)

最终效果如图14-25所示。

图 14-25
图 14-25 简单的订单表
图 14-25 简单的订单表

14.7.2 示例:身高信息表

本示例将构建一个学生身高信息表。数据样本如下:

data = [ ["481125", "小周", 155], ["481131", "小高", 167], ["481140", "小李", 172], ["481195", "小王", 160], ["481129", "小宁", 175], ["481148", "小范", 158], ["481135", "小郑", 166], ["481178", "小林", 171] ]

以下代码将初始化 QTableWidget 组件:

#表格为8行3列 table = QTableWidget(8, 3) #设置列标题 cols = [ QTableWidgetItem("学号"), QTableWidgetItem("姓名"), QTableWidgetItem("身高/cm") ] #设置标题的背景颜色和文本颜色 for c in cols: c.setBackground(QColor("#00FA9A")) #将内容设置到列标题单元格中 for i in range(len(cols)): table.setHorizontalHeaderItem(i, cols[i])

将数据源 data 添加到 QTableWidget 组件中。

for r in range(8): for c in range(3): val = data[r][c] item = QTableWidgetItem() item.setData(Qt.ItemDataRole.DisplayRole, val) table.setItem(r, c, item)

对于身高大于或等于170的数据行,将背景颜色设置为黄色(改变一行中所有单元格的背景)。

for r in range(table.rowCount()): h = table.item(r, 2).data(Qt.ItemDataRole.DisplayRole) if h >= 170: for i in range(table.columnCount()): item = table.item(r, i) #设置背景颜色 item.setBackground(QColor("yellow"))

最后的呈现效果如图14-26所示。

图 14-26
图 14-26修改单元格的背景颜色
图 14-26修改单元格的背景颜色
14.8

QTreeWidget

QTreeWidget是QTreeView的派生类,也是一个便捷类,使用时不需要创建列表模型。

树节点由QTreeWidgetItem类表示。可以通过构造函数来建立树形结构,例如:

tree = QTreeWidget() ...... #"A""B"均为顶层节点 #"C"是"A"的子节点,"D"是"B"的子节点 node1 = QTreeWidgetItem(tree, ["A"]) node2 = QTreeWidgetItem(tree, ["B"]) node3 = QTreeWidgetItem(node1, ["C"]) node4 = QTreeWidgetItem(node2, ["D"])

node1、node2 在调用 QTreeWidgetItem 构造函数时,传递的父级对象都是 QTreeWidget 实例。这表明“A”“B”将成为顶层节点。node3的父级对象是node1,使得“C”成为“A”的子节点,如图14-27所示。

图 14-27
图14-27 4个节点之间的层次关系
图14-27 4个节点之间的层次关系

也可以在实例化 QTreeWidgetItem 对象后,调用 addChild或 insertChild方法添加子节点,如下面代码所示。

node1 = QTreeWidgetItem(["A"]) node2 =QTreeWidgetItem(["B"]) node3 = QTreeWidgetItem(["C"]) node4 = QTreeWidgetItem(["D"]) node1.addChild(node3) node2.addChild(node4)

再调用 QTreeWidget 组件的 addTopLevelItem 或 addTopLevelItems 方法将“A”“B”添加为顶层节点。

tree.addTopLevelItems([node1, node2])

QTreeWidgetItem类支持多列,例如:

tree = QTreeWidget() tree.setColumnCount(3) #第一个顶层节点 root1 = QTreeWidgetItem(tree, ["A组"]) #添加子节点 rootl.addChild(QTreeWidgetItem(["小陈","市场部","18120002154"])) root1.addChild(QTreeWidgetItem(["小文","财务部","13625611240"])) #第二个顶层节点 root2 = QTreeWidgetItem(tree, ["B组"]) #添加子节点 root2.addChild(QTreeWidgetItem(["小孙","技术部","13200058925"])) root2.addChild(QTreeWidgetItem(["小罗","生产部","15826511123"]))

在添加节点前,QTreeWidget 组件需要调用 setColumnCount方法设置列数(上述代码中是 3 列)。

顶层节点“A组”“B组”只有1列,而它们的子节点有3列。效果如图14-28所示。

下面的代码设置并显示列标题:

#headerHidden属性默认是False,因此下面一行可以省略 tree.setHeaderHidden(False) #设置列标题 tree.setHeaderLabels(["姓名", "部门", "联系电话"])

效果如图14-29所示。

图 14-28、图 14-29
图14-28 包含多列的节点 图 14-29显示列标题
图14-28 包含多列的节点
图 14-29显示列标题