zoukankan      html  css  js  c++  java
  • 第15.28节 PyQt(Python+Qt)入门学习:Model/View架构中的便利类QTableWidget详解

    一、引言

    表格部件为应用程序提供标准的表格显示工具,在表格内可以管理基于行和列的数据项,表格中的最大数据项数为总行数和总列数的乘积,另外在表格中可以设置水平和垂直标题。

    表格部件对应类为QTableWidget ,QTableWidget 表格部件中的项类型为QTableWidgetItem类。QTableWidget 从QTableView派生的子类,内置默认模型,如果表格展现的应用需要使用自己的数据模型,则应使用QTableView类,而不是QTableWidget 类。

    二、QTableWidget的属性

    2.1、概述

    除了从父类继承的属性外,在Designer中QTableWidget只有两个属性,就是行数rowCount和列数columnCount,另外还有一部分就是用于设置表头的属性。如图:
    在这里插入图片描述

    2.2、行数rowCount

    QTableWidget的rowCount属性保存表格部件中的行数,在QTableWidget创建时如果没有指定行数,则缺省行数为0,QTableWidget创建后可以通过 setRowCount方法调整行数。

    要获取当前表格部件中的行数,可以通过rowCount()方法获取,要设置表格部件的行数,可以通过setRowCount(int rows)调整表格的行数,如果参数rows小于现在表格中的实际行数,则表格中超出参数的行数数据会丢弃,就算是后面将行数或列数恢复也不能恢复相关数据。

    2.3、列数columnCount

    columnCount属性保存表格部件中的列数,相关属性和操作方法和rowCount类似。在QTableWidget创建时如果没有指定列数,则缺省列数为0,QTableWidget创建后可以通过 setColumnCount方法调整。

    columnCount可以通过columnCount()方法获取,通过setColumnCount(int columns)设置。

    2.4、表头及属性

    QTableWidget的表头包括横表头和竖表头,如下:
    在这里插入图片描述
    上面的“列1”、“列2”、“列3”为横表头,“行1”、“行2”、“行3”为竖表头。横表头可通过方法setHorizontalHeaderLabels来设置,竖表头可以通过setVerticalHeaderLabels来设置。这是创建表头的最简单方法,它们将为表的列和行提供简单的文本标题。

    下面代码为上图设置表头的示例代码:

            self.tableWidget.setVerticalHeaderLabels(['行1','行2','行3'])
            self.tableWidget.setHorizontalHeaderLabels(['列1','列2','列3'])
    

    除了上述简单方法外,还可以从表格部件的项创建更复杂的表头,相关方法在下面介绍QTableWidget的方法时再介绍。

    除了设置表头文字外,表头还有相关属性设置,QTableWidget的表头属性是直接继承自QTableView的表头属性,相关属性的介绍请参考《PyQt(Python+Qt)学习随笔:QTableView的标题表头相关属性》。

    三、QTableWidget的项QTableWidgetItem

    3.1、概述

    QTableWidgetItem类为QTableWidget类的项实例类,用于保存表格部件的信息。项的内容可包含文本、图标或复选框等。

    默认情况下,QTableWidgetItem项是可用、可编辑、可选择和可选中的,并且可以用作拖放操作的源和拖放目标。如果要改变相关设置,可以通过使用setFlags()来更改每个项的标志(关于项的标记请参考《PyQt(Python+Qt)学习随笔:Model中项的标记flags取值及枚举类型Qt.ItemFlag》)。

    QTableWidgetItem的方法分为构造方法、位置相关方法、数据及展现内容相关、操作相关四类。

    3.2、构造方法

    QTableWidgetItem的构造方法有4个:

    1. QTableWidgetItem(int type = Type):构建一个空项
    2. QTableWidgetItem( str text,int type = Type):构建一个带文本的项
    3. QTableWidgetItem( QIcon icon, str text, int type = Type) :构建一个带图标和文本的项
    4. QTableWidgetItem( QTableWidgetItem other):从other复制项的内容构建一个新项

    3.3、位置相关方法

    QTableWidgetItem项在QTableWidget中的位置包括三个属性来决定,就是表格部件对象、行和列,由于这三个属性决定了项在界面的哪个组件的哪个地方,因此老猿将相关方法归类为位置相关方法。QTableWidgetItem项方法中:

    • QTableWidget tableWidget():返回项所在的表格部件实例,如果项没有插入到树型部件中,则返回None
    • int row() :返回项所在表格的行,注意是从0开始计数
    • int column():返回项所在表格的列,注意是从0开始计数

    3.4、数据和展现相关方法

    在部件中的数据有多种属性,包括文本数据、图标、复选状态、工具栏提示toolTip、whatsThis帮助、状态提示statusTip等,在Qt中将这些称为同一个项的不同角色数据(关于数据的角色请参考《PyQt学习随笔:Model/View中诸如DisplayRole的数据角色及含义》)。这些数据为呈现数据的不同状态情况,因此老猿将与此相关的方法归类为数据和展现相关方法。不同角色的数据在Qt中可以通过data和setData方法带角色参数统一访问,也可以通过各自不同的方法不带角色去访问。

    3.4.1、项数据的data和setData访问方法

    部件中的项数据可以通过项的data( int role) 方法获取项中指定列指定角色的数据,也可以通过setData(int role, QVariant value)方法设置指定角色的数据为value。例如项的文本可以通过data方法和setData方法使用Qt.DisplayRole、Qt.EditRole这两种角色去访问。

    关于数据的角色请参考《PyQt学习随笔:Model/View中诸如DisplayRole的数据角色及含义》。

    注意:
    • role虽然是整数,但实际上是枚举类型 Qt.ItemDataRole
    • QVariant 表示任何PyQt的数据类型,所以value的类型没有约束
    示例代码:
       
     		item.setData(QtCore.Qt.DecorationRole,QtGui.QIcon('.\icon\'+str(row+1)+'.gif'))#设置图标
            item.setData(QtCore.Qt.CheckStateRole, row%3) #设置复选状态
            item.setData(QtCore.Qt.TextAlignmentRole,col % 3)  #设置文本对齐方式
            
    
    3.4.2、项文本数据访问的text和setText方法

    项的文本可以通过data方法和setData方法使用Qt.DisplayRole、Qt.EditRole这两种角色去访问,除了这个方法外,还可以通过text() 和setText( str value)方法去访问。

    • text():获取项的文本数据
    • setText( str value):设置项的文本数据为value,注意没有返回值
    3.4.3、项文本对齐方式访问的textAlignment和setTextAlignment方法

    项中的文本可以设置水平和垂直对齐方式,相关对齐方式的访问方法调用方式如下:

    • int textAlignment() :返回对齐方式,返回类型是枚举类型Qt.Alignment,实际上是整数
    • setTextAlignment(int alignment):设置对齐方式,无返回值
    注:

    对齐方式是一个Qt.Alignment枚举类型的组合,具体取值及含义请参考《PyQt(Python+Qt)学习随笔:QListView的itemAlignment属性》。

    3.4.4、项中图标访问的icon和setIcon方法

    可以通过icon()来访问项的图标,通过setIcon( QIcon icon)来设置项的图标。

    如下面代码将顶的图标设置为指定文件:

    item.setIcon(QtGui.QIcon(r'F:小图标动物动物-025.gif'))
    
    3.4.5、项复选状态访问方法

    项可以单独设置复选状态,如图所有项都设置了复选状态,每行的复选状态不同:
    在这里插入图片描述
    项的复选状态可以通过checkState()来获取,如果要改变项的复选状态可以调用setCheckState(Qt.CheckState state)来实施。

    注意:

    3.4.6、项提示信息相关操作方法

    提示信息包括工具栏提示、状态栏提示和whatsThis提示:

    • 通过toolTip(int column)、setToolTip(int column, str toolTip)来操作toolTip
    • 通过statusTip(int column)、setStatusTip(int column, str statusTip)来操作statusTip
    • 通过whatsThis(int column)、setWhatsThis(int column, str whatsThis)来操作whatsThis

    关于这三个提示信息的区别请参考《PyQt(Python+Qt)学习随笔:Qt Designer中部件的toolTip、toolTipDuration、statusTip、whatsThis属性》。

    3.4.7、sizeHint相关方法

    关于QTableWidgetItem项的sizeHint属性,Qt中的文档说明非常简单,作用也没有展开说明,仅介绍QTableWidgetItem中sizeHint为项的缺省大小,如果没有设置则根据项的数据自动计算项的大小。

    为此老猿做了大量测试和验证,最终确认了QTableWidgetItem项的sizeHint的作用,QTableWidgetItem的sizeHint在项对应QHeadView表头的sectionResizeMode值为ResizeToContents时,作为计算项大小的一个因素:

    1. 判断项是否设置了sizeHint,如果没有设置则按项的内容计算项大小,确保项的内容在对应表头方向完整显示;
    2. 如果项设置了sizeHint,则取sizeHint的值作为项的大小。

    具体请参考《PyQt学习随笔:QTableWidget项sizeHint的作用以及与QHeadView的sectionResizeMode、ResizeToContents的关系》和《PyQt(Python+Qt)学习随笔:QTableWidget表格部件中行高和列宽的计算方式》。

    项的sizeHint相关访问方法及调用语法如下:
    • QSize sizeHint()
    • setSizeHint( QSize size)

    3.5、项操作相关方法

    QTableWidget中项操作相关的属性包括是否可用、是否可选中、是否可编辑、是否可复选、是否选中、是否复选等,这些属性的设置与影响界面上项的操作,所以老猿将其归类项操作相关方法。这些项操作相关方法所有Model/View相关类涉及项操作都是一样的。由于复选状态同时是项的展现状态,在前面数据和展现相关方法中已经介绍相关方法。

    3.5.1、项标记相关方法

    项的标记用于标记项是否可操作,由多个属性位组合而成,具体项标记的取值及含义请参考《PyQt(Python+Qt)学习随笔:Model中项的标记flags取值及枚举类型Qt.ItemFlag》。

    项标记的访问方法及调用语法如下:

    • Qt.ItemFlags flags()
    • setFlags(Qt.ItemFlags flags)

    3.5.2、项是否选中相关方法

    项是否被用户选中可以通过isSelected方法获取,如果要通过代码设置项的选中状态,则调用setSelected方法,相关调用语法如下:

    • bool isSelected()
    • setSelected(bool select)

    四、QTableWidget的主要方法

    除了从父类继承的方法外,QTableWidget的方法老猿将其归纳为构造方法、部件状态访问方法、项操作方法、项查找和定位方法、表头操作方法五大类。

    4.1、构造方法

    QTableWidget有2个构造方法:

    • QTableWidget(QWidget parent = None) :创建一个0行0列的QTableWidget实例
    • QTableWidget(int rows, int columns, QWidget parent = None):创建一个rows行columns列的QTableWidget实例

    这两个构造方法的区别就是后者指定了部件的行数和列数,而前者行数和列数为0,需要在实例构建后再另外去指定行数和列数。参数parent 一般传部件所在窗口,不传也没关系。

    注:

    这里的构造方法是沿用C++的说法,Python中的构造方法是__init__。

    4.2、部件状态访问方法

    老猿将部件中反映部件当前情况的一些方法归类为部件状态访问方法,包括部件的行数、列数、当前项、当前行、当前列等属性访问方法。由于部件行数和列数的访问方法在部件属性中已经介绍了,在此不重复介绍。

    4.2.1、当前项访问方法

    当前项是指当前鼠标和键盘焦点所在项,在项可以进行选择操作时,当前项可以是选中状态,也可以是未选中状态,选中项也不一定是当前项。与当前项相关的方法包括:

    • QTableWidgetItem currentItem() :返回当前项对应项对象,如果没有当前项则返回None
    • setCurrentItem(QTableWidgetItem item):设置当前项
    • setCurrentItem(QTableWidgetItem item, QItemSelectionModel.SelectionFlags command):设置当前项
    • setCurrentCell(int row, int column):设置当前项
    • setCurrentCell(int row, int column, QItemSelectionModel.SelectionFlags command):设置当前项
    注意:
    1. 后面4个方法都是设置当前项,方法名为setCurrentItem的参数是QTableWidgetItem实例,方法名为setCurrentCell的参数是行和列,即这两类方法依据的项定位方法不同
    2. 参数中带command参数的,要求除了将当前项改为参数确认的项之外,还要求有额外的响应,具体响应由command参数确认。关于QItemSelectionModel.SelectionFlags 请参考《PyQt(Python+Qt)学习随笔:Mode/View中的枚举类QItemSelectionModel.SelectionFlag取值及含义》;
    3. 如果当前项没有,则currentItem()方法返回None。

    4.2.2、当前行和列访问方法

    当前项所在的行和列,既可以通过当前项的QTableWidgetItem实例对象的行号(row()方法)和列号(column()方法)获取,也可以直接通过QTableWidget获取。调用语法如下:

    • int currentColumn()
    • int currentRow()
      如果部件没有当前项,则上述两个方法返回-1。

    4.3、部件中的项操作方法

    部件中的项操作包括设置项、删除项、编辑项等。

    4.3.1、设置项setItem方法

    setItem用于在表格部件QTableWidget创建后,设定指定行和指定列的项为一个QTableWidgetItem实例对象。调用语法如下:
    setItem(int row, int column, QTableWidgetItem item)

    注意:
    • 该方法没有返回值,但会触发itemChanged信号
    • 在使用setItem之前,需要确保表格部件的行数和列数已经设置,且参数 row和column在行数和列数的范围内,否则设置不会成功,这也意味着表格部件的行数和列数不能随着项的增加自动增加,必须预定义好
    示例代码:
            for row in range(5):
                for col in range(3):
                    item = QtWidgets.QTableWidgetItem(f"({row},{col})" )
    
                    self.tableWidget.setItem(row,col,item)
    

    4.3.2、触发编辑项的editItem方法

    QTableWidget提供了触发项编辑的方法,调用语法如下:
    editItem(QTableWidgetItem item)

    注意:
    • editItem方法生效必须设置项的标记flags为可编辑
    • editItem一次只能触发一个项进行编辑,一旦退出编辑状态(如改变焦点),除非再次调用editItem或设置editTriggers触发编辑或打开永久编辑器否则对应项不能再编辑
    • 连续多次调用editItem,中间没有触发事件处理,则只有第一次调用生效,后续调用无效

    4.3.3、openPersistentEditor打开持久编辑器

    上面介绍editItem时说明了editItem只能触发一次编辑,可以说进入临时编辑状态,一旦退出编辑除非再通过相关方式触发编辑否则项不可再编辑。与此相对应,与QTreeWidget类似,QTableWidget还提供了一种一旦打开编辑状态就可以随时再次编辑,除非显示关闭编辑状态,这种方式就是打开持久编辑器。调用方法如下:

    openPersistentEditor(QTableWidgetItem item)

    该方法没有返回值。

    4.3.4、从表格中取并移除项的takeItem方法

    takeItem方法是从表格部件中取一个项返回并从部件中删除该项,调用语法如下:
    QTableWidgetItem takeItem(int row, int column)

    4.3.5、项排序

    项排序是指针对表格部件中的数据行进行排序,调用语法如下:
    sortItems(int column, Qt.SortOrder order = Qt.AscendingOrder)
    排序依据参数column指定列进行。Qt.SortOrder为枚举类,有两个常量值,分别为:AscendingOrder升序,对应数值为0,DescendingOrder为降序,对应数值为1。

    4.4、部件中项的查找和定位方法

    项的查找和定位方法主要是查找特定的项和项的位置属性。

    4.4.1、搜索项

    在表格部件中,可以根据文本以及匹配模式来搜索满足条件的项,调用语法:
    list[QTableWidgetItem] findItems( str text, Qt.MatchFlags flags)

    返回值为所有满足条件的项构成的列表,如果没有找到匹配项,返回空列表。
    Qt.MatchFlags的取值及含义请参考《PyQt(Python+Qt)学习随笔:Model/View中的枚举类 Qt.MatchFlag的取值及含义》。

    4.4.2、访问选中项

    在表格部件根据选择模式的设置,只要选择模式不是NoSelection(关于选择模式继承自QAbstractItemView,请参考《PyQt(Python+Qt)学习随笔:QAbstractItemView的selectionMode属性》),则可以通过操作选中部件中的项。选中的项可以通过方法selectedItems()方法返回,其返回值为一个列表,列表中的每个元素是一个选中的QTableWidget项实例。

    调用语法:

    list[QTableWidgetItem] selectedItems()

    4.4.3、获取指定行和列的项

    根据行和列可以获取对应位置的项,调用语法如下:
    QTableWidgetItem item(int row, int column)

    如果对应位置没有项,则返回None。

    4.4.4、获取指定位置的项

    QTableWidget的itemAt方法通过视口内的坐标点获取对应坐标位置的项,相关调用方法如下:

    • QTreeWidgetItem itemAt( QPoint p)
    • QTreeWidgetItem itemAt(int x, int y)

    通过该方法可以获取到视口上对应坐标所在的项,如果对应坐标位置无项则返回None。

    4.4.5、获取指定项的行和列

    QTableWidget中可以根据项实例去定位项的行和列位置,调用方法如下:

    • int column( QTableWidgetItem item)
    • int row( QTableWidgetItem item)

    如果对应项在QTableWidget表格部件中不存在,则返回-1。

    4.4.5、获取指定逻辑行或列对应的界面可见行号和列号

    4.4.5.1、相关概念

    关于逻辑行、列和可见行、列的概念,在QTableWidget中没有介绍,老猿查了比较多的资料,并经过验证,最终才搞清楚相关概念。这里有几个关键点要说明一下:

    1. 逻辑行和逻辑列是指项本身在部件中存储数据的行和列,也是通过QTableWidgetItem项方法row()和column()返回的值
    2. 界面可见行和列是指在部件上的行号和列号,从部件中第一行和第一列数据开始到指定逻辑行或逻辑列在部件内的序号,但隐藏行和列(这里的隐藏包括通过表头方法hideSection实现、数据在视口外、在可以手工调整行和列大小时将其大小调整到最小不可见)也必须参与在内。这个地方是个坑,说是可见行和可见列,但实际上隐藏未展现的和在视口外的数据都是参与行号和列号的计数
    3. 仅当部件中的行或列通过表头的moveSection交换了行或列数据所在的位置时才出现逻辑行列和可见行列不一致的情况
    4.4.5.2、可见行和列相关方法

    要获取表格部件中的可见行号和列号,调用方法:

    • int visualColumn(int logicalColumn)
    • int visualRow(int logicalRow)
    4.4.5.3、小结
    1. 表格部件QTableWidget的项设置方法(setItem)所使用的行和列是项的逻辑行号和列号,表示数据在表格部件的存储位置,表格部件中绝大多数方法所使用的行和列都是逻辑行和列。
    2. 仅当表格部件的表头使用moveSection方法才会导致表格部件的逻辑行列号可能和可见行列号不一致,行和列的隐藏以及超出视口都不会导致逻辑行列号可能和可见行列号不一致;
    3. 由于隐藏数据也在可见行列中计数,因此这儿的可见行列其实是不准确的,visualColumn和visualRow这两个方法在使用上需要注意。
    4.4.6、获取项所在矩形位置的visualItemRect方法

    QTableWidget的visualItemRect方法返回参数对应项占用视口位置的矩形数据,调用语法如下:
    QRect visualItemRect( QTableWidgetItem item)

    注意:
    1、该方法在item对应项在视口中不可见(即需要滚动才可见)时同样会返回对应的矩形数据,只不过在视口左上角左边或上边的数据对应的横坐标或纵坐标为负数;
    2、如果项对应行或列被通过表头方法hideSection隐藏了,则返回的矩形为一个空矩形(无坐标、长宽数据的矩形)。

    案例:

    在这里插入图片描述
    上图中的QTableWidget部件中第一行数据已经通过hideSection进行了隐藏,第1列数据滚动到视口左边外去了,此时输出所有行第一列的可视矩形,输出结果如下:

        (0,0): PyQt5.QtCore.QRect()
        (1,0): PyQt5.QtCore.QRect(-76, 0, 99, 19)
        (2,0): PyQt5.QtCore.QRect(-76, 20, 99, 19)
        (3,0): PyQt5.QtCore.QRect(-76, 40, 99, 19)
        (4,0): PyQt5.QtCore.QRect(-76, 60, 99, 19)
        (5,0): PyQt5.QtCore.QRect(-76, 80, 99, 19)
        (6,0): PyQt5.QtCore.QRect(-76, 100, 99, 19)
        (7,0): PyQt5.QtCore.QRect(-76, 120, 99, 19)
    

    可以看到第一行第一列因为隐藏了是个空矩形,其他行的第一列x坐标都是负数。

    4.4.7、返回选中项范围的矩形列表

    在QTableWidget对项的操作支持选中多个项的情况下,可以通过方法selectedRanges返回选中矩形的列表,通过方法setRangeSelected将指定矩形范围的项选中或去选中。相关方法调用语法如下:

    • list[QTableWidgetSelectionRange] selectedRanges()
    • setRangeSelected( QTableWidgetSelectionRange range, bool select)
    注:
    • QTableWidgetSelectionRange描述一个基于逻辑项位置连续的矩形范围,其矩形范围的左上角坐标为该范围内左上角逻辑项的逻辑行号和列号,右下角为右下角逻辑项的逻辑行号和列号。注意这里是指逻辑项而不是可见项
    • 由于选中的项可能不连续,因此selectedRanges返回的可能是多个QTableWidgetSelectionRange矩形的列表,每个矩形内是一个连续的选中范围

    4.5、表头操作方法

    QTableWidget表格部件的表头包括水平表头和竖直表头,水平表头每节对应表格的一列,竖直表头对应表格的一行。如图:
    在这里插入图片描述
    上图中的“行”+编号的项就是竖直表头的一个项(也称为1节),“列”+编号的项就是水平表头的一个项(节)。

    4.5.1、设置水平表头的setHorizontalHeaderLabels方法

    setHorizontalHeaderLabels用于一次性顺序设置水平表头多个节显示的文本,调用语法如下:

    setHorizontalHeaderLabels( Iterable[str] labels)

    4.5.2、设置竖直表头的setVerticalHeaderLabels

    setVerticalHeaderLabels用于一次性顺序设置竖直表头多个节显示的文本,调用语法如下:
    setVerticalHeaderLabels( Iterable[str] labels)

    其他的方面与setHorizontalHeaderLabels方法相同。

    4.5.3、访问水平节对应项

    前面介绍了,表头的一个节实际上对应一个项,项的类型与表格部件的项类型相同,都是QTableWidgetItem实例对象。水平节对应项可以通过方法horizontalHeaderItem和setHorizontalHeaderItem方法访问,调用语法如下:

    • QTableWidgetItem horizontalHeaderItem(int column)
    • setHorizontalHeaderItem(int column, QTableWidgetItem item)

    4.5.4、访问竖直节对应项

    类似setHorizontalHeaderItem,QTableWidget提供了verticalHeaderItem、setVerticalHeaderItem方法,调用语法如下:

    • QTableWidgetItem verticalHeaderItem(int row)
    • setVerticalHeaderItem(int row, QTableWidgetItem item)

    相关方法使用与访问水平节类似,在此不重复说明。

    4.5.5、从表头节中取下节对应项

    QTableWidget可以取下表头节对应项并返回,相关方法如下:

    • QTableWidgetItem takeHorizontalHeaderItem(int column)
    • QTableWidgetItem takeVerticalHeaderItem(int row)

    对应节的项被取下后,表头对应节的标签将自动变更为节的序号。

    4.5.6、表头属性相关访问方法

    在Designer中,QTableWidget有如下表头属性可以设置:
    在这里插入图片描述
    相关属性是从QTableView继承过来的,相关访问方法请参考《PyQt(Python+Qt)学习随笔:QTableView的标题表头相关属性》。

    五、本节小结

    QTableWidget表格部件在创建以后,必须设置其包含的行数和列数才能插入项,项的插入通过setItem()进行。表格部件的QTableWidgetItem项是可用、可编辑、可选择和可选中的,并且可以用作拖放操作的源和拖放目标。项初始化后,可以进行选择、复选、编辑、排序等操作,除排序外相关操作会触发对应信号,可以通过信号来获取操作数据信息。

    广告

    老猿关于PyQt的付费专栏《使用PyQt开发图形界面Python应用》只需要9.9元,该部分与第十五章的内容基本对应,但同样内容在付费专栏上总体来说更详细、案例更多。本节内容对应付费专栏的《第二十三章、 Model/View便利类表格部件QTableWidget详解》。如果有兴趣也愿意支持老猿的读者,欢迎购买付费专栏。

    老猿Python,跟老猿学Python!

  • 相关阅读:
    joomla allvideo 去掉embed share
    程序员高效开发的几个技巧
    分布式icinga2安装与使用
    Openstack Murano(kilo)二次开发之添加Volume
    autohotkey在运维中的应用
    快应用之我见
    目前微服务/REST的最佳技术栈
    2016 年终总结
    2015年终总结
    用TypeScript开发了一个网页游戏引擎,开放源代码
  • 原文地址:https://www.cnblogs.com/LaoYuanPython/p/12570426.html
Copyright © 2011-2022 走看看