常用控件4
菜单栏/菜单
菜单栏/菜单 相关的 Python QT 官方文档 链接如下:
需要菜单栏的,通常是 QMainWindow 类型的窗口, 我们可以使用代码创建菜单栏内容,如下
from PySide2 import QtWidgets
from PySide2.QtGui import QIcon
class Window(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.resize(600,200)
# 创建 菜单栏QMenuBar 对象 并返回
menuBar = self.menuBar()
# 一级菜单
fileMenu = menuBar.addMenu("文件")
editMenu = menuBar.addMenu("编辑")
helpMenu = menuBar.addMenu("帮助")
# 一级Action
actionHomePage = menuBar.addAction('主页')
actionHomePage.triggered.connect(self.actionHomePageClicked)
# 一级菜单的 action项
actionAddNode = fileMenu.addAction(QIcon("./Images/folder.png"),"添加")
fileMenu.addSeparator() # 分隔符
actionDelNode = fileMenu.addAction("删除")
actionAddNode.triggered.connect(self.actionAddNodeClicked)
actionDelNode.triggered.connect(self.actionDelNodeClicked)
# 二级菜单
edit_1 = editMenu.addMenu("插入图表")
edit_2 = editMenu.addMenu("插入图片")
# 二级菜单的 action项
action1 = edit_1.addAction("action1")
action2 = edit_1.addAction("action2")
def actionHomePageClicked(self):
print('actionHomePageClicked')
def actionAddNodeClicked(self):
print('actionAddNodeClicked')
def actionDelNodeClicked(self):
print('actionDelNodeClicked')
app = QtWidgets.QApplication([])
ex = Window()
ex.show()
app.exec_()
大家可以运行一下上面的代码,看一下界面。
菜单层级结构是这样的:
菜单栏(QMenuBar) -> 菜单(QMenu) -> QAction
当然,菜单里面还可以有子菜单,就是这样
菜单栏(QMenuBar) -> 菜单(QMenu)-> 子菜单(QMenu) -> QAction
注意:上面说的子菜单,并不是一种新的类型,也是 QMenu
, 只是从属于其他Qmenu, 就像 QLayout
形成的层级关系一样。
菜单里面 点击能触发操作的条目,称之为 QAction
,中文叫 动作
也可以在 Qt Designer上很方便的为 QMainWindow
类型的窗口添加菜单,如下所示
点击 菜单Action, 会触发信号 triggered
, 处理点击菜单的的代码如下
self.ui.actionOpenFile.triggered.connect(self.openPageFile)
工具栏
工具栏的 Python QT 官方文档 链接如下:
工具里面的可以点击的条目 也是 QAction
,和菜单栏一样!
可以用代码创建工具栏的内容,如下
from PySide2 import QtWidgets
from PySide2.QtGui import QIcon
class Window(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.resize(600,200)
# 创建 工具栏 对象 并添加
toolbar = QtWidgets.QToolBar(self)
self.addToolBar(toolbar)
# 添加 工具栏 条目Action
actionAddNode = toolbar.addAction(QIcon("./Images/folder.png"),"添加")
actionAddNode.triggered.connect(self.actionAddNodeClicked)
action = toolbar.addAction("删除")
action = toolbar.addAction("修改")
action = toolbar.addAction("查询")
toolbar.addSeparator() # 添加分隔符
action = toolbar.addAction("帮助")
def actionAddNodeClicked(self):
print('actionAddNodeClicked')
app = QtWidgets.QApplication([])
ex = Window()
ex.show()
app.exec_()
如果 菜单栏 和 工具栏有 相同的 action
,可以公用一个Action对象。
也可以在 Qt Designer上很方便的为 QMainWindow
类型的窗口添加工具栏。
右键点击 Main Window
类型的窗体空白处,如下所示
选择添加工具栏
注意,只有 Main Window
类型的窗体,才能添加工具栏,如下
添加工具栏后,还要在工具栏上添加 条目Action
(中文称之为: 动作
)。
方法是点击右下角 动作编辑器
,新建动作,如下图所示
然后如下图所示进行设置
添加动作成功后,就可以直接拖到工具栏上了。
然后,在代码中定义动作触发后的处理函数,如下所示
self.ui.actionAddNote.triggered.connect(self.actionAddNode)
如果菜单和工具栏有 相同的 action
,通常是先在 动作编辑器 创建一个action, 然后分别拖动到 菜单 和 工具栏
状态栏
状态栏通常显示在窗口底部,对应的控件类型是: QStatusBar
需要底部状态栏的,通常是 QMainWindow
类型的窗口, 用 Qt Designer 设计的Qt Designer, 会自带状态栏,缺省属性名称为 statusbar
。
要在状态栏显示文本信息,只需要调用 状态栏 QStatusBar 的 showMessage
方法,如下
self.ui.statusbar.showMessage(f'打开文件{filePath}')
提示框
QMessageBox
类可以用来弹出各种提示框
该类可以通过一系列静态方法,显示 如下弹出框
- 错误报告
使用 critical
方法
from PySide2.QtWidgets import QMessageBox
QMessageBox.critical(
self.ui,
'错误',
'请选择爬取数据存储路径!')
- 警告
使用 warning
方法
from PySide2.QtWidgets import QMessageBox
QMessageBox.warning(
self.ui,
'阅读太快',
'阅读客户协议必须超过1分钟')
- 信息提示
使用 information
方法
from PySide2.QtWidgets import QMessageBox
QMessageBox.information(
self.ui,
'操作成功',
'请继续下一步操作')
也可以使用 about
方法
from PySide2.QtWidgets import QMessageBox
QMessageBox.about(
self.ui,
'操作成功',
'请继续下一步操作')
- 确认继续
使用 question
方法
from PySide2.QtWidgets import QMessageBox
choice = QMessageBox.question(
self.ui,
'确认',
'确定要删除本文件吗?')
if choice == QMessageBox.Yes:
print('你选择了yes')
if choice == QMessageBox.No:
print('你选择了no')
输入对话框
QInputDialog
输入对话框 只让用户输入一行数据信息,比如 姓名、年龄等。
可以方便的用来获取简单的信息。
比如
from PySide2.QtWidgets import QInputDialog,QLineEdit
# 返回值分别是输入数据 和 是否点击了 OK 按钮(True/False)
title, okPressed = QInputDialog.getText(
self,
"输入目录名称",
"名称:",
QLineEdit.Normal,
"")
if not okPressed:
print('你取消了输入')
常用的方法有:
-
getText
弹出对话框,让用户输入 单行文本
-
getMultiLineText
弹出对话框,让用户输入 多行文本
-
getInt
弹出对话框,让用户输入 整数
-
getItem
弹出对话框,让用户选择 选项
items = ["春天", "夏天", "秋天", "冬天"] item, ok = QInputDialog().getItem(self, "请选择", "季节:", items, 0, False) if ok and not item.isEmpty(): itemLabel.setText(item)
剪贴板
Qt程序可以获取和设置剪贴板内容
from PySide2.QtGui import QGuiApplication
cb = QGuiApplication.clipboard()
# 获取剪贴板内容
originalText = cb.text()
# 设置剪贴板内容
clipboard.setText(newText)
树控件
QTreeWidget 树控件
树控件, 是和 QTreeWidgetItem 树节点控件
结合使用的。
如下图所示
本节内容的视频讲解 在老师发你的 实战班内部补充学习视频网盘
里的 树控件
目录中。
说明
树控件用来展示树状层级结构的数据,典型例子有: 文件目录结构、知识点结构
设置属性
如果用 Qt 设计师 工具 制作界面,首先应该设置好 QTreeWidget 的一些属性,如下
其中,常见的设置是 :
-
设置有几个column ,也就是有几列,
双击树控件,就可以编辑列名,如下图
-
可以设置列标头 是否可见
添加节点
设置好 QTreeWidget 的属性以后,代码中加载界面资源文件,就可以在树上添加节点了,如下代码所示
注意:QTreeWidget 有一个不可见根节点 ,树的第一层节点 都是这个 不可见根节点的 子节点,它是整个树的根。 可以通过 invisibleRootItem 方法获得。
from PySide2.QtWidgets import QApplication
from PySide2.QtUiTools import QUiLoader
# 导入 QTreeWidget, QTreeWidgetItem, QIcon
from PySide2.QtWidgets import QTreeWidget, QTreeWidgetItem
from PySide2.QtGui import QIcon
class SomeWindow:
def __init__(self):
self.ui = QUiLoader().load('main.ui')
# 调用设置树控件
self.setupTree()
def setupTree(self):
# 获取树控件对象
tree = self.ui.tree
# 隐藏标头栏
tree.setHeaderHidden(True)
# 获取树控件的不可见根节点
root = tree.invisibleRootItem()
# 准备一个folder节点
folderItem = QTreeWidgetItem()
# 创建图标对象
folderIcon = QIcon("./Images/folder.png")
# 设置节点图标
folderItem.setIcon(0, folderIcon)
# 设置该节点 第1个column 文本
folderItem.setText(0, '学员李辉')
# 添加到树的不可见根节点下,就成为第一层节点
root.addChild(folderItem)
# 设置该节点为展开状态
folderItem.setExpanded(True)
# 准备一个 叶子 节点
leafItem = QTreeWidgetItem()
leafIcon = QIcon("./Images/leaf.gif")
# 设置节点图标
leafItem.setIcon(0, leafIcon)
# 设置该节点 第1个column 文本
leafItem.setText(0, '作业 - web自动化1')
# 设置该节点 第2个column 文本
leafItem.setText(1, '提交日期20200101')
# 添加到 叶子节点 到 folerItem 目录节点下
folderItem.addChild(leafItem)
app = QApplication([])
w = SomeWindow()
w.ui.show()
app.exec_()
上面的程序运行后,大概的结果图如下
注意:
-
每个 节点 都是
QTreeWidgetItem
对象 -
添加节点 必须通过 该节点的
父节点
可以使用
addChild
方法,添加到最后也可以使用
insertChild
方法,插入到指定位置,比如folderItem.insertChild(2, leafItem)
就插入到 第3个 子节点的位置上,因为第1个子节点的索引是0。
-
如果控件宽度不够,字符串会显示为省略号。
列宽度
列的宽度,可以在界面上 该 QTreeWidget
控件的属性编辑里面, 调整 headerDefaultSectionSize
和 headerStretchLastSection
来控制。
也可以通过代码来控制,如下
def setupTree(self):
# 其他代码...
tree.setColumnWidth(0, 200) # 设置第一列宽度,200px
header = tree.header()
header.setStretchLastSection(True)
如果想让列宽度 自适应 内容,使用 下面的方法
from PySide2.QtWidgets import QHeaderView
def setupTree(self):
# 其他代码...
header = tree.header()
# 让列宽度 自适应 内容
header.setSectionResizeMode(QHeaderView.ResizeToContents)
当前选中节点
我们有时要在当前节点下面添加节点、删除当前节点,这就需要先获取当前节点。
QTreeWidget
的 currentItem
方法可以获取当前节点。
下面是一段 在选中节点下面添加子节点 的示例代码
from PySide2.QtWidgets import QLineEdit, QInputDialog
from PySide2.QtCore import Qt
class SomeWindow:
def __init__(self):
self.ui = QUiLoader().load('main.ui')
self.ui.btn_add.clicked.connect(self.addChildNode)
# 其他部分代码
....
def addChildNode(self):
# 获取树控件对象
tree = self.ui.tree
# 获取当前用户点选的节点
currentItem = tree.currentItem()
# 没有当前选中节点,不可见根节点作为当前节点
if not currentItem:
currentItem = tree.invisibleRootItem()
# 当前节点的第一列文本内容
col1Text = currentItem.text(0)
print(col1Text)
# 让用户输入信息
title, okPressed = QInputDialog.getText(
self.ui, "输入名称", "名称:",
QLineEdit.Normal, "")
if not okPressed or title == '':
return
# 准备一个folder节点
folderItem = QTreeWidgetItem()
# 创建图标对象
folderIcon = QIcon("./images/folder_close.png")
# 设置节点图标
folderItem.setIcon(0, folderIcon)
# 设置该节点 第1个column 文本
folderItem.setText(0, title.strip())
# 设置该节点在以前的flag基础上,多一个可编辑 ItemIsEditable
folderItem.setFlags(folderItem.flags() | Qt.ItemIsEditable)
currentItem.addChild(folderItem)
currentItem.setExpanded(True)
获取父节点
获取一个节点的父节点是调用 该QTreeWidgetItem节点的 parent
方法
如下
# 获取某个节点item的父节点item
parentItem = treeItem.parent()
# 如果返回值为None,其必定为顶层节点,它的父节点是不可见的根节点
if not parentItem:
parentItem = tree.invisibleRootItem()
删除节点
删除 QTreeWidget 的节点, 是 通过其父节点调用 removeChild
方法。
示例代码如下:
# 获取当前用户点选的节点
currentItem = tree.currentItem()
# 如果没有当前选中节点
if not currentItem:
return
# 找到改节点的父节点
parentItem = currentItem.parent()
# 如果没有父节点,就是不可见父节点
if not parentItem:
parentItem = tree.invisibleRootItem()
# 删除该节点
parentItem.removeChild(currentItem)
注意:删除一个节点,同时也就删除了其所有子节点。
删除所有节点
可以使用 QTreeWidget 的方法 clear,如下
tree.clear()
也可以删除不可见根节点的所有子节点的方法,如下
root = tree.invisibleRootItem()
for i in reversed(range(root.childCount())):
root.removeChild(root.child(i))
编辑节点文本
假设我们要 实现,双击节点文本 可以 编辑节点文本,就是如下所示
首先必须设置节点flag为 : 可编辑 ItemIsEditable
。
可以在创建节点的时候设置,如下代码所示
from PySide2.QtCore import Qt
# 准备一个folder节点
folderItem = QTreeWidgetItem()
folderItem.setIcon(0, folderIcon)
folderItem.setText(0, '白月黑羽学员李辉')
# 设置该节点在以前的flag基础上,多一个可编辑 ItemIsEditable
folderItem.setFlags(folderItem.flags() | Qt.ItemIsEditable)
信号处理
QTreeWidget 常见的信号有:单击节点、双击节点、当前节点变动 等等。
详细信息参考官方文档
大家可以根据自己的需要,定义信号处理函数
比如,可以这样定义节点 被点击事件
的处理函数
class SomeWindow:
def __init__(self):
self.ui = QUiLoader().load('main.ui')
# 添加 被点击事件 的处理函数
self.ui.tree.itemClicked.connect(self.itemClicked)
# 参数 item 是被点击节点对应的 QTreeWidgetItem 对象
# 参数 column 是被点击的column号
def itemClicked(self, item, column):
# 获取被点击的节点文本
clickedText = item.text(column)
if 'bottle' in clickedText:
self.bottle()
elif 'cube' in clickedText:
self.cube()
再看一个例子,可编辑的节点文本被编辑改变后,其所属 QTreeWidget 就会触发 itemChanged
信号
我们应该定义该信号的处理函数,如下所示
class SomeWindow:
def __init__(self):
self.ui = QUiLoader().load('main.ui')
# 必须先加载树,然后再设置信号处理,
# 否则加载过程也会触发 itemChanged 信号
self.ui.tree.itemChanged.connect(self.itemChanged)
def itemChanged(self, item, column):
# 参数 item 是修改的 节点 QTreeWidgetItem对象
# 参数 column 是修改的 列号
newText = item.text(column)
# 根据改变的 column 得知是哪个数据修改了
# 保存到对应的数据库表记录中
节点关联数据
Python中, 类对象可以动态添加属性,
所以我们可以根据需要,给 QTreeWidgetItem 节点对象 关联 各种类型的数据。
如下所示:
nodeitem = QTreeWidgetItem()
nodeitem.setText(0, filename)
# python 对象可以动态添加属性
nodeitem._my_data1 = '给节点关联的数据,根据需要设定'
nodeitem._my_data2 = ['其它数据1','其它数据2']
这些关联的数据,可以在后续操作节点时,根据需要取出来使用。
关联数据又什么用呢? 想象这样一个案例:
我们用树控件显示一个目录下的文件。
当用户双击节点,修改了文件的名字,代码应该 在 itemChanged 信号处理函数中真正修改该文件名。
但是如果,用户修改的名字已经存在同名文件,就会产生冲突,这时,应该恢复该节点的名字为原来的名字。
但是,界面上改为新名字了,原来名字丢失了怎么办?
这时就可以使用 关联数据的方案:
- 添加节点时,除了 这样 设置文件名,
nodeitem.setText(0, filename)
再给节点动态添加一个属性,比如
nodeitem._my_filename = filename
备份一个文件名的记录
后面如果修改节点名方法,发现名字冲突了,就改回原来的名字
# 修改节点名
def treeItemNameChanged(self, item, column):
newName = item.text(column)
filepath = os.path.join(self.thisFolderPath, newName)
# 文件名冲突,还原
if os.path.exists(filepath):
QMessageBox.information(
self.ui, '错误',
f'文件 {filepath} 已经存在,请重新修改')
# 原来的名字保存在自己定义的 _my_filename 属性中
item.setText(0, item._my_filename)
return
# 不冲突的后续处理
oriFilePath = os.path.join(self.thisFolderPath, item._my_filename)
# 修改文件名
os.rename(oriFilePath, filepath)
获取节点的子节点
QTreeWidgetItem
的 childCount()
方法可以获取子节点的个数
QTreeWidgetItem
的 child()
方法可以获取子节点对应的 QTreeWidgetItem
对象,参数就是子节点的编号,从0开始。
tree.child(0)
就是获取第1个子节点
下面这段代码就是打印某个树节点的 所有直接子节点
文本
def printChildren(parent):
child_count = parent.childCount()
for i in range(child_count):
item = parent.child(i)
print(item.text(0))
如果要打印某个树节点的 所有后代节点
文本,最方便的就是用递归
def printChildren(parent):
child_count = parent.childCount()
for i in range(child_count):
item = parent.child(i)
print(item.text(0))
printChildren(item) # 调用自身,进行递归
上下文菜单
要用 树节点右键菜单功能,可以这样处理:
先这样做初始化设置
from PySide2.QtWidgets import QMenu, QAction
from PySide2.QtCore import Qt
# 其它代码
...
def __init__(self):
# 其它初始化代码
...
# 设置上下文菜单策略,也可以在 Qt Designer上设置
self.ui.tree.setContextMenuPolicy(Qt.CustomContextMenu)
# 定义信号处理方法
self.ui.tree.customContextMenuRequested.connect(
self.show_context_menu_onfiletree)
然后,定义信号处理方法
def show_context_menu_onfiletree(self, position):
tree = self.ui.tree
# 获取当前用户点选的节点
curItem = tree.currentItem()
# 没有当前选中节点
if not curItem:
print('没有选中节点,返回')
return
# 创建 上下文菜单 和 菜单项Action
menu = QMenu(tree)
action_delnode = QAction("删除")
action_delnode.triggered.connect(self.action_delnode)
action_addnode = QAction("添加子节点")
action_addnode.triggered.connect(self.action_addChildNode)
menu.addAction(action_delnode)
menu.addAction(action_addnode)
# 在鼠标点击处展示上下文菜单
menu.exec_(tree.mapToGlobal(position))
# 菜单项Action对应的处理函数
def action_delnode(self, position):
# 这些写处理代码
# 菜单项Action对应的处理函数
def action_addChildNode(self, position):
# 这些写处理代码
MDI 多个子窗口
QMdiArea
提供了一个主窗口区,里面可以存放多个 QMdiSubWindow
子窗口
如图:
本节内容的视频讲解 在老师发你的 实战班内部补充学习视频网盘
里的 MDI 多个子窗口
目录中。
QMdiSubWindow
里面的子窗口,就是我们前面讲解的 QMainWindow
。
可以把任何前面示例的窗口作为 子窗口里面的内容。
可以直接用代码 创建有QMdiArea的主窗口界面,和 子窗口界面
示例代码如下
from PySide2 import QtCore, QtWidgets
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.resize(600, 400)
self.setWindowTitle('主窗口')
# 顶层布局
centralWidget = QtWidgets.QWidget(self)
self.setCentralWidget(centralWidget)
topLayout = QtWidgets.QVBoxLayout(centralWidget)
# 添加一个 Label
topLayout.addWidget(QtWidgets.QLabel('下面是MDI区域'))
# 创建 MDI 区域
self.mdiArea = QtWidgets.QMdiArea()
topLayout.addWidget(self.mdiArea)
# 创建子窗口对象1
self.subWin1 = QtWidgets.QMdiSubWindow()
# 设置子窗口里面的 的功能窗口
self.subWin1.setWidget(Window1())
# 把子窗口加入到 MDI 区域
self.mdiArea.addSubWindow(self.subWin1)
# 创建子窗口对象2
self.subWin2 = QtWidgets.QMdiSubWindow()
self.subWin2.setWidget(Window2())
self.mdiArea.addSubWindow(self.subWin2)
class Window1(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
# 顶层布局
centralWidget = QtWidgets.QWidget(self)
self.setCentralWidget(centralWidget)
topLayout = QtWidgets.QVBoxLayout(centralWidget)
# 添加一个 Label
topLayout.addWidget(QtWidgets.QLabel('该窗口实现 设备管理功能'))
class Window2(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
# 顶层布局
centralWidget = QtWidgets.QWidget(self)
self.setCentralWidget(centralWidget)
topLayout = QtWidgets.QVBoxLayout(centralWidget)
# 添加一个 Label
topLayout.addWidget(QtWidgets.QLabel('该窗口实现 计费功能'))
app = QtWidgets.QApplication()
window = MainWindow()
window.show()
app.exec_()
也可以采用 QUiLoader().load
加载界面的方式,如下
from PySide2 import QtCore, QtWidgets
from PySide2.QtUiTools import QUiLoader
# 这个 func 里面的 SomeWindow 也是一个QUiLoader().load加载ui的类
# 其实例属性 ui 对应一个 QMainWindow
from func import SomeWindow
class MainWindow:
def __init__(self):
self.ui = QUiLoader().load('main.ui')
# 创建子窗口对象
self.subWin = QtWidgets.QMdiSubWindow()
# 设置子窗口里面的 的功能窗口
self.funcWin = SomeWindow()
'''
注意,上面这里不能用 funcWin = SomeWindow() 这样的局部变量保存
因为下面 setWidget 保存的是 SomeWindow的ui属性对应的窗口对象,
而不是 SomeWindow 对象本身
'''
self.subWin.setWidget(self.funcWin.ui)
# 把子窗口加入到 MDI 区域
self.ui.mdiArea.addSubWindow(self.subWin)
app = QtWidgets.QApplication()
window = MainWindow()
window.ui.show()
app.exec_()
也可以不是初始化界面就创建MDI子窗口,而是某个信号处理中,创建子窗口界面。
关键代码差不多的,就是最后要有把子窗口提到最上层显示的代码,
如下
from PySide2.QtWidgets import QMdiArea,QMdiSubWindow
from PySide2.QtCore import Qt
class MainWindow:
def __init__(self):
# 一些主窗口的初始化代码
# 信号处理代码
def clickAddSomeWindow(self):
# 创建子窗口对象,代码和前面差不多
self.subWin = QtWidgets.QMdiSubWindow()
self.funcWin = SomeWindow()
self.subWin.setWidget(self.funcWin.ui)
self.ui.mdiArea.addSubWindow(self.subWin)
# 显示子窗口
subWindow.show()
# 子窗口提到最上层,并且最大化
subWindow.setWindowState(Qt.WindowActive|Qt.WindowMaximized)
上面3D模型图图所示的例子是使用 pythonocc 3D 模型库开发的一个例子。
对应的代码包点击这里下载:
链接:https://pan.baidu.com/s/1N78n5ZJSqNxSs-tjrt6IOw 提取码:byhy
要能正确运行,需要按照如下步骤安装环境
-
先下载 miniconda
因为 pythonocc 包 只能通过conda 下载,不支持pip
点击这里下载 https://repo.anaconda.com/miniconda/Miniconda3-latest-Windows-x86_64.exe
(注意;清华源的pythonocc必须64位才能找到pkg)
安装好以后,打开 conda shell,执行下面的操作
-
添加 conda 清华大学的国内源
因为国外conda 源安装太慢了,所以要添加 conda 清华大学的国内源
执行命令
conda config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/
-
安装 pythonocc-core
conda install -c conda-forge pythonocc-core=7.4.0
-
安装 pyside2
这是 miniconda 里面Python解释器环境中安装 pyside2
执行命令 pip install pyside2