ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

PyQt5实战:用qfluentwidgets打造现代化桌面应用界面

PyQt5实战:用qfluentwidgets打造现代化桌面应用界面 前阵子把公司一个内部数据管理工具从老式 QWidget 界面翻新了一遍需求本身不复杂但客户补了一句“能不能不要那种灰扑扑的默认窗口”我一开始还想着用 QSS 慢慢磨磨了两天发现按钮、菜单、滚动条、窗口阴影这些东西靠手写样式实在太痛苦后来直接换成了 PyQt5 qfluentwidgets 的组合窗口部分用 FramelessWindow 自己接管。改完之后整个界面的观感确实像换了个产品而且改造面比我预期的要小。这篇文章就把这套组合怎么落地、中间踩了哪些坑一次性说清楚给想在 PyQt5 里做出“现代感”界面的朋友一个可以直接抄的参考。1. 组合思路为什么是 PyQt5 而不是 PySide61.1 PyQt5 和 PySide6到底选谁先聊一个绕不开的问题既然都用 Python Qt为什么不直接上 PySide6从 API 层面看PyQt5 和 PySide6 大多数时候是能互相看懂的但依然有细节差异比如枚举的调用方式、slot 的写法、部分信号参数的返回类型。对于新项目我确实建议直接看 PySide6毕竟 Qt 官方主推License 也更友好。但现实是很多团队手上已经躺着几千上万行 PyQt5 代码业务逻辑和 Qt 深度绑定这时候硬迁移到 PySide6 不是不行而是一旦遇到某个模块在两个绑定里的 API 不完全一致排查成本瞬间就上来了。对我来说选 PyQt5 还有一个现实原因qfluentwidgets 这个库对 PyQt5 的兼容性一直保持得不错我在项目里用到的导航栏、消息条、按钮、卡片这些组件都能直接跑不需要额外适配。PySide6 也可以运行但如果你手上已经有一套 PyQt5 工程没必要为了“新”而迁移。工具的“新”永远不如“能让你按时交付”重要。1.2 qfluentwidgets 补的是“颜值”这块短板PyQt5 自带控件的问题大家都知道功能没问题但视觉上特别容易显得“年代感”。按钮是灰的、滚动条是粗的、菜单是硬的虽然可以通过 QSS 调整但调得越多越容易失控。特别是做企业内部工具大家默认“能用就行”可需求方嘴上说能用就行看到界面之后又忍不住皱眉。qfluentwidgets 的价值就在这里。它把微软 Fluent Design 里那套视觉语言搬到了 Qt 控件上包括亚克力质感、圆角卡片、流畅动画、图标系统、Windows 11 风格的自定义标题栏等等。你不需要自己从零绘制控件拿过来直接用就行。更关键的是它不需要你重写整个界面可以渐进式替换先把窗口换成 FramelessWindow再把几个高频按钮换成 Fluent 风格组件其余部分慢慢改。1.3 FramelessWindow把标题栏握在自己手里PyQt5 默认的窗口标题栏是系统自带的写着“最大化”“最小化”“关闭”三个按钮风格完全由操作系统决定你很难塞进自定义操作。比如你想在标题栏右侧加一个“设置”按钮或者整个窗口做圆角阴影效果原生标题栏根本不给机会。FramelessWindow 是 qfluentwidgets 里的一个窗口基类它把系统标题栏去掉了然后自己绘制一个可拖动的标题栏同时保留阴影、边缘拉伸、系统按钮等功能。你可以在这个标题栏里随意加控件也可以让整个窗口用圆角、亚克力等效果。对用户来说窗口依然是“正常窗口”能拖动、能缩放、有阴影但你拿到了完整的 UI 控制权。2. 环境准备和最小骨架5 分钟跑出一个 Fluent 窗口2.1 安装与常见环境问题PyQt5 的安装本身不复杂但它体积大直接 pip 安装可能比较慢。我自己习惯用 uv 来管理速度会快很多uv venv .venv source .venv/bin/activate # Windows 下是 .venv\Scripts\activate uv pip install PyQt5 qfluentwidgets如果你还在用 pip也可以用国内镜像源减少等待pip install -i https://pypi.tuna.tsinghua.edu.cn/simple PyQt5 qfluentwidgets这里有个容易踩的坑qfluentwidgets 默认不会帮你自动装 Qt 绑定库所以至少要先把 PyQt5、PySide6 或 PyQt6 其中之一装好。装完之后建议顺手验证一下 PyQt5 能不能正常导入再继续往下写。另外一个和安装有关的诡异问题是锁文件或 requirements 里可能出现类似pyqt5-qt55.15.19 registryhttps://pypi.tuna.ts...这样的行。这通常不是代码问题而是你用不同的 pip 源生成过 requirementspip 把镜像源的地址记录进去了。这种脏环境很容易导致后续安装冲突最省事的解决方式是删掉当前虚拟环境统一用一个源重新安装并且把版本固定住不要再混着源锁依赖。2.2 最小可运行代码直接贴一个最小骨架。这个代码会把一个无边框窗口跑起来里面有 Fluent 风格的标题和按钮import sys from PyQt5.QtCore import Qt from PyQt5.QtWidgets import QApplication, QVBoxLayout from qfluentwidgets import ( FramelessWindow, PrimaryPushButton, TitleLabel, setTheme, Theme, ) class MainWindow(FramelessWindow): def __init__(self): super().__init__() self.setWindowTitle(Fluent Demo) self.resize(800, 520) layout QVBoxLayout(self) layout.setContentsMargins(24, 40, 24, 24) title TitleLabel(Hello qfluentwidgets, self) button PrimaryPushButton(开始使用, self) layout.addWidget(title) layout.addWidget(button) layout.addStretch() # 确保自定义标题栏始终浮在内容上方 self.titleBar.raise_() if __name__ __main__: # 高分屏适配必须在 QApplication 创建之前设置 QApplication.setAttribute(Qt.AA_EnableHighDpiScaling, True) QApplication.setAttribute(Qt.AA_UseHighDpiPixmaps, True) app QApplication(sys.argv) app.setStyle(Fusion) setTheme(Theme.LIGHT) window MainWindow() window.show() sys.exit(app.exec_())运行这个脚本你会得到一个自带阴影、标题栏可拖动、右上角有最小化最大化关闭按钮的窗口。标题和按钮已经是 qfluentwidgets 处理过的样式和原生 PyQt5 控件完全两种气质。2.3 这段代码里最值得注意的三个细节第一个是layout.setContentsMargins(24, 40, 24, 24)。FramelessWindow 的自定义标题栏是浮在窗口内容区的如果不给上下左右留出边距内容会直接顶到标题栏底下视觉上很挤。顶部边距我一般给 40 左右刚好能让标题栏独立出来。第二个是self.titleBar.raise_()。因为在 FramelessWindow 里标题栏是作为子控件叠加在窗口上的如果你往窗口里塞了复杂的布局标题栏可能被内容盖住。show 之前手动 raise 一下能让标题栏始终在最上层避免系统按钮不可点的诡异问题。第三个是app.setStyle(Fusion)。qfluentwidgets 的控件绘制逻辑比较依赖统一的样式基底Fusion 是最稳定的选择。不用 Fusion 也能跑但不同 Windows 版本下默认样式可能有细微差异用 Fusion 能减少很多“为什么他的机器和我的机器显示不一样”的问题。3. 进阶导航框架、页面容器和主题切换3.1 左侧导航 右侧页面切换的常用结构真实项目里不可能只有一个页面。我习惯做一个左侧导航栏、右侧堆叠页面的布局这也是后台管理工具最常见的结构。qfluentwidgets 提供了 NavigationInterface 这个导航组件配合 QStackedWidget可以很快拼出主界面框架。下面这段是核心结构from PyQt5.QtWidgets import QHBoxLayout, QStackedWidget, QWidget from qfluentwidgets import NavigationInterface, FluentIcon class MainWindow(FramelessWindow): def __init__(self): super().__init__() self.setWindowTitle(Fluent Navigation) self.resize(1080, 680) self.navigationInterface NavigationInterface(self) self.stackedWidget QStackedWidget(self) self.pageHome QWidget(self) self.pageSetting QWidget(self) self.stackedWidget.addWidget(self.pageHome) self.stackedWidget.addWidget(self.pageSetting) self.navigationInterface.addSubInterface( self.pageHome, FluentIcon.HOME, 首页 ) self.navigationInterface.addSubInterface( self.pageSetting, FluentIcon.SETTING, 设置 ) self.hBoxLayout QHBoxLayout(self) self.hBoxLayout.addWidget(self.navigationInterface) self.hBoxLayout.addWidget(self.stackedWidget, 1) self.hBoxLayout.setContentsMargins(0, 40, 0, 0) self.titleBar.raise_()这里要注意不同版本的 qfluentwidgets 对导航接口的参数名可能有一点调整如果运行时报 TypeError去官方示例里抄一份当前版本的用法就行。整体结构是稳定的左边 NavigationInterface 负责导航右边 QStackedWidget 负责页面切换。3.2 页面里放 Fluent 控件能带来多少提升导航框架搭好之后页面内部我不建议继续用一堆原始 QWidget 拼。最小成本的升级方式是把按钮、输入框、开关、消息通知这些高频控件换成 qfluentwidgets 的版本。比如原本你写提示消息可能要自己造一个弹窗而 qfluentwidgets 的 InfoBar 可以直接在窗口右上角弹出彩色消息条from qfluentwidgets import InfoBar, InfoBarPosition def show_success(self): InfoBar.success( title操作成功, content数据已保存, orientQt.Horizontal, isClosableTrue, positionInfoBarPosition.TOP_RIGHT, duration2000, parentself, )再比如开关按钮qfluentwidgets 的 SwitchButton 自带过渡动画和滑块效果比 QCheckBox 好看得多。这类控件替换成本很低但对用户观感的影响非常直接。我实际做的时候会把整个页面的按钮、输入框、下拉框都换成 Fluent 版本改动量远小于自己写 QSS。3.3 一键切换深浅色主题现代界面的另一个标志是深浅色主题切换。qfluentwidgets 对主题做了封装只需要调用from qfluentwidgets import setTheme, Theme setTheme(Theme.LIGHT) # 亮色 setTheme(Theme.DARK) # 暗色我一般会在标题栏加一个切换按钮用户点一下就在亮色和暗色之间切换。qfluentwidgets 的大部分控件会自动响应主题变更不需要你手动改样式表这一点比自己维护 QSS 省心太多。如果你有少量自定义控件记得在主题切换信号里刷新一下否则会出现部分区域颜色不跟随的情况。4. 高频问题与排查实录4.1 界面打开一片黑多半是 OpenGL 在捣乱这是 PyQt5 里最经典的坑之一。症状是程序能跑、没有报错但窗口打开后内容区域是黑的或者某些控件不刷新。尤其多见于远程桌面、虚拟机、老显卡驱动环境下。根因是 Qt 默认走 OpenGL 渲染而目标机器的 OpenGL 驱动不可用或不完整。解决方式很简单在创建 QApplication 之前强制 Qt 使用软件渲染。import os os.environ[QT_OPENGL] software或者在代码里from PyQt5.QtCore import Qt from PyQt5.QtWidgets import QApplication QApplication.setAttribute(Qt.AA_UseSoftwareOpenGL, True) app QApplication([])注意这两个办法都要在 QApplication 创建之前执行否则不生效。如果你的界面里用了 QWebEngineView 这类组件软件渲染还可能让网页绘制变得很慢这时候就需要针对目标机器重新评估到底是解决驱动问题还是换一个不依赖 WebEngine 的 HTML 展示方案。4.2 PyQt5 里显示 HTML别一上来就上 WebView“pyqt5 显示 html”这个需求我见过很多新手一上来就导入 QWebEngineView然后发现包特别大、启动特别慢。实际上如果你的 HTML 只是简单的富文本、图文混排、带基础样式的说明内容用 QTextBrowser 就够了代码简单渲染快from PyQt5.QtWidgets import QTextBrowser browser QTextBrowser() browser.setHtml(h1标题/h1p正文内容/p)如果确实需要完整网页渲染能力再考虑 QWebEngineView。关于 WebView2我也看到有人在 PyQt 项目里尝试把 Edge WebView2 嵌进来理论上可以通过窗口句柄方式做到但维护成本明显高于 Qt 自带的 WebEngine而且跨平台性变差。我的建议是纯 PyQt 项目优先用 QWebEngineView除非你整个应用已经深度绑定 WebView2 生态否则不要两头都占。4.3 QTreeWidgetItem 里加 ComboBox注意数量别太多“在 QTreeWidgetItem 中增加 ComboBox”这个需求其实很好实现PyQt5 本身就支持给 tree 的单元格设置任意控件from PyQt5.QtWidgets import QComboBox, QTreeWidgetItem combo QComboBox() combo.addItems([选项A, 选项B, 选项C]) tree.setItemWidget(item, 1, combo)但这个操作有个隐藏问题如果 QTreeWidget 里有很多行每一行都放一个 QComboBox控件数量会迅速膨胀界面滚动时会明显卡顿。我之前在一个批量配置工具里试过几千行下来切换页面都费劲。后来改成“只有当前编辑的行才放 QComboBox”其余行用 QLabel 显示当前值性能立刻好很多。这是一个很容易被忽略的实践细节。4.4 高分屏和分辨率适配必须在入口就做好PyQt5 在高分屏上的表现不如 PyQt6 省心尤其是 Windows 下缩放比例不是 100% 的时候界面容易发虚或者控件布局变形。这一步必须在程序入口处理from PyQt5.QtCore import Qt from PyQt5.QtWidgets import QApplication QApplication.setAttribute(Qt.AA_EnableHighDpiScaling, True) QApplication.setAttribute(Qt.AA_UseHighDpiPixmaps, True) app QApplication([])如果你用的是 Qt 5.15还可以在创建 QApplication 之后设置缩放策略app.setHighDpiScaleFactorRoundingPolicy( Qt.HighDpiScaleFactorRoundingPolicy.PassThrough )这样在缩放系数不是整数倍的屏幕上控件尺寸会更接近设计稿。实测下来高分屏问题越早处理代价越低等业务页面都做完再回头适配很多布局都得重新调。4.5 安装慢、依赖脏别硬扛PyQt5 的 wheel 本身就几十 MB安装慢是正常的但不是不能优化。用 uv 或者国内 pypi 镜像都能明显提速。相比安装慢更烦的是脏环境。我见过团队把pyqt5-qt55.15.19 registryhttps://pypi.tuna.ts...这种行直接提交到 requirements.txt结果同事一装就报错。这种问题多半是之前安装时用了带镜像地址的方案pip freeze 把访问地址也记进了依赖声明。不要在这种问题上浪费时间固定一个大版本重新生成锁文件比分析那行 URL 更划算。5. 我做这套方案时沉淀下来的几个细节5.1 自定义窗口后别自己重写拖动逻辑FramelessWindow 已经处理了窗口拖动、边缘缩放和阴影。很多第一次接触的人习惯性地去写 mousePressEvent 和 mouseMoveEvent结果写完发现跟框架自带的拖动逻辑冲突窗口位置错乱、按钮失灵。我的经验是只要用了 FramelessWindow就不要再造拖拽轮子。你只需要往 titleBar 里加业务控件即可。当然如果你想在标题栏加自定义按钮需要先搞清楚 titleBar 的布局结构。qfluentwidgets 的标题栏本身是一个可以扩展的控件左右区域都可以塞控件但别破坏它内部那套按钮顺序否则最小化、最大化、关闭的行为会变得不可控。5.2 渐进式替换比一次性重写稳妥如果你的项目已经有完整的 PyQt5 界面别想着一天内全部换掉。最稳的路线是先把外层窗口换成 FramelessWindow让整个应用先有一个现代感的“壳”然后把高频操作的主页面控件逐步换成 qfluentwidgets 版本比如按钮、输入框、提示条。这样每一步改动都能独立验证出问题也容易定位。我用这个思路做过一次改造旧代码基本没有伤筋动骨界面观感却完全是两个时代。对于着急交付的项目这一点很关键。5.3 团队协作时把 qfluentwidgets 的版本固定住qfluentwidgets 还在快速迭代版本升级偶尔会改 API。团队协作时建议大家像固定 PyQt5 一样把 qfluentwidgets 版本也锁死否则 A 机器用的新版、B 机器用的旧版同一个代码可能在一台机器上正常、另一台机器上报错。我在项目里一般会在 requirements 里写死到小版本号比如qfluentwidgets1.5.5不轻易升除非有非升不可的特性或修复。最后分享一个我个人的习惯不管项目多急用 FramelessWindow 的第一版永远是拿最小 demo 验证标题栏按钮、窗口阴影、拖拽缩放是否正常再往里面填业务页面。很多人一上来就把整个业务界面塞进去结果运行后一片黑、按钮拖不动排查起来特别费劲。这个组合适合渐进式改造先让壳好看再让内容好用来回折腾的成本才能降到最低。
返回列表