当前位置: 首页 > news >正文

压测工具开发实战篇(四)——client子窗口功能

在这里插入图片描述

你好,我是安然无虞。

文章目录

    • 树控件
    • 添加文件
      • 补充学习: 函数定义中循环体里的局部变量
      • 补充学习: 动态添加对象属性
    • 刷新文件
    • 上下文菜单 (右键菜单)
      • 实现右键菜单功能
    • 编辑节点文本

在这里插入图片描述

在学习本篇文章之前, 建议先看一下上篇介绍MDI子窗口的文章:
压测工具开发实战篇(三)——开发MDI子窗口功能

树控件

接下来, 我们想在client子窗口功能中添加 展现所选目录的代码文件的功能, 可以采用QTreeWidget树控件, 所展现的样式类似于下图:
在这里插入图片描述

说明: 我们这里暂时只实现展示的内容都是文件的功能, 以子目录的形式以后实现.

首先我们在 client.ui 子窗口中加入 QTreeWidget 树控件, 并且将对象名设置为 tree_file, 像这样:
在这里插入图片描述
OK, 有了树控件之后, 该怎么将获取到的文件显示到树控件上呢?

其实很简单, 只需要像这样改写代码即可:

# client.py

class Client:
    def __init__(self):
        self.ui = uiLoader.load('./ui/client.ui')

        self.nodeIcon = qta.icon('fa5.user', color='steelBlue')
        # 在树控件上显示文件
        self.list_file_on_tree()

    def list_file_on_tree(self):
        # 清除树上所有文件
        self.ui.tree_file.clear()
        # 这里获取的文件是完整的文件路径名
        pyfiles = glob.glob(os.path.join(SI.projectPath, 'client/*.py'))

        # 隐藏标头栏
        self.ui.tree_file.setHeaderHidden(True)
        # 获取树控件的不可见根结点
        root = self.ui.tree_file.invisibleRootItem()

        for pyf in pyfiles:
            # 获取完整文件路径名的基本文件名
            fname = os.path.basename(pyf)

            # 准备一个树节点
            nodeItem = QTreeWidgetItem()
            # 设置节点图标
            nodeItem.setIcon(0, self.nodeIcon)
            # 设置该节点的第一个列文本
            nodeItem.setText(0, fname)
            # 设置该节点在以前的flag基础上, 多一个可编辑ItemIsEditable
            nodeItem.setFlags(nodeItem.flags() | Qt.ItemIsEditable)
            root.addChild(nodeItem)

这样之后我们看:
在这里插入图片描述
符合预期, 这里需要再补充一个功能就是: 在还没有设置项目目录的时候, 应该在日志输出文本框中显示提示信息, 并且不能打开子窗口, 只有在设置了项目目录之后才能在点击侧边按钮的时候正确打开子窗口显示对应的数据.

如果要实现上面的功能, 只需要在 main.py 文件中的 打开子窗口方法之前判断是否已经设置了当前的项目目录.

# main.py

def load_client_sub_win(self):
	# 打开子窗口之前需要先判断是否已经设置了当前的项目目录
	if not SI.projectPath:
		self.logInfo('请先设置项目目录')
		return
	
	...

这样实现之后, 我们看:
在这里插入图片描述
OK, 符合预期, 继续看接下来的内容.

添加文件

接下来我们在 client.ui 中定义工具栏, 实现 添加客户端代码和刷新客户端代码的功能:
比如我们点击添加的动作:
在这里插入图片描述
会自动帮我们显示输入对话框, 并且给出默认的文件名(并且保证文件名没有重复):

# 设置工具栏条目图标
self.ui.action_addone.setIcon(qta.icon('fa5.file'))
self.ui.action_refresh.setIcon(qta.icon('mdi.refresh'))
# 定义点击工具栏action事件处理
self.ui.action_addone.triggered.connect(self.action_addone)
self.ui.action_refresh.triggered.connect(self.list_file_on_tree)

当点击添加动作时, 会执行 action_addone 信号处理函数:

def action_addone(self):
    # 获得合适的初始名字
    for i in range(1, 1000):
        filename = f'client_{i}.py'
        if not os.path.exists(os.path.join(self.thisFolderPath, filename)):
            break

    while True:
        # 输入对话框 - 返回值分别是输入数据 和 是否点击了 OK按钮
        filename, okPressed = QInputDialog.getText(self.ui, "请输入文件名字", "文件名: ",
                                                   QLineEdit.Normal, filename)
        filename = filename.strip()
        if not okPressed:
            return
        filepath = os.path.join(self.thisFolderPath, filename)
        if os.path.exists(filepath):
            QMessageBox.warning(self.ui, "错误", f"文件{filepath}已经存在, 请重新输入!!!")
        else:
            break
	# 直接创建一个新文件
    open(filepath, 'w', encoding='utf-8')

    parentItem = self.ui.tree_file.invisibleRootItem()
    # 准备一个树节点
    nodeItem = QTreeWidgetItem()
    # 保存原始文件名 - 添加树节点对象的动态属性
    nodeItem._original_filename = filename

    nodeItem.setIcon(0, self.nodeIcon)
    nodeItem.setText(0, filename)
    nodeItem.setFlags(nodeItem.flags() | Qt.ItemIsEditable)
    # 添加到树的不可见根结点下面, 成为第一层节点
    parentItem.addChild(nodeItem)

