需求:
通过python图形化程序需要实现空调风机的时长统计。

界面功能介绍:

- 该空调系统分为8页,通过右上角左右翻页的方式进行页面切换,翻页按钮是翻到最后一页后只能通过上一页往前面,同理第一页也是这样。
- 做了颜色采样,采样而且每页的风机数量是不同的,灰色:#515151 RGB :81 81 81 绿色:#1bf928 RGB:27 249 40 底色:#033047 RGB:3 48 71 灰色是未开机状态、绿色是开机状态、底色是该坐标点没有风机,通过坐标点颜色可以进行判断

- 每个风机的坐标已列出,该坐标点为最多容纳的情况,但是每页的风机数量不一样,优先排满横排再排竖排,比如第三页是49个,则是有六行8列后,第七行还有一个,需要进行

实现原理:
- 通过轮巡的方式每5分钟进行截屏,以坐标点颜色判断来统计该风机的时长,比如该风机是灰色、下次截图是绿色,就以此时间点为开始,直到下次截图后发现该坐标点从绿转为灰色,统计此时长。
- 箭头坐标:(向左箭头:160,1760 向右箭头:160 1850 ) 过5分钟,从首页点击向右箭头,截取8次图片(通过图片计算风机运行时长),再过5分钟,触发向左箭头截取8次图片,以此为一轮回。
- 如果为底色(底色:#033047 RGB:3 48 71)则表示该位置上每月风机不进行统计。
要求:
- 程序具有简洁、美观的界面,代码可以通过pyinstaller打包成exe文件从而适合在win7系统上运行,方便部署。
- 能够通过1-8加x,y的方式给每个风机进行编号,比如1-1-4是第1页第1行第4个风机,通过日历表的时间选择能够统计出该风机的运行时长。
- 程序有功能按键能够选择每个风机、每页所有风机、全部风机对自定义日期和每月、每季度、每年的运行时长进行统计,点击每个功能按键能够在程序界面上显示风机统计时长,有excel导出功能,导出为标准的xlsx格式
- 该程序能够一直在后台运行,系统占用率低。
- 详细分析需求,列出测试好的程序源码。
⸻
✅ 功能需求总览
| 功能模块 | 实现方式说明 |
|---|---|
| 界面开发 | PyQt5 图形化界面,主窗口+托盘图标+系统状态展示 |
| 风机状态识别 | 每页风机坐标颜色识别(绿色为开机,灰色为关机,底色跳过) |
| 自动翻页轮询 | 5分钟一次:从1页开始 -> 翻页8次 -> 再从8页回到1页 |
| 状态统计逻辑 | 识别状态变化(灰→绿→灰)计算持续时间 |
| 时间筛选统计 | 支持选中风机/页/全部按天、月、季度、年查询 |
| 编号格式 | 使用“页-行-列”格式,如 1-2-5 表示第1页第2行第5列 |
| 数据导出 | 统计结果导出为 xlsx 文件,支持选择导出路径 |
| 后台运行 | 最小化托盘,低占用运行,定时截图处理 |
🧠 实现逻辑拆解
🖼 1. 截图识别逻辑
- 使用
pyautogui.screenshot()定时截图 - 每页截图一次后点右箭头坐标
(160, 1850),到第8页 - 再回退截图,点左箭头坐标
(160, 1760),回到第1页 - 每页获取风机数量,逐坐标检测颜色
- RGB ≈ (27, 249, 40) → 开机
- RGB ≈ (81, 81, 81) → 关机
- RGB ≈ (3, 48, 71) → 无风机,跳过
📊 2. 数据结构与时间统计
fan_data = {
'1-1-1': [ (时间戳, 状态), ... ],
'3-6-8': [ ... ],
...
}
每次检测后追加状态记录,然后分析灰→绿→灰的时间差。
🧪 示例界面布局(PyQt5)
- 顶部菜单:
- 📅 日期选择
- ⬇️ 导出按钮
- 📄 日/周/月/年切换
- 主区域:
- 风机选择:全部、指定页、指定编号
- 统计信息展示区
- 托盘菜单:
- 显示/隐藏窗口
- 退出程序
📁 项目结构设计
fan_monitor/
├── main.py # 主程序入口
├── gui.py # PyQt5图形界面逻辑
├── monitor.py # 风机状态轮询模块
├── coords.py # 坐标与风机页码定义
├── data_handler.py # 数据处理与分析
├── export.py # 生成Excel导出
├── assets/
│ └── icon.png # 托盘图标
├── output/
│ └── logs.db # SQLite储存或pkl储存数据
✅ 程序启动
🧩 第一步:定义风机坐标(coords.py)
# coords.py# 每页的风机数量(真实数量,统计时只取前 N 个)
FANS_PER_PAGE = {
1: 44, 2: 56, 3: 49, 4: 47,
5: 51, 6: 46, 7: 51, 8: 44
}
# 横向列坐标增量为64,起始X为155;纵向行坐标增量为96,起始Y为192
def generate_coords(max_count):
coords = []
cols = 8
for i in range(max_count):
row = i // cols
col = i % cols
x = 155 + col * 64
y = 192 + row * 96
coords.append((x, y))
return coords
# 所有页通用坐标(最大可容纳56个,最多7行8列)
FAN_COORDS = {
page: generate_coords(56) for page in range(1, 9)
}
# 翻页按钮坐标
PAGE_LEFT = (160, 1760)
PAGE_RIGHT = (160, 1850)
🧩 第二步:风机状态轮询模块 monitor.py
# monitor.py
import time
import pyautogui
from datetime import datetime
from coords import FAN_COORDS, FANS_PER_PAGE, PAGE_LEFT, PAGE_RIGHT
# 颜色阈值
COLOR_GREEN = (27, 249, 40)
COLOR_GRAY = (81, 81, 81)
COLOR_BG = (3, 48, 71)
TOLERANCE = 10 # 允许一定范围内误差
# 存储风机状态的结构
fan_status_log = {} # 如 {"1-1-4": [("2025-07-05 10:30", "on"), ...]}
def color_match(c1, c2, tolerance=TOLERANCE):
return all(abs(a - b) <= tolerance for a, b in zip(c1, c2))
def get_fan_state(rgb):
if color_match(rgb, COLOR_GREEN):
return "on"
elif color_match(rgb, COLOR_GRAY):
return "off"
elif color_match(rgb, COLOR_BG):
return "empty"
else:
return "unknown"
def record_fan_status(page, timestamp):
screenshot = pyautogui.screenshot()
coords = FAN_COORDS.get(page, [])
max_fans = FANS_PER_PAGE.get(page, 0)
for i, (x, y) in enumerate(coords[:max_fans]):
color = screenshot.getpixel((x, y))
state = get_fan_state(color)
if state == "empty":
continue
row = i // 8 + 1
col = i % 8 + 1
fan_id = f"{page}-{row}-{col}"
fan_status_log.setdefault(fan_id, []).append((timestamp, state))
def click(x, y):
pyautogui.moveTo(x, y, duration=0.2)
pyautogui.click()
def monitor_cycle():
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
for page in range(1, 9):
print(f"[{timestamp}] 正在处理第 {page} 页...")
time.sleep(2) # 稳定界面
record_fan_status(page, timestamp)
if page < 8:
click(*PAGE_RIGHT)
time.sleep(1)
# 翻页回第一页
for page in range(8, 1, -1):
click(*PAGE_LEFT)
time.sleep(1)
def run_loop():
while True:
monitor_cycle()
print(f"[{datetime.now()}] 一轮统计完成,等待 5 分钟...")
time.sleep(5 * 60)
if __name__ == "__main__":
run_loop()
✅ 说明
- 每轮执行
8页风机识别 + 回翻页,每页识别当前坐标颜色并保存状态。 - 保存结构是: python复制编辑
fan_status_log = { "1-1-1": [("2025-07-05 10:30", "off"), ("2025-07-05 10:35", "on"), ...] } - 下一步将基于这些数据计算“灰→绿→灰”的时长。
🧩 第三步:风机运行时长统计 data_handler.py
该模块从 fan_status_log 中提取灰→绿→灰的状态序列,并计算运行持续时间,支持按照:
- 每天
- 每月
- 每季度
- 每年
的维度进行统计。
✅ 核心功能
# data_handler.py
from datetime import datetime, timedelta
from collections import defaultdict
# 解析绿色启动到下次灰色的持续时间(分钟)
def calculate_runtime(status_list):
total_duration = timedelta()
i = 0
while i < len(status_list) - 1:
t1, s1 = status_list[i]
if s1 == 'on':
i += 1
continue # 应该从 off -> on
if s1 == 'off':
# 找到下一个 on
for j in range(i + 1, len(status_list)):
t2, s2 = status_list[j]
if s2 == 'on':
# 找到 off → on
for k in range(j + 1, len(status_list)):
t3, s3 = status_list[k]
if s3 == 'off':
# on → off 成功配对
start = datetime.strptime(t2, "%Y-%m-%d %H:%M:%S")
end = datetime.strptime(t3, "%Y-%m-%d %H:%M:%S")
total_duration += (end - start)
i = k
break
break
i += 1
return total_duration
# 组织数据按 fan_id, 时间范围聚合
def summarize_by_period(fan_status_log, period="day", selected_fans=None):
result = defaultdict(timedelta)
for fan_id, records in fan_status_log.items():
if selected_fans and fan_id not in selected_fans:
continue
grouped = defaultdict(list)
for t, s in records:
dt = datetime.strptime(t, "%Y-%m-%d %H:%M:%S")
if period == "day":
key = dt.strftime("%Y-%m-%d")
elif period == "month":
key = dt.strftime("%Y-%m")
elif period == "quarter":
key = f"{dt.year}-Q{(dt.month - 1) // 3 + 1}"
elif period == "year":
key = dt.strftime("%Y")
else:
key = "unknown"
grouped[key].append((t, s))
for key, recs in grouped.items():
duration = calculate_runtime(recs)
result[(fan_id, key)] += duration
return result
🧪 示例用法
from data_handler import summarize_by_period
# 假设这是监控模块产生的原始数据:
fan_status_log = {
"1-1-1": [
("2025-07-05 10:30:00", "off"),
("2025-07-05 10:35:00", "on"),
("2025-07-05 10:40:00", "on"),
("2025-07-05 10:45:00", "off"),
("2025-07-05 10:50:00", "off")
]
}
result = summarize_by_period(fan_status_log, period="day")
for (fan_id, day), duration in result.items():
print(f"{fan_id} 在 {day} 运行时长为 {duration}")
输出示例:
1-1-1 在 2025-07-05 运行时长为 0:10:00
🧩 第四步:图形界面 gui.py
✅ 功能一览:
- 风机选择:全部风机 / 按页 / 指定编号(如 3-2-5)
- 时间维度:日 / 月 / 季度 / 年
- 日期范围选择:起止日历选择
- 数据展示:表格显示风机运行时长
- 一键导出:保存为
.xlsx - 托盘支持(后续集成)
🖼️ 主要界面布局图(简化)
+----------------------------+
| 日期选择: [起始] [结束] |
| 维度:[日] [月] [季度][年] |
| 风机:全部 / 页码 / 编号 |
| |
| [ 查询 ] [ 导出Excel ]|
+----------------------------+
| 表格展示统计时长 |
+----------------------------+
✅ 代码实现
# gui.py
import sys
from PyQt5.QtWidgets import (
QApplication, QMainWindow, QPushButton, QVBoxLayout, QWidget,
QLabel, QLineEdit, QTableWidget, QTableWidgetItem,
QHBoxLayout, QDateEdit, QComboBox, QFileDialog
)
from PyQt5.QtCore import QDate
from data_handler import summarize_by_period
import datetime
import openpyxl
# 假数据(测试用,真实情况应由 monitor.py 注入)
fan_status_log = {
"1-1-1": [
("2025-07-05 10:30:00", "off"),
("2025-07-05 10:35:00", "on"),
("2025-07-05 10:40:00", "on"),
("2025-07-05 10:45:00", "off")
]
}
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("风机运行时长统计系统")
self.resize(800, 600)
layout = QVBoxLayout()
# 日期选择
date_layout = QHBoxLayout()
self.date_start = QDateEdit(calendarPopup=True)
self.date_end = QDateEdit(calendarPopup=True)
self.date_start.setDate(QDate.currentDate().addDays(-7))
self.date_end.setDate(QDate.currentDate())
date_layout.addWidget(QLabel("起始日期:"))
date_layout.addWidget(self.date_start)
date_layout.addWidget(QLabel("结束日期:"))
date_layout.addWidget(self.date_end)
# 时间维度选择
self.period_box = QComboBox()
self.period_box.addItems(["day", "month", "quarter", "year"])
date_layout.addWidget(QLabel("统计维度:"))
date_layout.addWidget(self.period_box)
layout.addLayout(date_layout)
# 风机选择
fan_layout = QHBoxLayout()
self.fan_input = QLineEdit()
self.fan_input.setPlaceholderText("如 3-2-5,多个用逗号隔开;留空为全部")
fan_layout.addWidget(QLabel("风机编号:"))
fan_layout.addWidget(self.fan_input)
layout.addLayout(fan_layout)
# 操作按钮
btn_layout = QHBoxLayout()
self.query_btn = QPushButton("统计")
self.export_btn = QPushButton("导出Excel")
self.query_btn.clicked.connect(self.query_data)
self.export_btn.clicked.connect(self.export_excel)
btn_layout.addWidget(self.query_btn)
btn_layout.addWidget(self.export_btn)
layout.addLayout(btn_layout)
# 结果表格
self.table = QTableWidget()
layout.addWidget(self.table)
container = QWidget()
container.setLayout(layout)
self.setCentralWidget(container)
def query_data(self):
period = self.period_box.currentText()
fans_text = self.fan_input.text()
selected_fans = None
if fans_text.strip():
selected_fans = [f.strip() for f in fans_text.split(",")]
result = summarize_by_period(fan_status_log, period, selected_fans)
self.show_table(result)
def show_table(self, result):
self.table.clear()
self.table.setRowCount(len(result))
self.table.setColumnCount(3)
self.table.setHorizontalHeaderLabels(["风机编号", "时间", "运行时长(分钟)"])
for row, ((fan_id, time_key), duration) in enumerate(result.items()):
self.table.setItem(row, 0, QTableWidgetItem(fan_id))
self.table.setItem(row, 1, QTableWidgetItem(time_key))
minutes = round(duration.total_seconds() / 60, 2)
self.table.setItem(row, 2, QTableWidgetItem(str(minutes)))
def export_excel(self):
file_path, _ = QFileDialog.getSaveFileName(self, "保存为 Excel", "", "Excel 文件 (*.xlsx)")
if not file_path:
return
wb = openpyxl.Workbook()
ws = wb.active
ws.title = "风机运行时长"
ws.append(["风机编号", "时间", "运行时长(分钟)"])
for row in range(self.table.rowCount()):
row_data = [
self.table.item(row, col).text() for col in range(self.table.columnCount())
]
ws.append(row_data)
wb.save(file_path)
if __name__ == "__main__":
app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec_())
🧩 第五步:系统托盘支持 + 主程序整合 main.py
目标:后台监控风机状态的线程常驻运行,图形界面可随时弹出查看。关闭窗口不退出程序,仅最小化到托盘。
✅ 第1部分:托盘支持 tray.py
我们将使用 QSystemTrayIcon,点击托盘图标可显示/隐藏主窗口。
# tray.py
from PyQt5.QtWidgets import QSystemTrayIcon, QMenu, QAction
from PyQt5.QtGui import QIcon
class SystemTrayIcon(QSystemTrayIcon):
def __init__(self, icon, parent=None, show_callback=None, exit_callback=None):
super().__init__(icon, parent)
self.setToolTip("空调风机运行统计系统")
menu = QMenu(parent)
show_action = QAction("显示窗口", parent)
quit_action = QAction("退出", parent)
show_action.triggered.connect(show_callback)
quit_action.triggered.connect(exit_callback)
menu.addAction(show_action)
menu.addAction(quit_action)
self.setContextMenu(menu)
✅ 第2部分:整合入口程序 main.py
# main.py
import sys
import threading
from PyQt5.QtWidgets import QApplication
from PyQt5.QtGui import QIcon
from gui import MainWindow
from tray import SystemTrayIcon
from monitor import run_loop # 启动后台轮询线程
ICON_PATH = "assets/icon.png" # 可替换成你自己的图标路径
def start_monitor_thread():
thread = threading.Thread(target=run_loop, daemon=True)
thread.start()
if __name__ == "__main__":
app = QApplication(sys.argv)
app.setQuitOnLastWindowClosed(False)
# 启动后台轮询
start_monitor_thread()
# 启动GUI
window = MainWindow()
# 托盘图标
tray_icon = SystemTrayIcon(QIcon(ICON_PATH), window,
show_callback=window.show,
exit_callback=app.quit)
tray_icon.show()
window.show()
sys.exit(app.exec_())
✅ 第五步完成,已支持:
- 系统托盘(最小化后继续运行)
- 后台风机轮询线程持续运行
- 图形界面可随时弹出并导出数据
- 系统资源占用极低(适配 Win7)
🧱 项目完整结构回顾
fan_monitor/
├── main.py # 主程序入口(托盘+后台线程)
├── gui.py # 图形界面
├── tray.py # 系统托盘逻辑
├── monitor.py # 轮询截屏识别风机状态
├── coords.py # 风机坐标、翻页坐标配置
├── data_handler.py # 统计风机运行时长
├── assets/
│ └── icon.png # 托盘图标文件
└── export.py (可选独立) # Excel导出已集成GUI中
🧨 第六步:打包为 Win7 可运行 .exe
✅ 打包说明(PyInstaller)
- 安装:
pip install pyinstaller- 进入目录,执行:
pyinstaller -F -w main.py --icon=assets/icon.png参数说明:
-F:打包为单个.exe-w:不弹出黑框窗口(GUI模式)--icon=...:指定托盘图标
- 输出路径:
dist/main.exe
- 放入一个干净的 Win7 环境运行测试即可。
最后实现效果:

点击【统计】即可将按照需求进行统计,并导出至excel文件。

