在调试PyQt程序中,对于一些更底层所诱发的问题,有时候PyCharm无法捕捉到此类问题,通常需要在C/C++层面的Qt或sip进行调试。 而对于RiverBank的PyQt5,由于其并没有提供Debug版本的库,因此无法通过windbg等调试器对更底层的Qt或sip进行调试。本文以此为出发点,以Qt 5.15.2 64位为例,介绍如何手动构建PyQt5的各个层次,以及如何在VSCode中使用WinDbg进行调试。
这是一个耗时巨大的过程…
环境
-
Qt:5.15.2 (因为5.15.3及以上版本需要自行编译,这里为了方便,选择5.15.2)
-
操作系统:Windows 11
-
Visual Studio 2022
-
Python 3.11 (64位)
层次概述
PyQt5以sip为基础封装Qt的C++库。总的来说,PyQt5的层次大概如下:
+------------------------------------------+
| Python Interfaces |
+-------------------^----------------------+
|
+-------------------^----------------------+
| PyQt5(pyd) | # Step 5
+-------------------^----------------------+
|
+-------------------^----------------------+
| sip | # Step 4
+---------^-------------------------^------+
| |
+---------^--------+ |
| Qt | # Step2, 3 |
+------------------+ |
|
+---------^--------+
| CPython | # Step 1
+------------------+1. 安装带符号文件的Python
在官网上下载Python的安装包,并勾选Download debugging symbols和Download debug binaries,如图1所示:

图1:Python安装截图
为了能定位到具体的源码,可以在相同的下载页面下载Python的源码,如图2所示。将下载下来的源码压缩包解压到某个目录下,以便后续调试。

图2:Python源码下载地址
当然,也可以选择自己构建Python,构建方法可参见官方教程,这里不再赘述。
2. 下载Qt
随后我们需要安装Qt,为方便构建,在Qt官网上下载无需编译的在线安装程序(Qt目前仅提供5.15.2的安装包,5.15.3及以上版本需要自行构建),安装时在组件上务必选择Qt的源码(Sources),如图3所示。

图3:Qt安装截图
此时不断下一步即可,安装完成后,在Qt安装目录下,可以找到Qt的源码(<Qt安装路径>\5.15.2\Src),以及Qt的构建工具qmake(<Qt安装路径>\5.15.2\msvc_2019_64\bin)。
3. 绑定所下载Qt的dll至PyQt5
3.1 首先下载PyQt 5.15.2的wheel包(注意是64位,一般命名为PyQt5-5.15.2-5.15.2-cp35.cp36.cp37.cp38.cp39-none-win_amd64.whl),并解压至某个目录(为便于后续描述,这里假设为A)。
3.2 进去上述目录A,输入命令python -m venv buildenv构建虚拟环境,并激活虚拟环境.\buildenv\Scripts\activate。
3.3 使用pip install PyQt-builder安装PyQt-builder。
3.4 由于pyqt-bundle对Qt 5.15.2的支持有点问题,需要修改buildvenv\Lib\site-packages\pyqtbuild\bundle中的abstract_package.py文件,按图4修改get_target_qt_dir方法:

图4:修改abstract_package.py文件
3.5 通过以下命令绑定Qt的dll至PyQt5:
pyqt-bundle --verbose --qt-dir <你的Qt安装路径>\5.15.2\msvc2019_64 .\PyQt5-5.15.2-5.15.2-cp35.cp36.cp37.cp38.cp39-none-win_amd64.whl3.6 同理,下载PyQtWebEngine 5.15.2的wheel包,并解压至相同目录A。并通过以下命令绑定:
pyqt-bundle --verbose --qt-dir <你的Qt安装路径>\5.15.2\msvc2019_64 .\PyQtWebEngine-5.15.2-5.15.2-cp35.cp36.cp37.cp38.cp39-none-win_amd64.whl3.7 安装上述绑定后的wheel包,需要注意的是,由于我的Python版本是3.11,直接安装会报错(如图5),因为PyQt的版本5.15.2有点旧了,元信息上没有加上后续的py版本,需要在两个whl包名后面加上cp310,cp311就好啦:

