1. 开篇:为什么我最终选了PySide这条技术路线
先说个背景。去年我接了一个内部的"Excel发票查重"小工具需求,要求能拖拽导入Excel、自动标记重复项、还要生成一份带颜色的结果表格。第一反应是用纯Python加openpyxl写个脚本,但同事明确说"不想天天开着黑框敲命令"。于是桌面GUI就成了刚需,候选方案无非是PySide、PyQt、Tkinter、wxPython这几个。
Tkinter虽然内置,但控件审美实在太原始,做个像样点的表格要手工拼半天;wxPython文档老、社区活跃度一般;真正让我纠结的是PySide和PyQt。这两个东西本质上是同一份C++ Qt库的Python绑定,API几乎一样,但背后差异不小,尤其是许可协议。PyQt用的是GPL或商业双授权,PySide则是LGPL协议。
这个区别放在企业内部工具场景里非常实际:PyQt如果不开源、不买商业授权,打包分发给同事用是有法律风险的;而PySide的LGPL允许动态链接方式闭源分发,只要你不动Qt库本身的源码、遵循保留版权声明的义务,就能正常工作。对于我这种写内部工具、偶尔接外包的人来说,PySide是最省心的选择。
再说另一个现实因素:PySide6是Qt官方自己维护的绑定库,C++ Qt每个新版本发布后,PySide6基本同步跟进,文档直接挂在Qt官网上。相比之下PyQt是Riverbank Computing这个第三方团队在做,质量也很好,但我个人更倾向官方维护的东西。除此之外,PySide6的API命名也更贴近C++ Qt原生风格,遇到拿不准的类,可以直接去翻C++ Qt文档,逻辑是通的。
这套step by step系列,我会沿着我实际做项目的流程来写:环境搭建、第一个窗口、信号槽、布局样式、然后是很多新手最头疼的打包。整个系列的核心目标就一个——让你照着走一遍,手里能拿出一个能跑、能打包、能交付的桌面程序。
2. 环境准备:从裸Python到第一个能跑的PySide6窗口
很多人学PySide第一步就栽在环境上。不是装不上,而是装完了一堆东西混在一起,今天这个项目要PySide6,明天那个项目要PyQt5,后天pip升级把某个依赖搞炸了,然后就陷入"配环境两小时,写代码五分钟"的死循环。我的建议是:从第一天开始就用虚拟环境,不要有任何例外。
2.1 虚拟环境是第一道护城河
Python 3.3以后自带venv模块,不需要额外装virtualenv。Windows下操作如下:
# 创建一个项目目录并进入 mkdir pyside_demo cd pyside_demo # 创建虚拟环境,推荐用 venv python -m venv venv # 激活虚拟环境(Windows PowerShell) venv\Scripts\Activate.ps1 # 如果PowerShell执行策略限制,可以用cmd: # venv\Scripts\activate.bat # Linux / macOS # source venv/bin/activate激活成功后,终端前面会出现(venv)前缀。这之后所有pip安装都会进到这个隔离环境里,不会污染全局Python。为什么我强调这个?因为PySide6的依赖链比较长,包括shiboken6这类底层绑定库,一旦和全局环境里的其他包产生版本冲突,报错信息会非常难读懂——常见的是ImportError: DLL load failed,这种错误一出现,新手基本就劝退了。
2.2 安装与验证
激活环境后,安装就一行命令:
pip install PySide6如果网络环境一般,可以用国内镜像加速:
pip install PySide6 -i https://pypi.tuna.tsinghua.edu.cn/simple装完验证一下:
python -c "from PySide6.QtWidgets import QApplication; print('ok')"这里再强调一个小点:PySide6和PySide2、PyQt5、PyQt6不能装在同一个虚拟环境里。它们底层都依赖shiboken或sip这类绑定层,混装后运行时会出现TypeError: 'PySide6.QtCore.Signal' object is not connected之类完全莫名其妙的问题。如果你要同时维护多个Qt项目,请务必分开建虚拟环境,别图省事。
2.3 第一个窗口:理解QApplication和事件循环
装完环境,就可以写第一个程序了。很多人第一个例子是"Hello World",但我会先解释清楚那三行固定代码到底在干什么,否则后面遇到"窗口一开就闪退""代码卡住不动"时完全没头绪。
import sys from PySide6.QtWidgets import QApplication, QMainWindow, QLabel app = QApplication(sys.argv) window = QMainWindow() window.setWindowTitle("第一个PySide6窗口") label = QLabel("Hello, PySide6!") window.setCentralWidget(label) window.show() sys.exit(app.exec())拆开看,核心是QApplication和app.exec()。QApplication负责管理整个程序的事件系统,比如鼠标、键盘、窗口重绘、定时器这些都是"事件",事件会进入一个队列,由事件循环不断取出并分发给对应的控件去处理。app.exec()就是启动这个事件循环,它是一个死循环,直到你关闭最后一个窗口它才会退出并返回一个退出码。
那为什么窗口代码必须放在app.exec()之前?因为控件创建、信号连接、状态设置都要在事件循环启动前完成,否则界面还没准备好,事件循环就开始分发事件了,就可能出现"窗口没显示出来程序已经跑完退出"的现象。这也是最经典的"闪退"原因之一——没有exec(),窗口刚show出来程序就到末尾退出了。
另外有个细节:sys.exit(app.exec())的退出码会传给操作系统。如果程序是给别人用的批处理脚本,退出码很重要,调用方可以根据返回值判断程序是否正常结束。
3. 信号槽机制:掌握Qt组件之间的沟通协议
信号槽是Qt的灵魂,也是我从纯Python转到PySide后最需要转变思维的地方。普通Python程序是"你调我、我调你"的同步调用方式,而Qt程序更像是"发布订阅":一个对象状态变了,发出一个信号,任何关心这个信号的对象都可以连接过来处理。好处是组件之间彻底解耦,坏处是新手经常搞不清"这个函数到底什么时候被调用"。
3.1 从按钮点击开始理解信号槽
最常见的例子是按钮点击:
from PySide6.QtWidgets import QApplication, QPushButton def on_clicked(): print("按钮被点击了") button = QPushButton("点击我") button.clicked.connect(on_clicked)clicked是QPushButton的一个信号,connect(on_clicked)就是注册回调。用户点击按钮时,Qt内部生成一个鼠标事件,QPushButton处理完这个事件后发出clicked信号,事件循环再调用你注册的on_clicked函数。
注意clicked信号默认会带一个布尔参数,表示按钮的选中状态,所以如果你的槽函数只有一个无参定义,也是安全的——Qt的信号槽机制允许槽函数忽略多余参数,这一点比很多事件框架做得灵活。这个特性在后面配合lambda写小逻辑时特别方便。
3.2 connect的三种常见写法与闭包陷阱
连接信号的方式大致有三种,我推荐按场景混用:
# 方式一:直接连普通函数 button.clicked.connect(on_clicked) # 方式二:lambda表达式,适合简单逻辑 button.clicked.connect(lambda: self.do_something("参数")) # 方式三:装饰器方式,适合在类内部组织代码 class MyWindow(QMainWindow): def __init__(self): super().__init__() button = QPushButton("保存") button.clicked.connect(self.save_file) def save_file(self): passlambda虽然写着省事,但有个经典大坑:在循环里捕获循环变量。举个例子:
for i in range(5): button = QPushButton(f"按钮{i}") button.clicked.connect(lambda: print(i))你点击任意一个按钮,打印出来的可能都是4。原因是lambda里的i是闭包捕获的变量,循环结束后i等于4,lambda计算时才去取这个变量的当前值。正确做法是用默认参数绑定当前值:
button.clicked.connect(lambda checked, i=i: print(i))或者用functools.partial。这个坑在写批量创建的界面逻辑时几乎躲不掉,提前记住能省不少排查时间。
3.3 自定义信号:让组件自己说话
PySide6允许在QObject子类上定义自己的信号。这在拆分组件时特别有用。比如我写一个"登录表单"组件,点击登录后,组件内部去校验输入、请求接口,完成后把结果通知给主窗口。如果不用信号,你就得把回调函数传进组件里,组件和外部逻辑强耦合;用信号,组件只管发消息,外部想听就听。
from PySide6.QtCore import Signal, QObject class LoginWorker(QObject): login_succeeded = Signal(str) # 参数:用户名 login_failed = Signal(str) # 参数:错误信息 def do_login(self, username, password): if username == "admin" and password == "123456": self.login_succeeded.emit(username) else: self.login_failed.emit("账号或密码错误")自定义信号定义在类属性上,用Signal类型标识,可以带参数。调用emit()就是发出信号。主窗口那边这样连接:
worker = LoginWorker() worker.login_succeeded.connect(self.on_login_success) worker.login_failed.connect(self.on_login_fail)这种"组件内只发信号、不关心谁处理"的模式,对于一个几十个文件的桌面项目来说,代码结构会清晰很多。我后来做稍微大一点的工具,基本每个功能模块都是一个QObject,模块之间只用信号通信。
3.4 跨线程更新UI的黄金规则
PySide6里有个铁律:QObject不是线程安全的,绝不要在工作线程里直接操作UI控件。很多新手在QThread里写了self.label.setText("..."),然后要么报错,要么界面一会儿好一会儿坏。
正确的跨线程做法是:工作线程发信号,主线程用信号连接槽函数来更新UI。Qt的信号槽机制在这种情况下会自动采用队列连接方式,把槽函数的调用转发回主线程事件循环里执行。所以QThread里只emit信号,不碰任何控件,就绝对安全。
我在后面写实战项目时会再用一整章展开QThread的正确姿势,这里先把这个原则钉死。
4. 用Qt Designer排界面,用手写代码控逻辑
PySide6自带一个非常有价值的工具:Qt Designer。它是图形化的界面编辑器,可以拖拽控件、设置属性、做布局,最后保存成.ui文件。我见过两种极端的开发者:一种完全不用Designer,所有界面用代码一层层addWidget;另一种什么都拖控件,复杂到一定规模的界面难改命。我的建议是混合使用,而且推荐以Designer为主。
4.1 为什么我推荐Qt Designer而不是手写布局代码
手写布局代码的问题是"看不见摸不着",调整间距、对齐、伸缩比例完全靠想象。尤其当你需要做嵌套布局(水平里套垂直、垂直里套表单),手写代码很容易括号对错、层级搞混。Designer里拖控件是所见即所得,右键可以直接套布局,比如把三个按钮用"水平布局"包起来,再一键把整体放到"垂直布局"里。
Designer还提供了一个很好用的预览功能(快捷键Ctrl+R),可以实时看到窗口在不同系统风格下的效果。这个特性我在调样式表时经常用——改一下qss,预览一下,比一遍遍跑程序快多了。
4.2 .ui文件如何进入Python代码
.ui文件本质是XML,你不能让Python直接import它,需要转成Python模块。转换工具有两个思路:
方式一:用pyside6-uic命令行工具转换成.py文件
pyside6-uic mainwindow.ui -o ui_mainwindow.py然后在你代码里from ui_mainwindow import Ui_MainWindow。这是最经典的做法,生成的类不直接是一个窗口,而是一个"界面描述类",需要你在自己的QMainWindow子类里实例化它并调用setupUi。
方式二:运行时动态加载.ui文件
from PySide6.QtUiTools import QUiLoader loader = QUiLoader() window = loader.load("mainwindow.ui") window.show()这种方式不用提前转换,改完.ui文件立刻生效,适合快速原型开发。但缺点是QUiLoader在类名映射上有些限制,比如某些自定义控件处理起来麻烦。我个人的偏好是:项目里用方式一,生成的.py文件提交到版本库;临时验证用方式二。
推荐方式一的另一个原因是,当你需要给控件添加额外的功能逻辑时,"界面类+业务类"分离的结构更清晰。Ui_MainWindow只负责界面描述,你的窗口类负责业务,可以避免把一堆逻辑堆在setupUi里。
4.3 QSS样式表:让界面从"能看"到"能用"
PySide6支持用样式表(QSS)描述控件外观,语法几乎和CSS一模一样。说实话,一个内部工具默认的Windows98风格也能用,但稍微写点QSS,整个工具的专业感和可用性会显著提升。我自己的设计原则是:把一套QSS定义在独立文件里,运行时加载,改样式不用动代码。
一个基础例子:
QPushButton { background-color: #2d89ef; color: white; border: none; border-radius: 4px; padding: 6px 16px; } QPushButton:hover { background-color: #1e6fd9; } QPushButton:pressed { background-color: #1857a8; } QLineEdit { border: 1px solid #ccc; border-radius: 4px; padding: 4px 8px; selection-background-color: #2d89ef; }在代码里加载:
with open("style.qss", "r", encoding="utf-8") as f: app.setStyleSheet(f.read())这里有个容易踩的坑:QSS文件包含中文注释时,open一定要指定encoding="utf-8",否则Windows下默认GBK读取会报UnicodeDecodeError,而且报错位置往往让你摸不着头脑。
4.4 布局实战:一个简单的登录表单
用Designer拖这些控件:两个QLabel、两个QLineEdit、一个QPushButton、一个QCheckBox(记住密码)。在Designer里把它们垂直排列,然后选中所有控件,点击工具栏上的"垂直布局"。此时控件之间会均匀分布,窗口拉伸时它们会跟着调整。接下来选中"记住密码"复选框和"登录"按钮,再套一个"水平布局",这样登录按钮会紧跟复选框排在一行。
这里有个基于实践补充的小经验:给QLineEdit设置setPlaceholderText提示用户该输入什么,给密码输入框设置setEchoMode(QLineEdit.Password)遮蔽明文,这两个属性在Designer的右侧属性栏里就能直接配,不用写代码。真正要写代码的是读取输入后做校验的逻辑,这部分放在按钮的clicked信号槽里即可。
布局中另一个隐蔽的坑是"控件被压缩"。QLineEdit默认水平尺寸策略是Expanding,而QPushButton默认是Fixed,所以当你宽高不当时,按钮会保持原大小,输入框却会被拉伸得很怪异。遇到这种情况不要慌,用Designer里的"水平伸展"和"垂直伸展"属性调整伸缩因子(stretch factor),比如希望用户名输入框占2份、其他占1份,就在属性面板设置QLineEdit的sizePolicy或布局的stretch。这也解释了为什么我只用Designer排布局而不用纯代码:这些视觉因子调试起来,图形化比代码直观得多。
5. 打包发布:把PySide6程序变成可以交付的exe
写完了能跑的程序,接下来就是许多人真正被卡住的一关——打包。"PySide打包"这个热词我能理解为什么这么高频:开发环境里一切正常,pyinstaller一打包,双击要么闪退要么弹could not find the Qt platform plugin,99%的人在这里放弃了桌面开发。这一节我会把打包的完整路径走一遍。
5.1 为什么打包是桌面程序的最后一公里
Python脚本给别人用时,对方机器上大概率没有Python环境,也没有PySide6依赖。你需要把脚本解释器、依赖库、Qt运行库、平台插件、资源文件全部塞进一个可执行文件(或一个带目录结构的文件夹)。PyInstaller是这一领域最成熟的选择,它分析你的入口脚本,追踪所有import,然后把运行时需要的二进制和模块收集到一起。
不过PyInstaller的分析器对PySide6这种带C扩展、带插件体系、带动态加载的库,天然容易漏东西。它不是无法打包,而是需要你理解Qt运行时到底有哪些组成部分,才能正确配置打包参数。
5.2 pyinstaller基础打包
在项目虚拟环境里安装pyinstaller(一定要装进同一个环境,这样它才知道PySide6装在哪里):
pip install pyinstaller假设入口脚本是main.py,最基础的打包命令:
pyinstaller main.py --name MyTool --noconsole参数说明:
--name MyTool:指定生成的可执行文件名,不指定的默认是入口脚本名。--noconsole:不显示黑色控制台窗口,适合GUI程序。这个参数老版本里写作--windowed,两者等价。
执行完成后,dist目录下出现MyTool文件夹(或按配置生成单文件),里面是启动exe和一堆依赖dll。如果一切正常,双击MyTool.exe就能跑起来。
但绝大多数第一次打包不会那么顺利,接下来是重头戏。
5.3 最经典的"缺平台插件"坑
如果你直接双击exe,遇到黑窗口闪退,或稳定弹出一个错误框写着:
qt.qpa.plugin: Could not find the Qt platform plugin "windows" in ""原因基本可以确定:PyInstaller打包时没有把Qt的平台插件收集进来。Qt需要一个和操作系统对应的平台插件才能创建窗口,Windows下的插件文件名是qwindows.dll,存放在PySide6的plugins/platforms目录里。PyInstaller默认应该能识别到,但某些环境、某些旧版本下会漏。
解决办法不止一种,最省事的做法是使用PyInstaller的钩子参数:
pyinstaller main.py --name MyTool --noconsole --collect-all PySide6--collect-all PySide6的意思是:把所有PySide6相关的包数据、二进制、插件全部收集进dist目录。想象一下,这相当于打包时把整个PySide6的"抽屉"都搬过来,不会再缺东西,代价是体积会大一些。我的建议是:第一次打包先用这个参数保证能跑通,后续再考虑裁剪体积。
如果用了--collect-all还是报平台插件找不到,还可以手动在spec文件里指定binaries和datas,把PySide6/plugins整个目录加进去。spec文件是PyInstaller生成的配置文件,没生成的话用pyi-makespec main.py先生成。在spec里找到Analysis,把datas部分改成:
datas=[('路径/venv/Lib/site-packages/PySide6/plugins', 'PySide6/plugins')],5.4 资源文件、图标和qss怎么一起带走
Qt程序中经常会有图标、QSS、图片素材,打包时这些资源不会自动被收集。如果你在代码里用相对路径加载资源文件,打出来的exe在别的目录下双击,就会因为找不到相对路径而加载失败。
这里分享两种稳妥方案:
方案一:用--add-data把资源目录打包进dist目录:
pyinstaller main.py --name MyTool --noconsole \ --add-data "assets;assets" \ --add-data "style.qss;." \ --add-data "app.ico;."Windows下--add-data的源和目标用分号分隔,Linux和macOS用冒号。目标路径是相对于dist目录根的位置。打包后,assets目录、style.qss、app.ico都会平移到exe所在目录。代码加载资源时就写相对路径:
with open("style.qss", "r", encoding="utf-8") as f: app.setStyleSheet(f.read())方案二:用Qt的资源系统(qrc文件)把资源编译进二进制,这样根本不依赖外部文件。但配置相对复杂些,需要pyside6-rcc把.qrc转成.py,然后import resource_rc。适合素材数量少、希望彻底干净的场合。
我的踩坑建议是:如果只是内部使用,方案一就够了,但要注意代码里所有资源路径都做成相对于exe所在目录的形式。比如拿到exe所在目录,可以用:
import sys, os if hasattr(sys, '_MEIPASS'): base_path = sys._MEIPASS else: base_path = os.path.dirname(os.path.abspath(__file__))sys._MEIPASS是PyInstaller在运行打包程序时设置的临时解包目录,单文件模式下资源文件会被解压到这里。记住__file__在打包后不一定指向exe所在目录,所以要优先用sys._MEIPASS。
5.5 打包体积与启动速度的经验
一个最简单的PySide6程序用--collect-all打出来,体积随便就上80MB-100MB往上,启动也要好几秒。对于体积敏感的场景,可以做几件事:
- 去掉Qt中用不到的模块。比如语音、音频、WebEngine这些模块体积巨大。如果你的程序只用Widgets和网络,可以在spec文件里手动排除某些模块,或者在
Analysis的excludes里写:
excludes=['PySide6.QtWebEngineWidgets', 'PySide6.QtWebEngineCore', 'PySide6.QtMultimedia'],不生成单文件。
--onefile会生成一个巨大的exe,启动时需要先解压到临时目录再运行,启动更慢且容易被杀毒软件误拦截。我一般用默认的one-folder模式(就是生成一个文件夹),启动快、排错方便。考虑压缩资源。PySide6插件里包含调试符号,体积大,实际运行不需要;在spec文件里也可以调整
UPX压缩选项,但PySide6的某些Qt DLL被UPX压缩后可能运行异常,这点务必实测,别为了压缩牺牲稳定性。
6. 避坑合集:PySide6开发中我亲身踩过和常见的问题
到了这个系列的最后一部分,我把开发过程中反复遇到的问题集中写出来,都是默认文档里不会讲的细节。这些问题任何一个单独拎出来都能浪费你半天时间,一起记着可以少走很多弯路。
6.1 venv里正常,打包崩溃
这个坑我前面提到了一部分:venv里运行良好,打包后双击闪退。
有个隐蔽原因是Qt插件路径硬编码。PySide6在运行时通过内部机制定位插件目录,在venv里一切正常,但打包后插件路径变了。--collect-all PySide6能解决大多数情况。另一种情况是系统缺少Visual C++运行库,PySide6的DLL依赖MSVC运行时,目标机器如果没有对应的vcredist,程序启动就会报错。常见报错是The code execution cannot proceed because VCRUNTIME140.dll was not found。把VC_redist.x64.exe装上基本就好。
6.2 QTimer没有触发
QTimer是桌面开发里常用的组件,比如做一个自动存盘功能,每隔一段时间触发一次。新手常见写法是:
timer = QTimer() timer.timeout.connect(self.auto_save) timer.start(1000)结果发现auto_save从来没被调用过。原因是timer是局部变量,函数一结束,timer对象就被Python垃圾回收了。信号连接不阻止对象被回收,一旦timer没了,事件循环里根本没有这个定时器。正确做法是把timer保存为self.timer:
self.timer = QTimer(self) self.timer.timeout.connect(self.auto_save) self.timer.start(1000)同样的道理适用于任何QObject:创建时要给一个父对象(比如self),或者保持一个引用,否则Qt内部不会替你维护它的生命周期。
6.3 中文字符显示和编码问题
PySide6本身支持Unicode,但Windows下有几个典型场景会出问题:
一是QSS文件带中文注释,前面说过,打开文件要指定utf-8编码。
二是打包后控制台(如果你用了--console)输出中文,可能会乱码。实际上GUI程序不需要控制台,直接--noconsole最省心。
三是代码里硬编码的中文字符串在某些旧PySide6版本与Windows下控制台编码冲突。我建议所有外部文本文件统一用utf-8,代码文件头部加# -*- coding: utf-8 -*-(虽然Python 3默认utf-8,但部分编辑器仍需要这个声明)。
6.4 高DPI模糊问题
Windows下如果显示缩放为125%或150%,PySide6默认行为可能让界面变得模糊。这是因为Qt需要感知系统的DPI并进行缩放。PySide6.5之后默认启用高DPI缩放,但在某些老版本或混合DPI环境下,你需要在创建QApplication之前设置环境变量:
import os os.environ["QT_ENABLE_HIGHDPI_SCALING"] = "1" from PySide6.QtWidgets import QApplication注意这行必须放在import PySide6之前,否则设置不生效。如果依然模糊,可以尝试:
QApplication.setHighDpiScaleFactorRoundingPolicy(Qt.HighDpiScaleFactorRoundingPolicy.PassThrough)这个坑碰到的人不少,表现是"开发机清楚,客户机模糊",原因基本都是DPI缩放策略不同。提前加了这行,至少能保证大多数Windows显示设置下界面清晰。
6.5 QThread还是不要直接碰控件
这个在前面信号槽部分说过,但值得用具体案例再讲一次。我最早写过这样一段代码:
class DownloadThread(QThread): def run(self): self.label.setText("开始下载") # 直接操作UI,大忌 # 耗时下载运行起来偶尔会报QObject: Cannot create children for a parent that is in a different thread,UI还可能在下载过程中卡死甚至崩溃。正确做法是定义一个自定义信号,线程内emit下载状态,主线程槽函数再去更新label:
class DownloadThread(QThread): progress_changed = Signal(int) finished_signal = Signal() def run(self): for i in range(100): time.sleep(0.1) self.progress_changed.emit(i) self.finished_signal.emit() # 使用 thread = DownloadThread() thread.progress_changed.connect(lambda v: progressBar.setValue(v))线程内只动信号,主线程只动控件,两条线交叉点就是信号槽——这是PySide6多线程UI编程的黄金模式,比任何锁、任何标志位都简单可靠。
6.6 打包后杀毒软件误报
这个不算PySide的bug,但发生率不低。PyInstaller打出来的exe,尤其--onefile模式,经常被某些杀毒软件误报为木马,因为它要做"运行时解压并执行",行为上和恶意软件类似。我的实践经验是:优先用one-folder模式,误报率更低;如果客户公司有强制安全软件,建议给exe做代码签名(就算是自己内部用,也可以用自签证书),能有效降低误报率。
最后分享一点个人体会:PySide6这套体系,从上手到能交付,其实就三个大的门槛——环境、信号槽、打包。环境是最容易的,照着步骤来就好;信号槽是思维转换,理解了"事件循环+发布订阅"以后,写起来会非常顺手;打包是耐心活,它不是在考验你的Python能力,而是在考验你排查问题的能力。我自己第一次打包PySide应用时,光研究qwindows.dll就花了一整天,后来才明白与其瞎试,不如直接用--collect-all跑通流程,再慢慢做减法。
如果你正在踩这些坑,希望这篇step by step能帮你省下那亏掉的一天。照着这个顺序把demo跑出来,再换成你自己的业务逻辑,一个可以直接交付的桌面工具就没那么遥远了。