ARTICLE DETAIL

资讯详情

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

用PyQt打造桌面数据看板:从接口到界面完整实践

用PyQt打造桌面数据看板:从接口到界面完整实践 简介基于PyQt开发的产品数据看板项目面向需要快速掌握桌面端数据可视化监控的Python开发者可展示产品良率、销售或运营数据。整个压缩包仅14KB共6个文件由3个Python脚本、1个Qt Designer界面文件、1份Excel样例数据及1个缓存文件构成其中App.py负责应用入口main_page.py与main_page.ui定义主窗口和交互布局test.py用于功能验证。已有147人浏览学习适合PyQt初学者参考或在此基础上扩展为实际看板工具。项目麻雀虽小但结构清晰既演示了PyQt程序的标准组织方式也展示了如何将外部Excel数据通过界面图表呈现读者可掌握.py与.ui协作开发、数据导入展示和基本测试流程快速移植到自己的产品质量监控场景中。1. 项目背景与整体设计思路1.1 为什么用 PyQt 做产品看板先交代一下背景我所在的小团队一直需要一个内部产品看板用来实时展示各业务线的数据指标。之前大家习惯打开浏览器看Web端报表但实际用下来有几个痛点很明显一是每次都要手动刷新开一堆标签页数据分布在不同系统里二是领导或运营同事希望打开电脑就能看到核心指标不想折腾登录、导航那一套三是我们有些数据来自内部接口并不适合直接暴露给浏览器端。后来我决定用 PyQt 写一个桌面端产品看板。PyQt 是 Qt 框架的 Python 绑定拿来写这种内部工具特别顺手既有 Qt 成熟的桌面组件生态又能直接用 Python 的 requests、pandas 做数据获取和处理。比起从零写 Web 前端PyQt 的桌面形态天然适合开机即看的场景而且打包成 exe 后扔给同事双击就能跑不需要配环境。这个项目适合谁来参考如果你也是用 Python 做数据分析或后端开发但需要快速交付一个带界面的内部工具或者你想把日常的报表、监控、看板类需求从 Web 搬回桌面端那这篇内容应该对你有帮助。用 PyQt 做看板核心价值就是四个字快、稳、省。快是开发快稳是 Qt 的组件成熟不容易出幺蛾子省是省掉了一整套 Web 前后端链路。1.2 核心需求拆解动手之前我先把需求拆成了几个明确的问题展示什么业务线核心指标比如今日订单量、销售额、客单价、库存预警、TOP 排行榜。数据从哪来内部 HTTP 接口返回 JSON部分数据来自数据库查询结果。刷新方式支持手动刷新和自动定时刷新间隔可配置。交互要求点击某一行能跳转详情关键指标变色高亮异常数据醒目提醒。部署形态Windows 桌面 exe双击即用非技术同事也能上手。这些需求看起来简单但真正实现的时候涉及到的坑不少。比如刷新数据的时候界面不能卡死这就需要把网络请求放到子线程里跑再比如表格需要定时更新数据还要保证用户在拖动滚动条、点击排序时不会被刷新打断。这些都是在实际开发中才浮现出来的问题。我的整体方案分三层数据层负责拉取和处理数据逻辑层负责状态管理和定时刷新界面层用 QTableWidget 和 QLabel 拼出看板布局。层与层之间通过 Qt 的信号槽机制通信这样结构清晰后续要加指标、换数据源都方便。2. 技术选型与界面布局方案2.1 PyQt5 还是 PySide6怎么选先说一下框架版本。PyQt5 和 PySide6 我都用过这个项目最终选了 PyQt5。原因很简单一是内部同事的 Windows 机器大多还是 Python 3.8、3.9 的环境PyQt5 的兼容性最好打包出来的 exe 在 Win10 上跑得很稳二是网上 PyQt5 的中文资料和踩坑帖子最多遇到问题基本都能搜到答案。如果你是新项目也可以考虑 PySide6它是 Qt 官方支持的 Python 绑定License 更友好API 和 PyQt5 差别很小。这次选择 PyQt5说到底还是在已有环境里求稳的思路。比如 QTableWidget、QTimer、QThread 这些核心模块两个框架用起来几乎一样后续真要迁移也不费劲。顺便说一句装 PyQt5 的时候建议用pip install PyQt55.15.x别直接装最新版有些新版本对旧版 Windows 的兼容性反而不好。我在一台 Win7 机器上实测过5.15.4 能跑更高版本就报错这属于典型的兼容性坑。2.2 看板布局表格为主、卡片为辅界面布局我一开始想做得很炫但后来发现清晰比炫重要。产品看板的核心目标是快速传递信息所以我的主界面只分三个区域顶部状态栏显示当前时间、最近刷新时间、数据源连接状态。中部主表格用 QTableWidget 展示核心指标一行一条数据关键列用颜色标出。右侧辅助面板放几个 QLabel 显示汇总数字比如总销售额、总订单量、异常数。顶部状态栏用 QHBoxLayout 加几个 QLabel 搞定右侧面板用 QVBoxLayout 从上到下排列卡片。主表格占了大部分宽度毕竟这是用户最常盯的地方。每个卡片其实就是一个带样式的 QFrame内部放两个 QLabel一个显示指标名一个显示数值。这样设计的好处是布局灵活哪天想增加一个指标卡片加一个 QFrame 就行完全不动主逻辑。表格部分我选 QTableWidget 而不是 QTableView是因为这个看板的数据量不大几十行到几百行QTableWidget 用起来更直接不需要自定义 Model。如果你要展示上万行数据那建议换 QTableView QAbstractTableModel性能差距还是很明显的。我这个场景里QTableWidget 省了很多样板代码。2.3 为什么不用 Web 技术栈可能有人会问现在做看板用 Vue ECharts 做个网页不是更漂亮吗确实Web 方案在图表生态上有优势ECharts 的图表很漂亮交互也灵活。但用 PyQt 做桌面看板有几个 Web 方案没法比的点桌面端可以很方便地读取本地配置、调用本地脚本不需要起服务、配端口。可以做成开机自启、最小化到系统托盘像原生应用一样常驻。内部数据不需要暴露到 Web 服务端减少一层安全风险。对不擅长前端的 Python 工程师来说开发效率更高。当然PyQt 的图表不如 ECharts 丰富这是事实。我的对策是核心数据用表格展示趋势和对比图优先接第三方库如 pyqtgraph 或者直接把 matplotlib 的 Figure 嵌入 QWidget。实际用下来表格展示当前状态非常合适图表展示变化趋势也有现成方案。这个看板项目里我暂时没有做特别复杂的图表因为团队最关心的是现在怎么样而不是过去怎么变。3. 核心模块实现与关键代码3.1 数据层从接口拉数据统一成 DataFrame所有看板的数据都从内部接口获取返回 JSON结构大概是{ code: 0, data: { total_orders: 1280, total_sales: 356000.50, products: [ {name: A产品, orders: 320, sales: 89000, stock: 45}, {name: B产品, orders: 289, sales: 76000, stock: 12} ] } }我在数据层写了一个DataFetcher类负责两件事用 requests 请求接口、解析 JSON并把核心数据转成 pandas DataFrame。转成 DataFrame 的好处是后面对齐、排序、计算汇总指标都非常方便比手动遍历字典省事太多。import requests import pandas as pd class DataFetcher: def __init__(self, api_url, timeout5): self.api_url api_url self.timeout timeout def fetch(self): resp requests.get(self.api_url, timeoutself.timeout) resp.raise_for_status() payload resp.json() if payload.get(code) ! 0: raise RuntimeError(f接口返回异常: {payload.get(msg)}) data payload[data] df pd.DataFrame(data[products]) summary { total_orders: data[total_orders], total_sales: data[total_sales], updated_at: pd.Timestamp.now() } return df, summary这里有个细节网络请求不能放在 UI 线程里做否则刷新的时候整个窗口会卡住用户拖动窗口都没反应。我的做法是每次刷新时把请求任务丢给 QThread 或者用 QThreadPool 跑请求完成后通过信号把 DataFrame 传回主线程再更新表格。如果你数据量小、接口响应快也可以先用同步请求顶着但一旦数据量上来或者网络抖动卡界面这个问题迟早会暴露。3.2 界面层用 QTableWidget 做核心表格主表格的列设计是这样的产品名称、订单量、销售额、库存、状态。状态列的值是程序根据库存和销售情况算出来的比如库存低于阈值就标记为库存预警销售额环比下降就标记为下降方便用户一眼看到问题。关键代码如下from PyQt5.QtWidgets import QTableWidget, QTableWidgetItem, QHeaderView from PyQt5.QtCore import Qt from PyQt5.QtGui import QColor class ProductTable(QTableWidget): def __init__(self, parentNone): super().__init__(parent) self.setColumnCount(5) self.setHorizontalHeaderLabels([产品名称, 订单量, 销售额, 库存, 状态]) self.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) self.setEditTriggers(QTableWidget.NoEditTriggers) self.setSelectionBehavior(QTableWidget.SelectRows) def update_data(self, df): self.setRowCount(len(df)) for row, item in df.iterrows(): name_item QTableWidgetItem(str(item[name])) order_item QTableWidgetItem(str(item[orders])) sales_item QTableWidgetItem(f{item[sales]:,.0f}) stock_item QTableWidgetItem(str(item[stock])) status 正常 if item[stock] 20: status 库存预警 elif item[sales] 50000: status 偏低 status_item QTableWidgetItem(status) if status 库存预警: status_item.setForeground(QColor(#d93025)) status_item.setBackground(QColor(#fce8e6)) elif status 偏低: status_item.setForeground(QColor(#e37400)) status_item.setBackground(QColor(#fef7e0)) for col, cell_item in enumerate([name_item, order_item, sales_item, stock_item, status_item]): self.setItem(row, col, cell_item)用 QTableWidgetItem 的setForeground和setBackground可以直接设置文字颜色和背景色这是最常用的高亮手段。实测下来有三种状态颜色就够用了不要搞太多颜色否则整个表格看起来花花绿绿的反而失去了重点。表格更新的频率要控制好。我一开始设置 5 秒自动刷新结果发现用户在盯某个数据的时候表格突然跳一下体验很差。后来改成默认 30 秒刷新一次同时提供一个立即刷新按钮30 秒只是个兜底保证数据不会太旧。这个设计我觉得更贴近实际使用习惯。3.3 刷新机制QTimer QThread 的组合自动刷新用 QTimer 实现设置好时间间隔然后绑定刷新函数即可。但前面说过刷新函数里如果直接发 HTTP 请求界面会卡。所以我把请求放到 QThread 里跑完后再发信号回来更新界面。代码大致长这样from PyQt5.QtCore import QThread, pyqtSignal, QTimer class FetchWorker(QThread): fetched pyqtSignal(object, object) failed pyqtSignal(str) def __init__(self, fetcher): super().__init__() self.fetcher fetcher def run(self): try: df, summary self.fetcher.fetch() self.fetched.emit(df, summary) except Exception as e: self.failed.emit(str(e)) class DashboardWindow(QMainWindow): def __init__(self): super().__init__() self.current_worker None self.timer QTimer(self) self.timer.timeout.connect(self.start_refresh) def start_refresh(self): if self.current_worker and self.current_worker.isRunning(): return self.current_worker FetchWorker(self.fetcher) self.current_worker.fetched.connect(self.on_data_ready) self.current_worker.failed.connect(self.on_fetch_failed) self.current_worker.start()这里有个很关键的坑如果上一次请求还没完成又触发了一次新的刷新容易造成数据乱序。我的方案很简单——在start_refresh里判断 worker 是否还在运行如果还在跑就直接 return放弃这次刷新。这个逻辑虽然简单粗暴但很有效避免了多线程更新界面的各种诡异问题。刷新完成后on_data_ready里拿到 DataFrame调用表格的update_data同时更新顶部状态栏的最近刷新时间。def on_data_ready(self, df, summary): self.table.update_data(df) self.label_orders.setText(f总订单量: {summary[total_orders]}) self.label_sales.setText(f总销售额: {summary[total_sales]:,.0f}) self.label_time.setText(f刷新时间: {summary[updated_at].strftime(%H:%M:%S)})3.4 样式优化QSS 让看板更像看板默认的 Qt 界面确实不太好看但好消息是 Qt 支持 QSSQt Style Sheets语法跟 CSS 差不多花点时间调一下就能让界面观感提升不少。我主要做了三件事设置全局字体、给头部加底色、给卡片加圆角和边框。APP_STYLE QMainWindow { background-color: #f5f6fa; } QTableWidget { background-color: #ffffff; alternate-background-color: #fafbfc; border: 1px solid #e1e4e8; border-radius: 6px; font-size: 13px; } QHeaderView::section { background-color: #f0f2f5; padding: 8px; border: none; border-bottom: 2px solid #d0d7de; font-weight: bold; } QFrame#cardFrame { background-color: #ffffff; border: 1px solid #e1e4e8; border-radius: 8px; } 用#cardFrame这种 ID 选择器可以精确控制某个组件的样式比逐个设置 setStyleSheet 干净多了。这算是一个实用经验把全局样式放在一个常量字符串里启动时app.setStyleSheet(APP_STYLE)统一设置完全不用在每个控件上单独写样式。表格开启交替行背景色setAlternatingRowColors(True)后大密度数据看起来舒服很多。字体建议优先用微软雅黑在 Windows 下渲染效果比默认字体好不少。4. 常见问题与排查技巧实录4.1 界面卡死requests 阻塞主线程症状点击刷新后整个窗口拖不动按钮点了没反应过几秒才恢复。原因网络请求直接写在了按钮的回调函数里请求期间主线程被阻塞Qt 的事件循环无法处理界面事件。排查思路在请求前打一个日志请求回来后打一个日志看两个日志的时间差就能确认阻塞位置。如果时间差正好等于接口响应时间说明问题就是同步请求导致的。解决办法把网络请求放到 QThread 里执行或者用 QNetworkAccessManager 的异步方式。用 QThread 看起来代码多一点但逻辑直观排查也方便。4.2 自动刷新时用户正在操作表格症状30 秒一到表格强制更新用户正在看某一行或者准备右键复制数据结果行内容突然变了选中状态也没了。原因update_data直接调setRowCount重建数据和单元格会把用户的交互状态全部清掉。解决办法更新表格前先记住当前选中行的产品名称更新完后再按名称找回对应的行并恢复选中。另一个办法是只在数据确实变化时才更新表格数据没变就不动界面。对于状态变化频繁的列可以只更新变化单元格的颜色而不是整行重建。# 更新前记住选中行 selected_name None if self.table.currentRow() 0: selected_name self.table.item(self.table.currentRow(), 0).text() # 更新数据... # 更新后尝试恢复选中行 if selected_name: for row in range(self.table.rowCount()): if self.table.item(row, 0).text() selected_name: self.table.selectRow(row) break4.3 打包 exe 后字体模糊、样式丢失症状开发环境界面正常打包后用 PyInstaller 生成的 exe 打开字体发虚样式明显不对。原因PyInstaller 打包时没有带上 Qt 的样式插件或者字体渲染环境跟开发机不一致。解决办法PyInstaller 打包命令加上--hidden-importPyQt5.QtSvg并且在入口代码里把渲染策略设置为 AA_EnableHighDpiScaling。另外尽量用相对路径或资源文件加载 QSS别用绝对路径否则换台机器路径就失效了。pyinstaller -F -w -i dashboard.ico dashboard.py这里-F是打包成单文件-w是不显示命令行窗口。如果打包后运行报缺 DLL那大概率是 PyInstaller 版本跟 PyQt5 版本不匹配换一个 PyInstaller 版本一般能解决。4.4 请求接口偶尔超时症状看板用着用着某个数据不更新了状态栏时间停留在上一次刷新时间没有任何提示。原因requests 默认没有超时时间一旦接口端卡住请求线程就会一直挂着。如果接口服务重启或者网络抖动就很容易出现这种假死。解决办法给 requests.get 加 timeout 参数并在 FetchWorker 里捕获超时异常把错误信息抛到界面上显示。同时加一个最后成功时间的记录超过一定时长没成功就自动把状态栏的连接状态标红。try: resp requests.get(self.api_url, timeout5) except requests.Timeout: raise RuntimeError(接口请求超时)这个超时时间要根据实际接口响应速度来定。内部接口响应一般几十毫秒设 5 秒已经非常宽松了。4.5 数据字段对不上导致表格错列症状接口返回的产品数据里某个字段为空或者字段名改了表格里出现NaN或者数据串位。原因接口的数据结构属于约定大于配置看板端硬编码了字段名一旦接口方的键名变化表格就乱了。解决办法在数据层里做一个字段映射和校验。用 pandas 读取 JSON 后检查必要的列是否存在不存在就补默认值存在但为空就填充 0 或未知。这样即使接口有些小变动看板也不会直接崩。required_cols [name, orders, sales, stock] for col in required_cols: if col not in df.columns: df[col] 05. 配置化改进与后续扩展方向5.1 用配置文件管理接口地址和刷新间隔这个看板做好之后我又顺手做了一个配置化的小改进把接口地址、刷新间隔、库存预警阈值都放到一个 config.json 里这样同事拿到 exe 后不需要改代码编辑一下配置文件就能适应自己的环境。{ api_url: http://internal.example.com/api/dashboard, refresh_interval: 30, stock_warning_threshold: 20, sales_warning_threshold: 50000, window_title: 产品看板, top_summary: true }程序启动时用 json.load 读配置文件把值传给各个模块。有了这个配置文件看板的复用性一下子就高了。换了数据源、改了阈值只需要改文本文件不用再重新打包。在实际的团队协作中这种配置化的设计特别实用。不同业务线关心的指标不一样阈值也不一样有了配置文件每个人都能根据自己的需求调整。5.2 后续可以扩展什么这个看板项目目前已经稳定运行了一段时间后续如果要继续迭代我可能会按下面几个方向走接入 pyqtgraph 画趋势图在右侧面板里增加近 7 天销售额趋势的小图数据从历史表里查用 pyqtgraph 画折线图轻量不卡顿。支持数据导出右键导出当前表格为 CSV方便同事做进一步分析。用 QFileDialog 结合 pandas 的df.to_csv几行代码就能实现。增加告警音效当某个指标触达预警线时播放一个提示音或者弹一个系统托盘通知。Qt 有 QSystemTrayIcon做起来不复杂。多数据源聚合目前只接了一个接口如果后续接多个数据源可以在数据层做聚合统一转成标准 DataFrame界面层基本不用动。6. 写在最后的实操心得这个基于 PyQt 的产品看板项目技术上不算难真正花时间的反而是把需求想清楚这一步。做之前我一度纠结要不要上 ECharts、要不要做酷炫大屏后来发现团队真正需要的只是一个打开就能看到关键数据的桌面工具。想清楚了这一点技术选型和界面设计就顺理成章了。我个人的体会是PyQt 特别适合做团队内部的小工具它的下限很低随便拖几个控件就能跑上限也不低QSS 加上自定义信号槽能做出非常专业的桌面应用。如果你也遇到数据散落在各个系统、打开看板要登录半天、图表看板只展示不用来干活这类问题不妨试试 PyQt 自己写一个成本不高但每天都能省下不少时间。最后再分享一个小技巧看板类应用一定要把数据新鲜度展示出来。用户看到界面上有最后更新时间心里就有底不会一直怀疑数据是不是挂了。这个小细节比任何炫酷的视觉效果都实用。本文还有配套的精品资源点击获取
返回列表