补充学习: 函数定义中循环体里的局部变量

问题是: 上面的代码中 for循环里 filename 的作用域, 它为什么可以作为 .getText(self.ui, "请输入文件名字", "文件名: ",QLineEdit.Normal, filename)中的参数filename, 不是出了for循环的作用域了吗?

在 for 循环中,filename 是在循环体内部被赋值的, 根据 Python 的作用域规则,如果一个变量在某个代码块(如循环体、if 块等)中被赋值,那么它会被提升到整个函数的作用域中,而不仅仅局限于该代码块.

随意对于整个函数来说:

虽然 filename 是在 for 循环中被首次赋值,但由于它是在函数 action_addone 的作用域内被赋值的,因此它是一个局部变量,可以在整个函数内部被访问.

补充学习: 动态添加对象属性

在上面的代码中, 我们在 创建树节点的时候, 紧接着就为这个新建的树节点添加属性_original_filename, 保留原始文件名, 为了方便后面修改文件名.

我们知道, 本身 QTreeWidgetItem 类没有定义 _original_filename 属性, 我们可以像上面那样直接赋值为其实例添加新的属性, 但是要注意的是最好在新建节点的时候就加上, 防止后面忘记.

所以代码就这样:

# 准备一个树节点
nodeItem = QTreeWidgetItem()
# 保存原始文件名 - 添加树节点对象的动态属性
nodeItem._original_filename = filename

这样做的好处是:

  • 当用户点击某个节点时,你可以通过 item._original_filename 获取到该节点对应的原始文件名.
  • 在处理文件操作(如打开文件、删除文件等)时,可以直接使用 _original_filename 属性来获取文件名,而不需要再从其他地方查找.

刷新文件

定义刷新信号处理方法就很简单了, 直接列出树控件上的所有文件节点即可.

def list_file_on_tree(self):
    # 清除树上所有文件
    self.ui.tree_file.clear()
    # 这里获取的文件是完整的文件路径名
    pyfiles = glob.glob(os.path.join(SI.projectPath, 'client/*.py'))

    # 隐藏标头栏
    self.ui.tree_file.setHeaderHidden(True)
    # 获取树控件的不可见根结点
    root = self.ui.tree_file.invisibleRootItem()

    for pyf in pyfiles:
        # 获取完整文件路径名的基本文件名
        fname = os.path.basename(pyf)
		
		# 树控件需要通过节点来组织和显示数据,而不是直接显示文件路径
        # 准备一个树节点
        nodeItem = QTreeWidgetItem()
        # 保存原始文件名 - 添加树节点对象的动态属性
        nodeItem._original_filename = fname
        # 设置节点图标
        nodeItem.setIcon(0, self.nodeIcon)
        # 设置该节点的第一个列文本
        nodeItem.setText(0, fname)
        # 设置该节点在以前的flag基础上, 多一个可编辑ItemIsEditable
        nodeItem.setFlags(nodeItem.flags() | Qt.ItemIsEditable)
        root.addChild(nodeItem)

上下文菜单 (右键菜单)

现在我们还需要实现右键树节点时的删除操作:
在这里插入图片描述
这就要补充学习上下文菜单 的知识了:

“上下文菜单”指的是在某个特定的上下文(如树节点)中,通过鼠标右键点击而弹出的菜单. 这种菜单通常包含与当前上下文相关的操作选项,例如在树形控件中,右键点击某个节点可能会弹出一个菜单,提供对该节点进行操作的选项,如“添加子节点”、“删除节点”、“重命名节点”等.

实现右键菜单功能

1.设置上下文菜单策略:

通过调用setContextMenuPolicy(Qt.CustomContextMenu)方法,将树形控件的上下文菜单策略设置为自定义模式.

这意味着菜单的显示和内容将由用户自己定义,而不是使用默认的系统菜单.

2.连接信号与槽:

通过customContextMenuRequested.connect(self.show_context_menu_onfiletree),将树形控件的customContextMenuRequested信号连接到一个自定义的槽函数show_context_menu_onfiletree.

当用户右键点击树形控件时,会触发这个信号,进而调用槽函数来显示自定义的上下文菜单.

# 设置树控件上下文策略 - 也可以在 Qt Designer 中设置
self.ui.tree_file.setContextMenuPolicy(Qt.CustomContextMenu)
# 定义信号处理方法
self.ui.tree_file.customContextMenuRequested.connect(self.show_context_menu_on_filetree)

