PyQtInspect : 推荐一个好用的 PythonQT 界面 Debug 工具

2,344 阅读6分钟

一. 前言

这1-2个月太忙了 ,完全没有时间做整理 ,所剩无几的时间也在维护开源项目在,所以这一篇就简简单单的分析一个工具吧。

这个工具是在写开源的时候找了半天找到的 ,虽然还不完美 ,但是已经有很大的帮助了。

二. PyQtInspect 基础使用

写过 PyQT 项目的兄弟 ,应该都有感觉 ,不像开发前端 ,可以在Chrome 上面进行调整。 在纯代码写 PyQT 项目的时候 ,界面上的组件对应代码哪个部分 ,纯粹靠经验和猜。

早期自己的代码可能问题不大 ,但是时间一久 ,就很麻烦了。

PyQtInspect 是什么 ?

@ PyqtInspect | PyqtInspect, a Chrome DevTool-like inspector for PyQt/PySide.

PyQtInspect 是一个强大的 PyQt/PySide 应用程序调试和检查工具 ,其效果如下 :

image.png

效果如上 ,通过 Select 可以选择窗口里面的组件 ,就可以查找到对应的代码位置以及相关信息 ,便于调试。

同时会展示出组件的各种信息 ,属性树 ,便于问题的分析。

使用方式 :

我是 VSCode 里面添加 Task :

        {
            "label": "Run with PyQtInspect",
            "type": "shell",
            "command": "python",
            "args": [
                "-m",
                "PyQtInspect",
                "--direct",
                "--qt-support=PyQt6",
                "--file",
                "${workspaceFolder}/main.py"
            ],
            "group": {
                "kind": "build",
                "isDefault": false
            },
            "problemMatcher": []
        }
  • python -m PyQtInspect --direct --qt-support=PyQt6 --file D:\code\python\ant-tools/main.py

三. 原理浅学一下

主要流程 :

  • 阶段1: 系统启动 ,核心类: ArgHandlerWithParam, PQIWindow, PQYWorker
    • 功能: 解析启动参数,创建服务器主窗口,开始监听指定端口等待客户端连接。支持服务器模式和直连模式。
  • 阶段2: 网络连接 ,核心类: PyDB, Dispatcher, ReaderThread, WriterThread
    • 功能: 客户端连接服务器,建立双向通信通道。每个连接创建独立的调度器和读写线程,实现并发处理。
  • 阶段3: Qt补丁注入,核心类: ThreadWrapper, QProcessWrapper, HighlightController
    • 功能: 动态检测Qt版本,对QThread、QProcess等核心类进行运行时包装,注入调试跟踪功能和事件监听器。
  • 阶段4: 事件监听,核心类: EventListener, EnteredWidgetStack, QWidgetInfo
    • 功能: 实时监听鼠标进入控件事件,维护控件栈,提供红色半透明高亮效果,收集并发送控件详细信息。
  • 阶段5: GUI交互,核心类: HierarchyBar, WidgetPropertiesGetter, CodeWindow
    • 功能: 更新层级导航栏,展示控件属性树,支持远程Python代码执行,提供完整的可视化调试界面。

3.1 注入与Monkey Patching机制

👉 在 pqi_monkey.py 中 ,会通过猴子补丁拦截所有进程创建相关的系统调用,比如 :

  • os.execl, os.execv, os.execve 系列函数
  • os.spawnl, os.spawnv, os.spawnve 系列函数
  • subprocess.Popen 和相关函数
  • Windows 平台的 CreateProcess

👉 同时pqi_monkey_qt_helpers.py还会对函数进行拦截 ,对 Qt 控件的构造函数进行补丁,实现控件创建时的自动注册

  • _new_QWidget_init : 其中会调用原始构造函数,初始化全局事件过滤器

3.2 事件拦截技术

  • 全局事件过滤器pqi_monkey_qt_helpers.py:188-351
    • EventEnum.Enter / EventEnum.Leave /EventEnum.MouseButtonPress
  • 原生事件过滤器 : 针对不同平台的原生事件处理
    • self.WM_NCHITTEST /

