干了十年工业软件,我见过太多功能做得扎实、界面却停留在Windows 98时代的工控上位机。灰底白框、密密麻麻的表格、莫名奇妙的立体按钮,客户说“能用就行”,但每次交付完自己都不太想打开那个界面。直到我在一个固件刷写项目中彻底用PySide6重写了上位机界面,才真正意识到:工控界面不该是丑陋的代名词,上位机UI完全可以做到既专业又现代。
这篇文章就围绕“用PyQt/PySide6打造现代化工业上位机UI”展开,聊一聊为什么大多数工控界面会“辣眼睛”,以及从窗口框架、导航布局、QSS样式到高DPI适配、性能优化的一整套落地手法。适合正在做或准备做上位机开发的工程师,也适合想在Qt技术栈里把界面做得更像样的朋友。我会给出可以直接抄作业的代码片段和设计思路,也会把我踩过的坑一并说出来。
1. 为什么工业上位机普遍“丑”,以及到底要改成什么样
1.1 工控界面通病的源头
先说个现象:你去现场看设备,绝大多数触摸屏和上位机的界面都是深灰或浅灰底、蓝色渐变按钮、硬邦邦的分割线。这种风格不是没人吐槽,而是大家默认“工控软件就长这样”,加上很多项目周期紧、工期短,领导只看功能和稳定性,界面就被扔到了最后。久而久之,“丑”成了工控软件的刻板印象。
更深一层的原因是技术栈和人力资源错配。工控领域大量上位机是用C# WinForms、VB、LabVIEW甚至古老的MFC写的,这些框架默认控件样式基本停留在系统主题上,要自定义外观得额外花不少功夫。而真正会写现代UI的人,大多去了互联网行业,很少有人愿意扎进串口、CAN、Modbus这些底层协议里。所以工控界缺的不是审美,是“既能搞通讯协议、又能把界面做漂亮”的复合型开发者。
另一个容易被忽略的痛点是:工业上位机的界面往往是“信息密度极高”的。一个设备监控界面,可能要同时显示几十路温度、压力、速度,还要加曲线、报警、参数表。信息一多,界面就容易变成“数据堆砌”。很多工程师把每个数据直接摆上去,没有任何信息层级和分组,用户的视线完全不知道该落哪里,这也是“丑”的重要来源。
1.2 现代化不等于花哨:先定义目标界面
在动手改外观之前,我建议你先想清楚“现代化”到底要达成什么。真正的现代工业上位机UI,不是把界面做得像游戏官网一样花哨,而是做到三层:第一层是视觉上干净、统一、有明确的层级;第二层是交互上反馈及时、操作路径短;第三层是信息上重点突出、符合使用逻辑。
我个人的设计目标可以归结为几个关键词:扁平化、卡片化、留白、栅格对齐、克制的配色。扁平化不是不做阴影,而是不用厚重渐变和立体感;卡片化是把相关联的信息装进一个带圆角的容器里,让用户在视觉上一眼看出“这组数据是一伙的”;留白是给界面“呼吸感”,不要把控件挤得严丝合缝。配色上,工业软件最稳妥的选择是深灰或浅灰中性色打底,配一个品牌色做强调,再配一个状态色(比如绿色正常、黄色警告、红色故障)做反馈。
定好这些设计原则以后,技术上才有清晰的执行方向。我用的是 Qt 的 QWidget + QSS 方案,而不是 Qt Quick/QML。理由很简单:上位机绝大多数场景是表格、表单、参数配置、数据曲线这类“重信息”界面,QWidget 的控件体系更成熟,QSS 的写法和 CSS 接近,团队上手快,调试也直观。QML 虽然动画能力更强,但对传统上位机场景来说学习成本和工程复杂度都不成正比。
2. 工具选型:PySide6 还是 PyQt6
2.1 授权模式决定项目边界
很多新手会在这里纠结:PyQt6 和 PySide6 到底选哪个?我直接说我的习惯:新项目一律 PySide6,老项目如果已经是 PyQt 体系的,就继续用 PyQt,不要为了“潮流”去中途换阵。这个选择背后最核心的变量是许可证。
PyQt6 是 GPL 和商业双许可,PySide6 是 LGPL。这意味着如果你写的是内部自用工具,两者都没问题;但如果你要给客户交付闭源商业软件,PySide6 的 LGPL 让你在动态链接方式下可以保持代码闭源,而 PyQt6 的 GPL 条款会把你的代码也拖进开源义务里。曾经有项目组用 PyQt 做了一套设备管理系统,交付时客户要求源代码托管审查,差点因为协议问题出乱子,后来改成了 PySide6 才松一口气。
另外 PySide6 是 Qt 官方维护的 Python 绑定,文档和示例都由 Qt 项目组直接维护,API 和 C++ Qt 的对应关系更“正宗”。在工控圈子很多底层设备库本身就依赖 Qt,比如一些相机 SDK 和运动控制卡的驱动示例,用 PySide6 对接时版本兼容性往往更顺滑。
2.2 API差异与迁移成本
从 PyQt5 切到 PySide6,很多人最担心的是代码能不能直接搬。实际上两者的 API 在大多数常规用法上几乎一致,差别主要在细节:比如信号槽的写法,PySide6 用Signal()、Slot(),PyQt6 里是pyqtSignal()、pyqtSlot();枚举值的使用也不同,PyQt6 建议Qt.AlignmentFlag.AlignCenter,PySide6 也支持同一种风格,但老代码里Qt.AlignCenter的写法在 PySide6 里会报错。
还有个细节是exec()和exec_()的区别,新版统一用exec()就好。遇到要从 PyQt 迁移的项目,建议先做一次全量的枚举扫描,这是最容易踩坑的地方。整体迁移成本不算高,工具脚本几个小时能处理完,但前提是你的代码里没有大量用私有接口。
版本匹配上,我目前使用的是 Python 3.10 + PySide6 6.5+ 组合。安装就是一条命令:
python -m pip install pyside6装完之后记得验证一下,很多环境问题都是因为装到了别的 Python 解释器里:
python -c "from PySide6.QtWidgets import QApplication; print('PySide6 OK')"3. 上框架:无边框窗口、左侧导航与卡片布局
3.1 无边框窗口与自定义标题栏
现代桌面软件的“现代感”通常从无边框窗口开始。Windows 自带的标题栏又宽又老气,想换颜色换不了,想把标题栏和工具栏融合成一个整体更不可能。解决方案是自己画一个标题栏:先去掉系统边框,再在窗口顶部放一个自定义的 QWidget 作为标题栏,里面放窗口标题、菜单按钮、最小化和关闭按钮。
去掉系统边框的代码很简单:
from PySide6.QtCore import Qt from PySide6.QtWidgets import QMainWindow class MainWindow(QMainWindow): def __init__(self): super().__init__() self.setWindowFlags( Qt.WindowType.FramelessWindowHint | Qt.WindowType.WindowMinMaxButtonsHint )去掉边框后会立刻遇到一个问题:窗口不能拖动了。所以要在自定义标题栏上补回鼠标拖拽逻辑。思路是重写mousePressEvent和mouseMoveEvent,记录按下时的全局位置,移动时计算偏移增量,再调用self.move()。
def mousePressEvent(self, event): if event.button() == Qt.MouseButton.LeftButton: self._drag_pos = event.globalPosition().toPoint() - self.frameGeometry().topLeft() event.accept() def mouseMoveEvent(self, event): if event.buttons() == Qt.MouseButton.LeftButton and hasattr(self, "_drag_pos"): self.move(event.globalPosition().toPoint() - self._drag_pos) event.accept()这里有个细节值得注意:globalPosition()在 PySide6 的鼠标事件里返回的是浮点坐标,需要先.toPoint()转成整数点,否则坐标累加之后会出现窗口慢慢漂移的毛病。我在第一次实现时就是因为直接拿浮点数用,拖了几次窗口就歪掉了。同时,自定义标题栏里的按钮样式要单独写 QSS,悬停变红、按下加深,交互反馈要敏锐。
3.2 左侧导航栏的现代化做法
工业软件一般模块多:设备监控、参数配置、数据记录、报警列表、系统设置。这么多模块往一个界面里塞,最清晰的组织方式就是左侧导航栏。我做的是固定宽度的大图标加文字菜单,菜单项用 QListWidget 承载,选中项用一段圆角高亮块表示。
导航栏的视觉设计直接影响整个界面的气质。推荐做法是给导航栏一个独立的深色背景,比如#1E293B或#2D3748,菜单项文字默认用浅灰,选中时用品牌色半透明底加白色文字。这样视觉上导航区与内容区形成明确对比,用户的视线自然会先落在导航,再导航到具体内容。
菜单切换用一步到位的方式实现:QListWidget.currentRowChanged信号触发QStackedWidget.setCurrentIndex,从而切换右侧的内容页面。为了看起来不生硬,可以在 QStackedWidget 的切换动画上用简单的透明度渐变,但别做得太重。工业软件不是演示 Demo,切换页面花半秒钟都是浪费操作员时间。
导航栏里还要考虑用户的使用习惯:现场操作工不可能像程序员一样理解树形结构,菜单名称要直白,最好不超过八个字;分组逻辑要按“任务”分而不是按“技术”分。比如“电机设置”比“寄存器配置”友好得多。这些看似和工作界面无关的细节,恰恰是衡量一个上位机“好不好用”的隐形标准。
3.3 内容区的卡片与留白体系
内容区是所有信息控件的落脚点,怎么组织这里的内容,导航做得再漂亮也没用。我采取的是“栅格 + 卡片”模式:内容区用 QGridLayout 或 QHBoxLayout 分成若干列,每个功能模块装进一个 QFrame 容器,设置统一的背景色、圆角、内边距,相当于把网页里的“卡片”概念搬运到桌面程序里。
一个典型的设备监控页面可以这样划分:顶部一行放关键运行状态(总电源、运行时长、当前产量等),用几个横向卡片展示大数字;中间一行放实时曲线和参数表,用两个纵向卡片各占一半;底部一行放最近报警记录,用整宽卡片加表格。这样从上到下的信息流是“总览 → 趋势 → 明细”,符合人的阅读习惯。
卡片容器的 QSS 写法可以直接复用一套模板:
QFrame#card { background-color: #FFFFFF; border-radius: 8px; border: 1px solid #E2E8F0; }要注意的是,卡片大量出现后,页面里就不建议再加更多的“框”了,否则会重新陷入线框密集的视觉噪音。表格、曲线、输入框这些子控件在卡片内部尽量去掉边框,留白反而更干净。我用下来最舒服的效率是:内容区背景浅灰#F5F7FA,卡片纯白,两者对比度刚好,眼睛长时间盯屏幕不容易疲劳。
4. QSS样式系统:现代化UI的灵魂
4.1 扁平化控件的核心QSS写法
QSS 是 Qt 的样式表语言,语法和 CSS 非常接近,是让 QWidget 界面脱胎换骨的关键武器。我见过不少工程师的 QSS 只有setStyleSheet("background-color: gray")这种级别,完全没发挥出它的能力。实际上 QSS 可以精确到控件类型、对象名、状态伪类,写好了能实现非常细腻的视觉效果。
以“主按钮”为例。现代化主按钮的核心是:背景色填充、无立体感、圆角适中、文字加粗、有清晰的悬停和按压反馈。对应 QSS 如下:
QPushButton#primaryButton { background-color: #2B6CB0; color: #FFFFFF; border: none; border-radius: 6px; padding: 8px 24px; font-size: 14px; font-weight: 600; } QPushButton#primaryButton:hover { background-color: #3182CE; } QPushButton#primaryButton:pressed { background-color: #2C5282; }这里有几个我反复试过的要点。一是border: none必须写,否则 Qt 默认会给按钮画一个立体的边框,瞬间回到老式风格。二是padding不能省,按钮没有内边距时文字会紧贴边缘,显得局促。三是圆角的弧度,工业界推荐 4~8px 之间,太小没有精致感,太大会显得不稳重。
对于输入框,现代风格的关键是“安静”。默认状态用浅灰描边,获得焦点时边框换成品牌色、并带一个淡淡的发光效果:
QLineEdit { background-color: #FFFFFF; border: 1px solid #CBD5E1; border-radius: 4px; padding: 6px 10px; selection-background-color: #BEE3F8; } QLineEdit:focus { border: 1px solid #2B6CB0; }注意:focus状态在 QLineEdit 里是受支持良好的,但有些自定义控件需要自己处理焦点事件才能生效。另外,QSS 里写阴影是很麻烦的,需要借助QGraphicsDropShadowEffect,但那个效果在大量控件上使用会拖累性能,我一般只给主窗口和弹窗用,卡片级别全放弃阴影效果。
4.2 状态反馈:悬停、按下、禁用一个都不能少
现代 UI 和旧式 UI 最大的区别就在状态反馈的完整性上。旧式控件按下去最多凹进去一点,而现代控件要求每个可交互元素都要能明确告诉用户“你可以点我”“你正在点我”“你现在不能点我”。这三种状态如果缺失,操作员在关键时候会犹豫,严重时会导致误操作。
按钮的hover、pressed、disabled三个状态要分别设计。默认浅色背景的次要按钮,悬停时背景加深一点;按下去时再深一档,并且可以加一个轻微的“内缩”效果,模拟真实按压的行程感。禁用状态不能只是变灰,还要把文字颜色也调成低对比度,并且去掉任何悬浮反馈:
QPushButton#secondaryButton { background-color: #EDF2F7; color: #2D3748; border: 1px solid #CBD5E1; border-radius: 6px; padding: 8px 24px; } QPushButton#secondaryButton:hover { background-color: #E2E8F0; } QPushButton#secondaryButton:pressed { background-color: #CBD5E1; } QPushButton#secondaryButton:disabled { background-color: #F7FAFC; color: #A0AEC0; border-color: #E2E8F0; }状态反馈最容易被忽略的地方是表格和列表。表格行没有悬停高亮、没有选中态,用户看数据时很难聚焦。我把QTableView的样式统一写成这样:选中的整行用品牌色的浅色底,悬停用灰色底,文字始终深色保证阅读性。但是要注意,表格选中态的 QSS 要依赖QStyledItemDelegate或开启列的select属性,如果设置了列表的某些属性,选中色会显示异常,这个坑后面会细说。
4.3 用调色板变量统一管理视觉规范
一个界面做得好不好,很大程度上取决于配色是不是统一。最忌讳的是这个按钮一个蓝色、那个标签一个绿色、曲线又一个橙色,整体杂乱无章。我建议在项目里建立一个theme.py,把所有颜色定义成常量,做成一套“品牌色 + 中性色 + 功能色”的调色板:
# theme.py COLOR_BG = "#F5F7FA" COLOR_CARD = "#FFFFFF" COLOR_PRIMARY = "#2B6CB0" COLOR_PRIMARY_HOVER = "#3182CE" COLOR_PRIMARY_PRESSED = "#2C5282" COLOR_TEXT = "#1A202C" COLOR_TEXT_SECONDARY = "#718096" COLOR_BORDER = "#E2E8F0" COLOR_SUCCESS = "#48BB78" COLOR_WARNING = "#ED8936" COLOR_DANGER = "#F56565"然后写一个函数集中的apply_theme(app),所有 QSS 里引用这些常量。这样一来,当客户想换一套主题色时,只需要改几个值就行,不用满项目去搜颜色代码。我实际遇到过甲方要求把品牌色从蓝色改成绿色的需求,靠这个方案十分钟就全量切换了,连曲线和报警图标都一起变了,客户还以为我专门做了一版新皮肤。
还有一点很实用:把 QSS 文件独立出来放到resources/theme.qss,用QFile读取后app.setStyleSheet()加载,这样 UI 升级时不用重新编译 Python 代码,甚至可以做成运行时热加载,调试效率会明显提升。
5. 高分屏适配、性能优化与异步刷新
5.1 高DPI缩放配置
工业现场现在大量使用 2K、4K 分辨率的触摸屏和显示器,如果上位机不做高分屏适配,界面要么糊成一片,要么控件小得没法点。Qt 6 在高DPI支持上比 Qt 5 好很多,在 Qt6 里默认开启缩放,但你还是需要在入口处确认几个关键属性,尤其是使用 PySide6 时,建议在创建QApplication之前设置:
import os os.environ["QT_ENABLE_HIGHDPI_SCALING"] = "1" os.environ["QT_AUTO_SCREEN_SCALE_FACTOR"] = "1" from PySide6.QtWidgets import QApplication from PySide6.QtCore import Qt QApplication.setAttribute(Qt.ApplicationAttribute.AA_EnableHighDpiScaling, True) QApplication.setAttribute(Qt.ApplicationAttribute.AA_UseHighDpiPixmaps, True)不过用好缩放只是第一步,你的布局必须用布局管理器而不是绝对坐标。很多老式上位机喜欢用setGeometry()硬编码位置,一到高分屏就乱套。现代 Qt 开发的底线就是:所有控件都放进QHBoxLayout、QVBoxLayout、QGridLayout里,让布局系统自动适应。间距用统一的spacing常量,控件最小尺寸用setMinimumSize约束,这样才能在 1080P 和 4K 屏上保持一致的比例感。
还有个小坑:高分屏下字体渲染。Qt 默认字体在 Windows 上可能是“宋体/Microsoft YaHei”,到了高分屏上发虚。我习惯在入口处统一设置应用字体为Microsoft YaHei UI或HarmonyOS Sans SC,并把基础字号设为 14px。
app.setFont(QFont("Microsoft YaHei UI", 10)) # 10pt ≈ 13-14px字体名称在不同系统上存在差异,跨平台部署时建议做成配置项,让运维能临时改。
5.2 界面卡顿的排查思路
聊到“界面卡顿”,这是上位机高频问题。热搜里也总有人问“ui界面卡顿怎么办”。我告诉你,卡顿的来源不是 Qt 本身不够快,而是你用错了方式。最常见的卡顿场景是:主线程里做耗时操作,比如串口读一大包数据后直接在主线程解析、排序、刷新表格,或者初始化时同步加载设备固件信息。
解决卡顿的第一武器是“绝对不要在 UI 线程做阻塞操作”。所有耗时逻辑必须放到工作线程里,做完再通过信号把结果丢回主线程。Qt 官方方案是QThread搭配moveToThread,也可以直接用QRunnable加线程池。我用的比较多的模式是:数据采集线程一直在跑,解析完一批数据后发一个data_ready信号,主线程的槽函数里只做“把数据塞给表格/曲线”的轻量操作。
第二个卡顿原因是“过度重绘”。QSS 里大量使用带透明度的背景、阴影、圆角会显著增加绘制开销。我遇到过一个页面,几百个指示灯控件每个都用了border-radius和box-shadow风格,滚动时帧率直接掉到个位数。排查后把指示灯改成QLabel加纯色背景,圆角效果用一张小的 PNG 资源,滚动立刻流畅了。绘制开销这个事情,可以用 Qt 自带的QPainter开销分析工具来看,但最简单的方法还是控制透明层数量。
第三个原因是“主线程里频繁 sleep”。有些新人会在轮询里写time.sleep(0.1)控制频率,这会让事件循环被阻塞,界面必然卡。正确的做法是用QTimer或QThread内部的延时循环,始终保证事件处理能被执行。
5.3 串口与网络数据的异步更新模式
上位机最重要的功能是和下位机通讯,常见的是串口、TCP/UDP 和 CAN。这里最关键的模式是“数据采集线程 + 信号槽刷新界面”。拿串口示例来说,用QSerialPort的readyRead信号配合QThread处理,不要在槽函数里做耗时解析。更简洁的方式是单独开一个采集线程,循环readAll(),把原始字节流按协议拆帧后,把解析好的结构化数据放进队列,再一次性发射信号。
如果你的画面里有实时曲线,我强烈建议用pyqtgraph而不是 QtCharts。pyqtgraph 是专门为高性能数据可视化设计的,绘制几千个点的实时刷新毫无压力,而 QtCharts 在数据量大时重绘开销会明显上升。代码上只需要把数据追加进PlotDataItem,设置setClipToView和autoDownsample,就能在 50Hz 波形刷新下保持流畅。
还有一股容易忽视的“隐晦卡顿”来自日志控件。上位机往往会加一个运行日志输出框,直接append几万行后,QPlainTextEdit 的性能会急剧下降。解决方法是限制最大块数:每 append 一条就检查是否超过上限,超过就删除前面的行。我一般把日志上限设为 2000 行,日志调优不但在性能上有收益,操作员看日志也更轻松。
6. 实操中踩过的坑:环境、样式与打包
6.1 环境安装与版本匹配问题
PySide6 的环境问题通常集中在版本不匹配。最常见的报错就是很多人在 Python 3.11 下装了 PySide6 6.2 的老版本,一跑就提示缺少某些 Qt6 运行时组件。建议直接装最新稳定版,并且用虚拟环境隔离项目依赖:
python -m venv venv venv\Scripts\activate python -m pip install --upgrade pip python -m pip install pyside6开发机上装完 PySide6 后,还会带一堆附加工具:pyside6-designer、pyside6-rcc、pyside6-uic。如果你习惯用 Designer 拖界面,生成的.ui文件可以通过pyside6-uic转成 Python 代码,但我不推荐把所有界面都拖在 Designer 里做,因为复杂布局在 Designer 里管理困难,反而代码方式更好调。最实用的方式是:静态布局用代码,复杂自定义控件用 Designer 画原型,最终代码以手工方式整合。
如果你在运行时看到“could not load Qt platform plugin”之类的错,十有八九是你的可执行文件找不到 Qt 的插件目录。这个放在打包部分一起处理。
6.2 样式不生效的排查技巧
QSS 看似简单,踩坑的姿势却不少。我遇到过最常见的三个问题:一是为某个控件单独setStyleSheet,它把父级样式给覆盖了;二是伪类状态不生效,比如QPushButton:focus写在了对象名的后面导致选择器失效;三是属性定义在 QSS 里,但控件的setAttribute或 flag 不允许对应状态触发。
第一个问题的典型场景:你在顶层设置了全局 QSS,QPushButton { ... },然后某个页面给按钮单独设置了内边距,结果那个按钮的样式整体都变了。原因是setStyleSheet作用于该控件及其子控件,它不会继承全局样式,而是完全覆盖。正确的做法是:全局样式只写在 app 级别,个别控件尽量用“对象名选择器”去覆盖,不在代码里单独调setStyleSheet方法。
第二个问题需要留意选择器语法。官方支持的优先级是:#objectName比.class高,伪类必须放在最后面。比如:
QPushButton#primaryButton:hover { ... } /* 正确 */ #primaryButton QPushButton:hover { ... } /* 错误:选择器之间多了空格 */第三个问题的经典例子是表格选中色不生效。QTableView里要显示选中行变色,除了 QSS 之外,还需要在代码中设置:
table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows) table.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection)否则虽然 QSS 写了QTableView::item:selected { background: ... },但选中行为是单元格不是整行,视觉上就会显得“一半变蓝一半不变”,很突兀。排样式的时候,用 Qt 自带的QStyleSheetStyle调试器逐层看样式来源,比瞎猜快很多。
6.3 打包部署的几个注意点
上位机最终要交付给客户,打包是绕不开的一环。PySide6 应用推荐用PyInstaller或Nuitka打包。我的习惯是先用 PyInstaller 做快速验证,如果客户对性能或体积敏感再切换到 Nuitka 编译模式。打包之前有几个固定动作:
第一,把图标和资源文件转成 Qt 资源模块,用pyside6-rcc把.qss、图标图片、字体文件打包进resources.py。这样做的好处是不需要额外分发资源目录,不容易出现客户机器上图片找不到的尴尬。
第二,PyInstaller 打包时加入--collect-data PySide6之类参数,否则 Qt 插件和翻译文件容易缺失。实际上最简单的做法是用 PyInstaller 的 hook:
pyinstaller --windowed --onefile --name DeviceTool main.py如果最后客户机器上打开弹出“缺少 Qt platform plugin”或者“could not load library”,可以先检查dist目录下有没有PySide6的插件目录,没有就手动把开发环境里site-packages/PySide6/plugins/platforms复制过去。这个问题我遇到不止一次,每次都是因为打包 source 里漏了插件目录。
第三,--onefile模式会让程序启动时解压到临时目录,在工控机那种性能普通的机器上可能要等两到三秒才能出界面。如果客户对启动速度有要求,建议用--onedir模式,或者干脆把 Python 嵌入部署,启动体验会好不少。上位机是天天开的,启动快一点,维护工程师的体验差很多。
关于“现代化UI”这件事,我的最终体会
我最近在做一个基于 PySide6 的 ECU 刷写上位机时,第一次把“现代化 UI”这个目标认真落实到项目的每个页面。做完后自己打开那台设备,终于不再觉得难堪。客户现场验收时,甲方主管盯着界面夸了一句“这个界面看着舒服,像个正经软件公司做的”,那一瞬间我才明白,这不是面子功夫,而是对用户体验负责的一部分。
特别想提醒正在做上位机的朋友:不要因为“工控就是丑”这个惯性思维就放弃对 UI 的追求。在 PyQt/PySide6 的生态里,一套熟练的 QSS 技能加上合理的布局方式,完全能把界面做得不输给任何互联网桌面产品。对工程师来说最大的挑战不是技术难度,而是在赶工期的压力下,还愿意把界面的每个像素都当作品来打磨。先把框架搭好,把主题色定义好,把几个核心控件样式做透,后面所有页面都是往模板里填充内容,边际成本会越来越低。
最后给一句实话:UI 现代化的功夫一半在技术上,另一半在你自己愿不愿意“嫌弃”那些丑东西。嫌弃了,就会想办法改。改好了,你交付的每一个项目都能成为下一个项目的活广告。