我们来实现这个右键树控件时触发信号调用的槽函数:

def show_context_menu_on_filetree(self, position):
    """右键树控件菜单"""
    tree = self.ui.tree_file

    # 获取当前用户点选的节点
    curItem = tree.currentItem()

    # 没有当前选中节点
    if not curItem:
        print('没有选中节点,返回')
        return

    # 创建 上下文菜单 和 菜单项Action
    menu = QMenu(tree)

    action_delnode = QAction("删除")
    action_delnode.triggered.connect(self.action_delnode)

    menu.addAction(action_delnode)

    # 在鼠标点击处展示上下文菜单
    menu.exec_(tree.mapToGlobal(position))

我们发现触发上面的删除动作会执行 action_delnode 方法:

# 右键菜单的删除节点方法 - 注意别忘了删除电脑中的文件
def action_delnode(self, position):
    tree = self.ui.tree_file

    # 获取当前用户点选的节点
    currentItem = tree.currentItem()

    # 真正的在电脑中把文件删掉
    filepath = os.path.join(self.thisFolderPath, currentItem.text(0))
    try:
        os.remove(filepath)
    except:
        pass

    # 在Qt界面上把文件删掉
    # 找到该节点的父节点
    parentItem = currentItem.parent()
    # 如果没有父节点, 就是不可见的父节点
    if not parentItem:
        parentItem = tree.invisibleRootItem()
    # 删除该节点
    parentItem.removeChild(currentItem)

编辑节点文本

如果我们要实现 双击树节点 可以编辑节点文本, 首先需要在创建节点的时候, 设置节点的flag为: 可编辑 ItemIsEditable

# 准备一个树节点
nodeItem = QTreeWidgetItem()
# 保存原始文件名 - 添加树节点对象的动态属性
nodeItem._original_filename = filename

nodeItem.setIcon(0, self.nodeIcon)
nodeItem.setText(0, filename)

# 设置该节点在以前的flag基础上,多一个可编辑 ItemIsEditable
nodeItem.setFlags(nodeItem.flags() | Qt.ItemIsEditable)

树节点文本被编辑时会触发ItemChanged信号:

# 树节点文本被编辑后会触发ItemChanged信号
self.ui.tree_file.itemChanged.connect(self.item_changed)

对这个信号进行处理:

def item_changed(self, item, column):
    """对文件重命名"""
    new_name = item.text(column)
    # 这个时候就用到了之前为树节点添加的属性_original_filename属性,保存原文件名
    original_name = item._original_filename

    original_path = os.path.join(self.thisFolderPath, original_name)
    new_path = os.path.join(self.thisFolderPath, new_name)
    # 检查新文件名是否已经存在
    if os.path.exists(new_path):
        QMessageBox.warning(self.ui, "错误", f"文件{new_path}已经存在, 请重新输入!!!")
        # 注意需要将名字改为原来的名字
        item.setText(column, original_name)
    else:
        # 如果文件名合法, 更新原始文件名属性
        item._original_filename = new_name
        # 进行文件重命名操作
        os.rename(original_path, new_path)
遇见安然遇见你,不负代码不负卿。
谢谢老铁的时间,咱们下篇再见!

相关文章:

  • 高德地图 3D 渲染-区域纹理图添加
  • IntelliJ IDEA 中通义灵码插件使用指南
  • 【从0到1学MybatisPlus】MybatisPlus入门
  • JS中parseFloat()函数的使用
  • Vue响应式系统的简单实现
  • 机器学习 从入门到精通 day_01
  • 文件存储的路径简单分析
  • 使用Prometheus监控systemd服务并可视化
  • Completablefuture的底层原理是什么
  • 优艾智合人形机器人“巡霄”,开启具身多模态新时代
  • GeoTime:主要用于执法、公共安全和情报分析领域GIS软件
  • AI训练存储架构革命:存储选型白皮书与万卡集群实战解析
  • git `switch` 命令详解与实用示例
  • 图论----拓扑排序
  • MyBatis的第三天笔记
  • VSCode使用Remote-SSH连接服务器时启动失败glibc不符合
  • Java学习——day23(反射的对象创建与方法调用)
  • Text-to-SQL技术深度解析:从理论突破到工程实践
  • Jmeter+Jenkins+Ant自动化持续集成环境搭建
  • [ctfshow web入门] web7
  • 云南陆良县发生一起装载机致人伤亡事件,造成6死4伤
  • 区域、学校、课堂联动,上海浦东让AI素养培育贯穿基础教育全学段
  • 凤阳文旅局回应鼓楼瓦片脱落:鼓楼楼宇系仿古建筑,动工时已履行报批手续
  • 在“三只手”上跳舞:公共政策的科学与艺术——读《市场、国家和民众:公共政策经济学》
  • 东南亚五大经济体一季度增长放缓,美国关税大棒或阻全年增长
  • 大学2025丨苏大教授王尧:文科最大的危机是阐释世界的能力弱化