3.3 动态代码执行

在目标进程中动态执行 Python 代码,用于实时调试和控件操作 , 核心代码在 pqi_monkey_qt_helpers.py:564-575

通过这部分逻辑 ,可以访问以下资源 :

  1. 全局命名空间: Python 内置函数和模块
  2. 局部命名空间: 当前控件实例 (self)
  3. Qt 模块: PyQt/PySide 的所有功能
  4. 调试器 API: PyQtInspect 提供的调试接口

四. 待扩展的点

  • 由于我是通过 VSCode 编写的 Python , 这里本来可以通过双击定位到代码行的 ,VSCode 则不行。
  • 整体界面的效果还有很大的优化空间

后续时间空出来了 ,就会主要针对这两点在大佬的基础上进行一定的改造。

总计

这一块没什么知识积累 ,看起来很吃力 ,为了后面去二次改造这个,还是磨了一下源码 ,也不知道对不对。

我个人现在只找到这一种方式 ,也不知道有没有更好的工具 ,欢迎大佬推荐。

最后的最后 ❤️❤️❤️👇👇👇

附录

类名作用
FuncWrapper函数包装器,为信号连接注入跟踪功能
StartedSignalWrapperQThread.started信号包装器,注入跟踪逻辑
ThreadWrapperQThread包装器,注入调试跟踪和信号监听
RunnableWrapperQRunnable包装器,为线程池任务注入跟踪
QProcessWrapperQProcess包装器,拦截进程启动并修改参数
HighlightController控件高亮显示管理器,实现红色半透明覆盖
EnteredWidgetStack鼠标进入控件的堆栈跟踪管理器
EventListenerQt事件监听器,处理鼠标和键盘事件
服务器端核心
PQIWindow服务器主窗口,GUI管理和线程交互中心
DirectModePQIWindow直连模式窗口,自动监听指定端口
PQYWorker服务器工作线程,监听连接并创建调度器
DummyWorker占位符类,空对象模式避免空指针检查
Dispatcher单客户端通信处理器,管理完整通信生命周期
DispatchReader调度器专用读取线程,处理URL解码和转发
客户端核心
PyDB客户端核心调试器,管理远程调试会话
TrackedLock线程感知锁,跟踪线程锁状态防止死锁
通信层
CommunicationRole通信角色常量类,定义客户端服务器角色
PyDBDaemonThread守护线程基类,管理所有后台线程
ReaderThread通信读取线程基类,处理Socket数据接收
WriterThread通信写入线程,处理消息队列和发送
NetCommandFactory网络命令工厂,创建各种协议消息
数据结构
QWidgetInfo控件信息数据类,存储完整Qt控件信息
QWidgetChildrenInfo控件子元素信息数据类,存储层级关系
WidgetPropertiesGetter控件属性获取器,支持50+种Qt控件类型
配置和工具
SetupHolder全局配置持有器,定义所有配置键常量
DataCenter数据中心类,管理GUI数据状态和缓存
DataHolder数据持有器,存储GUI运行时数据
KeyboardHookHandler键盘钩子处理器,处理全局快捷键事件
KeyboardHookWinWindows键盘钩子实现,底层键盘事件捕获
StackFrameInfo堆栈帧信息类,存储单个堆栈帧数据

一些比较重要的代码

服务器端监听

class PQYWorker(QtCore.QObject):
    def run(self):
        self._socket = socket(AF_INET, SOCK_STREAM)
        self._socket.bind(('', self.port))
        self._socket.listen(1)
        
        while self._isServing:
            newSock, _addr = self._socket.accept()
            dispatcher = Dispatcher(None, newSock, dispatcherId)
            dispatcher.start()

客户端连接

class PyDB:
    def connect_to_server(self):
        self.writer = WriterThread(sock)
        self.reader = ReaderThread(sock)  
        # 建立双向通信通道

通信通道建立

  • 核心类: Dispatcher, DispatchReader, WriterThread, ReaderThread
  • 消息工厂: NetCommandFactory