第 14 章
列表模型与视图
本章共 8 个小节 · PySide6 Basic Tutorial
本章要点:
- 常见的列表模型类
- QStringListModel 和 QStandardItemModel
- QFileSystemModel
- QListWidget
- QTableWidget
- QTreeWidget
14.1
模型的抽象基类
列表模型(ItemModel)是应用数据与界面视图之间的桥梁,掌控着数据的呈现方式。列表模型采用二维表结构,可通过行号、列号来索引数据,如图14-1所示。
行、列编号从0开始,例如图14-1中B的行号为1,列号为2。由于每个数据项都包含父节点,因此对于顶层数据来说,它也包含一个隐藏的根节点。
如果数据是一维列表(如array、list),那么它的模型布局只有一列,每个元素为一行,如图14-2所示。
另一种比较复杂的模型是树形结构,数据项存在“父子”关系,如图14-3所示。
A所在的行号为0,列号为0。由于B是A的子节点,它的编号要重新计算,因此B的行号和列号也是0。
QAbstractItemModel类是列表模型的抽象基类。作为公共接口,不能直接实例化,开发人员需要创建 QAbstractItemModel 的派生类,然后才能与视图组件一起使用。
对于只读模型(仅获取和显示数据,不可编辑),只要实现以下方法即可。
- columnCount:返回某个父节点所包含的列数。
- rowCount:返回某个父节点下包含的行数。
- parent:获取给定索引的父节点,返回父节点的索引。
- index:获取列表项的索引。
- data:返回给定索引处的基础数据(原始数据)。
如果要视图支持编辑功能,并且希望列表模型能够将已修改的数据更新到原始数据,那就要实现以下方法。
- setData:用新值更新原始数据。注意,当数据更新后,需要发出dataChanged信号。
- flags:返回的值中必须包含ItemIsEditable(Qt.ItemFlag枚举定义的成员)。
14.1.1 ItemDataRole
Qt.ItemDataRole枚举定义了一组成员,描述了数据项在列表模型中的用途,即视图组件在绘制各部分界面元素时所需要的数据类型,可称为“数据角色”。ItemDataRole枚举定义的成员如下。
- DisplayRole:显示在视图中的文本,字符串类型。
- DecorationRole:装饰元素,例如列表项前面的小图标,其类型可以是QIcon,也可以是QPixmap和 QColor。
- EditRole:数据处于编辑状态,例如在文本框中修改内容。
- ToolTipRole:返回字符串数据,显示在工具提示中。
- StatusTipRole:返回的文本将显示在状态栏上。
- WhatsThisRole:文本将显示在“这是什么”帮助信息中。
- SizeHintRole:返回QSize类型的数据,表示列表项要占用空间的大小(宽度和高度)。
- FontRole:应返回QFont类型的对象,用来绘制视图中的内容。
- TextAlignmentRole:返回Qt.AlignmentFlag枚举的值,用于设置文本的对齐方式。
- BackgroundRole:返回的数据类型为 QBrush,用来绘制列表项的背景。
- ForegroundRole:也是返回QBrush类型的数据,表示文本的颜色。
- CheckStateRole:返回CheckState枚举的值,用于指定复选框的状态。
- InitialSortOrderRole:列表头的排序方式,返回 SortOrder枚举的值,即升序或降序。
- AccessibleTextRole:返回字符串类型的数据,用于设置辅助提示的主文本,例如“屏幕朗读”功能。
- AccessibleDescriptionRole:返回辅助提示的描述信息。
实现 QAbstractItemModel 类的 data 和 setData 方法都需要对 ItemDataRole 的值进行分析,从而返回或设置对应类型的值。
14.1.2 示例:整数列表模型
本示例将自定义一个提供整数类型的列表模型。该模型所使用的原始数据是一个list对象,其元素结构为单个维度—只有一列,每行表示一个元素。然后通过该模型将整数列表呈现在QListView组件中。
具体实现步骤如下。
- 定义 CustItemModel类,它派生自 QAbstractItemModel。
class CustItemModel(QAbstractItemModel):
- 为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
- 如果在构造类实例时未指定原始数据,可以通过 setSourceData 方法设置。其他代码可以通过sourceData方法获取原始数据。
def sourceData(self):
return self._myData
def setSourceData(self, data: list):
self._myData = data
- 重写parent方法,返回指定索引的父级。
def parent(self, index: QModelIndex) -> QModelIndex:
#此模型没有真正的父级,应返回无效的索
return QModelIndex()
本示例所处理的数据只有一个层级,数据项之间不存在父子关系。因此,不需要返回特定的索引,而应返回无效的索引,调用QModelIndex类的默认构造函数即可。无效索引的行号和列号都是-1。
- 重写index方法,返回指定行、列处的索引。
def index(self, row: int, column: int, parent: QModelIndex =QModelIndex ()) ->QModelIndex:
return self.createIndex(row, column)
外部代码不能直接访问 QModelIndex 类的成员,但可以通过 createIndex方法来创建索引。本示例不需要考虑父级索引。
- 重写rowCount方法,返回数据的行数。在本示例中,行数就是列表的元素个数。
def rowCount(self, parent: QModelIndex = QModelIndex()) -> int:
#由于该模型表示的是一维列表,列表项没有子级
#因此,如果parent是有效索引,就要返回0
if parent.isValid():
return 0
#返回数据源的元素个数
return len(self._myData)
- 重写columnCount方法,返回数据包含的列数。本示例所处理的数据是普通列表,只有一列。
def columnCount(self, parent: QModelIndex = QModelIndex()) -> int:
if parent.isValid():
return 0
#列表始终只有一列
return 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。
- 实例化一个视图组件,本示例使用的是QListView。该组件适用于简单的列表项。
lv = QListView()
- 初始化原始数据(一个整数序列)。
intList = [105, 17, 915, 400, 3020, 840]
- 实例化自定义模型类。
model = CustItemModel(intList, lv)
调用CustItemModel类的构造函数时传递原始数据。如果未传递原始数据,可以稍后调用setSourceData方法设置,例如:
model.setSourceData(intList)
- 调用视图组件的 setModel方法关联列表模型。
lv.setModel(model)
示例的运行效果如图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组件显示一组学生信息。具体实现步骤如下。
- 构建原始数据,稍后用于填充模型。
init_data = [
["小许", 19,"13205445202","
[email protected]"],
["小赵", 21, "18927274113", "
[email protected]"],
["小马",25,"13609210500","
[email protected]"],
["小易",27,"15530048951","
[email protected]"],
["小何",30,"47300125336","
[email protected]"]
上述数据样本是一个嵌套列表。列表中的顶层元素为学生记录,一条学生记录包含4个元素,代表姓名、年龄、联系电话和电子邮箱。
- 定义模型类 StudentItemModel,基类是 QabstractTableModel。
class StudentItemModel(QAbstractTableModel):
- 在__init__方法中传递基础数据(initData参数)。
def __init__(self, initData, parent: QObject = None):
super().__init__(parent)
self._theData = initData
- 重写 rowCount 方法,返回数据表的行数。
def rowCount(self, parent: QModelIndex = QModelIndex()) -> int:
if parent.isValid():
return 0
return len(self._theData)
二维表结构中的行一般没有父/子节点,所以如果parent参数是有效索引,应当返回0,如果索引是无效值,表明正在访问的是顶层数据,此时应返回原始数据的元素个数。
- 重写columnCount方法,返回二维表的列数,本示例是固定值4。
def columnCount(self, parent: QModelIndex = QModelIndex()) -> int:
if parent.isValid():
return 0
return 4
- 重写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时才返回用于视图呈现的数据。
- 重写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”。
- 二维表数据应使用 QTableView组件来显示。下面代码初始化 QTableView 组件和 StudentItemModel模型。
view = QTableView()
view.setWindowTitle("学生信息")
#创建模型实例
model = StudentItemModel(init data, view)
#为视图组件设置模型
view.setModel(model)
#显示组件
view.show()
示例运行效果如图14-5所示。
14.1.5 示例:突出显示销量高于平均值的行
本示例模拟一个月度销售表,在视图呈现时,如果某行数据的销量高于平均值,该行文本就显示为蓝色。
在重写 data方法时,当列表项需要绘制显示文本时,role参数的值是 DisplayRole;当需要根据数据来获取绘制文本的颜色时,role参数的值为ForegroundRole。将当前记录的销量与整体销量的平均值比较,若销量大于或等于平均值,就返回QColor("blue")。
具体实现步骤如下。
- 准备示例数据。
sourceData = [
['F302011', '热缩管', 1823],
['L281055', '元器件', 890],
['Q490084', '面包板', 442],
['T348192', '二极管', 2058],
['E419001', '导线', 1679],
['G780357', '三极管', 1315],
['K853692', 'LED灯珠', 4089],
['U160928', '热敏电阻', 2146]
]
- 调用 mean 函数求得销量的平均值。
evg = mean([x[2] for x in sourceData])
示例数据是嵌套的列表类型,子项也是列表。其中,子项的第三个元素为销量(索引是2)。
- 定义项目模型类 CustItemModel,派生自QabstractTableModel类。
class CustItemModel(QAbstractTableModel):
- 在__init__方法中通过 data参数传递示例数据。
def __init__(self, data, parent: QWidget = None):
super().__init__(parent)
self._data = data
- 重写 rowCount 方法,返回数据表的行数。
def rowCount(self, parent = QModelIndex) -> int:
if parent.isValid():
return 0
return len(self._data)
- 重写 columnCount 方法,返回数据表的列数。
def columnCount(self, parent = QModelIndex()) -> int:
if parent.isValid():
return 0
return len(self._data[0])
- 重写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
- 重写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
- 实例化 QTableView 组件。
table = QTableView()
table.setWindowTitle("月度销售额")
- 为QTableView 组件设置模型。
#实例化模型
model = CustItemModel(sourceData, table)
#为视图设置模型
table.setModel(model)
#显示组件
table.show()
运行示例程序,效果如图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.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行的员工信息表。具体步骤如下。
- 实例化 QTableView 和 QStandardItemModel类,代码如下:
#创建视图组件实例
view = QTableView()
view.setWindowTitle("员工信息")
#创建模型实例
stdModel = QStandardItemModel(view)
- 调用 setHorizontalHeaderLabels 方法设置列标题。
stdModel.setHorizontalHeaderLabels(["员工ID","姓名","年龄","部门"])
- 向模型添加数据。
# ------ 第一行 ------
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("人力资源部")
)
- 将模型对象设置到QTableView组件中,显示数据。
view.setModel(stdModel)
#显示视图
view.show()
示例程序的运行效果如图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.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.3.4 示例:多列树形视图
本示例将实现具有多个列的树形数据表。用到的仍然是 QTreeView组件和 QStandardItemModel类。
一般树形视图的子节点只有一列,而实现多列视图的原理就是在调用setChild方法时同时指定行号和列号。不过,QTreeView组件只有第一列才支持树形结构,因此在建立列表项的层级关系时仅需考虑第一列即可。
本示例将构建一个值班人员表。顶层节点是工作人员所在的分组,子节点才是值班信息。具体实现步骤如下。
- 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)
- 创建标准列表模型实例。
model = QStandardItemModel()
#设置列标题
model.setHorizontalHeaderLabels(["姓名","性别","值班时间"])
- 准备示例数据。
#添加列表项
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)
- 初始化 QTreeView 组件。
treeview = QTreeView()
treeview.setWindowTitle("值班安排")
- 为视图组件设置列表模型。
treeview.setModel(model)
- 显示组件。
14.4
QFileSystemModel
QFileSystemModel类是 Qt提供的列表模型,支持对本地文件系统的访问。如 QTreeView等视图组件可以借助该类显示文件和目录结构。另外,QFileSystemModel类还提供了以下方法,可以方便地进行一些常用操作。
- mkdir:创建新目录。
- rmdir:删除目录。
- remove:删除文件。
- setNameFilters:过滤文件名。
- 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.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.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所示。
在 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类派生出自定义的模型类,实现一维列表的编辑功能。具体实现步骤如下。
- 定义 CustModel,基类是 QAbstractListModel。
class CustModel(QAbstractListModel):
- 在__init__方法中通过 data参数传递原始数据。
def __init__(self, data: list, parent: QObject = None):
super().__init__(parent)
self._data = data
- 定义两个方法,用于获取和设置原始(基础)数据。
def sourceList(self):
return self._data
def setSourceList(self, list: list):
self._data = list
- 重写rowCount方法,返回列表中的元素数量。
def rowCount(self, parent: QModelIndex = QModelIndex()) -> int:
if parent.isValid():
return 0
return len(self._data)
- 重写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前提下返回数据,那么在用户界面上编辑数据时,输入文本框的初始内容就是空白字符串。这不太符合人们的使用习惯,通常在编辑数据时,文本框应默认显示原有的数据。
- 重写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。
- 重写 flags方法,返回包含ItemIsEditable的标志值。
def flags(self, index: QModelIndex) -> Qt.ItemFlag:
#获取基类设置的标志
f = super().flags(index)
#加上可编辑标志
f = f | Qt.ItemFlag.ItemIsEditable
return f
ItemFlag枚举支持使用“或”运算符组合多个值。
- 构建测试数据。
srcList = ['白菜', '卷心菜', '芥菜', '萝卜', '绿花菜', '芹菜']
- 创建自定义的模型实例。
model = CustModel(srcList)
- 构建用户界面。
#程序窗口
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组件,用来显示数据。第二行放置一个按钮和一个标签组件。按钮被单击后会在标签上显示原始数据列表。此做法是为了验证数据是否被成功修改。
- 连接按钮的 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.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类,实现过程如下。
- 构造函数内初始化一个列表对象,用于存放图书信息。
def __init__(self, parent: QObject = None):
super().__init__(parent)
#内部数据,存放图书信息
self._books = []
- 重写columnCount方法,返回表格的列数。本示例直接返回2,表示表格有两列。
def columnCount(self, parent: QModelIndex = QModelIndex()) -> int:
if parent.isValid():
return 0
#本模型只有两列:书名、作者
return 2
- 重写 rowCount 方法,返回表格的行数。
def rowCount(self, parent: QModelIndex = QModelIndex()) -> int:
if parent.isValid():
return 0
return len(self._books)
- 重写flags方法,返回相关的标志位。本示例的模型不需要编辑功能,因此返回的值中不包含ItemIsEditable.
def flags(self, index: QModelIndex) -> Qt.ItemFlag:
# ItemIsEnabled:可交互
# ItemIsSelectable:允许选择
#ItemNeverHasChildren:列表项无子项
f = Qt.ItemFlag.ItemIsEnabled | Qt.ItemFlag.ItemIsSelectable | Qt.ItemFlag.ItemNeverHasChildren
return f
- 重写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
- 重写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
- 重写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方法。
- 重写 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。
- 定义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 没有编辑功能。
- 定义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.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个按钮,“添加”按钮允许用户输入和添加列表项,“删除”按钮将删除被选择的项,“清空”按钮将清空整个列表。
具体的实现步骤如下。
- 实现窗口基本布局。
#应用程序窗口
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 个按钮。
- 连接“添加”按钮的 clicked信号,通过 QInputDialog 组件获取用户输入的文本,然后将文本添加到 QListWidget 组件中。
def onAddClicked():
#通过输入对话框获取文本
text,ok = QInputDialog.getText(window,"输入","请输入列表项文本:")
if ok:
#添加列表项
listwg.addItem(text)
btnAdd.clicked.connect(onAddClicked)
- 连接“删除”按钮的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组件弹出消息框,显示已删除项的文本。
- 连接“清空”按钮的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.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.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.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.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所示。
也可以在实例化 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所示。