图5:pip安装报错截图
# 重命名whl包,加上cp310,cp311
mv PyQt5-5.15.2-5.15.2-cp35.cp36.cp37.cp38.cp39-none-win_amd64.whl PyQt5-5.15.2-5.15.2-cp35.cp36.cp37.cp38.cp39.cp310.cp311-none-win_amd64.whl
mv PyQtWebEngine-5.15.2-5.15.2-cp35.cp36.cp37.cp38.cp39-none-win_amd64.whl PyQtWebEngine-5.15.2-5.15.2-cp35.cp36.cp37.cp38.cp39.cp310.cp311-none-win_amd64.whl
# pip安装
pip install .\PyQt5-5.15.2-5.15.2-cp35.cp36.cp37.cp38.cp39-none-win_amd64.whl
pip install .\PyQtWebEngine-5.15.2-5.15.2-cp35.cp36.cp37.cp38.cp39-none-win_amd64.whl4. 构建sip
官方的PyQt5-sip并不提供pdb符号文件,如果需要通过Attach调试C++层级的源码时,仅用官方的sip二进制文件是无法调试里面的内容的。
首先下载官方的源码分发包(Source Distribution),并解压到本地的一个目录上,用编辑器打开setup.py,新增以下代码:
# 下面两行是新增的部分
ext_compile_args = []
ext_link_args = []
if sys.platform == 'win32':
module_src.append('bool.cpp')
# 以下两行是新增的部分
ext_compile_args.append('/Z7')
ext_link_args.append('/DEBUG')
# 以下一行是修改的部分
module = Extension('PyQt5.sip', module_src, extra_link_args=ext_link_args, extra_compile_args=ext_compile_args)以PyQt5-sip 12.13.0为例,最终代码文件为:
# ... 省略官方注释
import glob
import sys
from setuptools import Extension, setup
# Build the extension module.
module_src = sorted(glob.glob('*.c'))
ext_compile_args = []
ext_link_args = []
if sys.platform == 'win32':
module_src.append('bool.cpp')
ext_compile_args.append('/Z7')
ext_link_args.append('/DEBUG')
module = Extension('PyQt5.sip', module_src, extra_link_args=ext_link_args, extra_compile_args=ext_compile_args)
# Do the setup.
setup(
name='PyQt5_sip',
version='12.13.0',
license='SIP',
python_requires='>=3.7',
ext_modules=[module]
)打开终端(命令提示符 or Powershell),构建sip
python setup.py build在目录的build\lib.xxxxx\PyQt5上,找到pyd文件和pdb文件,将其替换到Python安装目录的Lib\site-packages\PyQt5上(原来的sip的pyd文件可按需备份一下)。
5. 构建PyQt5
至此,我们已经可以调试Qt的C++层,CPython层,以及sip层,但我们还是会发现有些地方是没办法找到符号文件的(如图6),这是因为PyQt5的pyd文件并没有符号文件。

图6:Qt相关的pyd没有符号文件
其实,第3步只是将我们的Qt的dll绑定到PyQt5的pyd文件上,并没有真正构建PyQt5的pyd文件,这些pyd文件是RiverBank构建的。如果我们想看看sip内部对Qt具体做了什么封装,还需要手动构建PyQt5的pyd文件,并输出调试符号。
首先去PyPI下载PyQt5 5.15.2的源码分发包(注意是Source Distribution),然后解压到本地某个目录上(同样,为了方便描述,下文将该目录描述为B),用编辑器打开project.py,找到方法PyQt.update,在该方法的末尾,新增三行以使得构建时生成符号文件:
# ...其他内容
class PyQt(PyQtProject):
# ...其他内容
def update(self, tool):
# ...其他内容
# !!! 在build方法的末尾新增以下三行 !!!
for binding in self.bindings.values():
binding.extra_compile_args.extend(['/Z7'])
binding.extra_link_args.extend(['/DEBUG'])在目录B上,打开Developer PowerShell for VS 2022,构建PyQt5:
sip-build --verbose --tracing --disable QtNfc在这里,我们使用--verbose以输出更多信息,--tracing以在运行程序时可输出更多的调试信息,--disable QtNfc以禁用QtNfc模块,因为QtNfc模块会存在导致构建失败的问题。
我们之所以不用--debug选项,而选择直接编辑project.py文件,是因为--debug选项需要Python的debug版本(如下图所示),这样会导致一连串的后续问题:python_d.exe的运行需要一系列的debug版本的dll,而这些dll往往都找不到。相关报错如图7所示。

图7:sip-build的--debug选项需要Python的debug版本
注:如果出现图8所示的解码错误,这往往是我们的代码页并非
utf-8导致的。图8:构建
PyQt5时的解码错误我目前的做法是直接修改
sip-build的源码…根据输出找到报错的地方,使用编辑器打开,找到方法Project.read_command_pipe,在with subprocess.Popen语句内,修改如图9:图9:修复编码错误
在这里,如果使用
utf-8无法解码子进程输出的字节串,就再尝试使用gb2312解码一遍(如果还是不行就使用替换字符),具体用哪个编码取决于自己系统的配置。
构建完成后,在build目录内每个模块都会生成对应pyd和pdb文件,将这些文件直接复制到Python的Lib\site-packages\PyQt5目录下,重启Python即可。
6. 在VSCode中调试
此时大功告成,我们可以愉快地调试任一层次的源码了!
以VSCode为例,我们将上述各个层次的源码目录统统放在一个workspace上,新增Launch配置,编写如下:
{
"version": "0.2.0",
"configurations": [
{
"name": "(Windows) Attach",
"type": "cppvsdbg",
"request": "attach",
"processId": "${command:pickProcess}",
// 下面这行是新增的部分, 用于指定pdb符号文件的路径, 注意修改为自己的路径
"symbolSearchPath": "C:\\Qt\\5.15.2\\msvc2019_64\\bin;E:\\PyQt5_sip-12.13.0\\build\\lib.win-amd64-cpython-311\\PyQt5",
"sourceFileMap": {
// Qt源码路径映射, 因为符号文件是下载下来的, 并非自己编译的, 所以需要构建映射
"c:\\Users\\qt\\work\\qt\\": "C:\\Qt\\5.15.2\\Src",
// Python源码路径映射, 理由同上
"D:\\a\\1\\s\\python\\": "C:\\Users\\Jeza\\Downloads\\Python-3.11.7\\Python",
"D:\\a\\1\\s\\Objects\\": "C:\\Users\\Jeza\\Downloads\\Python-3.11.7\\Objects",
"D:\\a\\1\\s\\Include\\": "C:\\Users\\Jeza\\Downloads\\Python-3.11.7\\Include",
}
}
]
}此时再Attach到一个Python进程上,就可以愉快地调试了!如图10所示:

图10:最终效果

