当前位置:首页>python>工业HMI界面开发:Python Tkinter实战

工业HMI界面开发:Python Tkinter实战

  • 2026-06-30 10:29:40
工业HMI界面开发:Python Tkinter实战

🏭 你以为Tkinter只能做玩具界面?

说真的,我第一次接到工业HMI项目的时候,脑子里第一个念头是:用Tkinter?这不是开玩笑吗?

那是一个污水处理厂的监控系统。甲方要求:实时显示12路传感器数据、阀门开关控制、历史曲线回放、报警联动。工期45天,预算有限,不允许引入商业SCADA授权。同事推荐Qt,但部署环境是老旧的Windows XP工控机——4GB内存,CPU还是赛扬双核。Qt的运行时直接把内存吃掉一半。

最后我们用Tkinter搞定了。整个程序启动时间不超过1.2秒,内存占用稳定在80MB以内,连续运行72小时无崩溃。

这篇文章,就是那段经历的技术沉淀。咱们不聊那些Hello World级别的按钮教程——直接上工业级的玩法:Canvas绘制动态仪表盘、串口数据实时刷新、多线程防界面冻结、报警状态机设计。能跑、能用、能上生产。


🔍 问题根源:Tkinter为什么在工业场景里"翻车"

很多人踩坑不是因为Tkinter不行,而是用法根本就错了

❌ 最常见的三个死法

第一种死法:在主线程里跑串口读取。

python1# 这是错的!千万别这样写2while True:3    data = serial_port.read(64)4    label.config(text=data)5    time.sleep(0.1)  # 界面直接卡死

主线程被占用,Tkinter的事件循环mainloop()根本没机会执行。界面冻住,鼠标点哪儿都没反应。用户以为程序崩了,直接强制关闭——然后串口没有正确关闭,下次启动报"端口被占用"。恶性循环。

第二种死法:Canvas上直接堆几百个图形对象,从不清理。

工业界面往往有实时曲线,每秒刷新一次,每次create_line()一个新对象。跑一小时之后,Canvas里堆了3600个line对象。内存泄漏,响应越来越慢,最终OOM。

第三种死法:用after()做定时刷新,但忘了处理异常。

串口断线、传感器超时、数据格式异常——任何一个未捕获的异常都会让after()的回调链断掉。界面看起来还在,但数据早就停止更新了。操作员盯着一个"假实时"的界面做决策,后果不堪设想。


💡 核心机制:你必须理解这三件事

1. Tkinter的单线程本质

Tkinter底层是Tcl/Tk,严格单线程。所有UI操作必须在主线程执行。这不是缺陷,是设计。理解这一点,你才能用对多线程方案。

正确姿势是:子线程负责IO,主线程负责渲染,用线程安全的队列传数据

2. Canvas的对象管理哲学

Canvas里每个图形都是一个"item",有唯一ID。实时更新的正确做法是复用item,而不是删了重建。coords()修改坐标,itemconfig()修改样式,性能差距可以达到10倍以上

3. after()是你的心跳,不是定时器

after(ms, callback)在Tkinter里是事件驱动的——它把回调注册到事件队列,由mainloop()在合适时机执行。这意味着:如果主线程被阻塞,after()也会延迟。所以绝对不能在回调里做任何耗时操作。


🚀 方案一:多线程架构 + 队列通信

这是整个HMI系统的骨架。先把这个搞对,后面才能谈别的。

python1import tkinter as tk2import threading3import queue4import serial5import time6import random  # 演示用,实际替换为真实串口78class HMIApp:9def __init__(self, root):10        self.root = root11        self.root.title("工业监控系统 v1.0")12        self.root.geometry("1024x768")13        self.root.configure(bg="#1a1a2e")1415# 线程安全队列,子线程往里塞数据,主线程来取16        self.data_queue = queue.Queue(maxsize=100)17        self.running = True1819        self._build_ui()20        self._start_data_thread()21        self._schedule_refresh()  # 启动心跳2223def _build_ui(self):24# 顶部标题栏25        title_frame = tk.Frame(self.root, bg="#16213e", height=50)26        title_frame.pack(fill=tk.X)27        title_frame.pack_propagate(False)2829        tk.Label(30            title_frame, text="⚙ 污水处理厂监控系统",31            bg="#16213e", fg="#e94560",32            font=("微软雅黑"16"bold")33        ).pack(side=tk.LEFT, padx=20, pady=10)3435        self.status_label = tk.Label(36            title_frame, text="● 通信正常",37            bg="#16213e", fg="#00ff88",38            font=("微软雅黑"11)39        )40        self.status_label.pack(side=tk.RIGHT, padx=20)4142# 数据显示区43        self.value_labels = {}44        data_frame = tk.Frame(self.root, bg="#1a1a2e")45        data_frame.pack(fill=tk.BOTH, expand=True, padx=20, pady=10)4647        params = [48            ("flow_rate""瞬时流量""m³/h"),49            ("pressure",  "管道压力""kPa"),50            ("ph_value",  "pH值",    ""),51            ("turbidity""浊度",    "NTU"),52        ]5354for i, (key, name, unit) in enumerate(params):55            cell = tk.Frame(data_frame, bg="#16213e", relief=tk.FLAT, bd=0)56            cell.grid(row=i//2, column=i%2, padx=10, pady=10, sticky="nsew")57            data_frame.columnconfigure(i%2, weight=1)58            data_frame.rowconfigure(i//2, weight=1)5960            tk.Label(cell, text=name, bg="#16213e",61                     fg="#888", font=("微软雅黑"10)).pack(pady=(15,0))6263            val_label = tk.Label(cell, text="--",64                                 bg="#16213e", fg="#00d4ff",65                                 font=("微软雅黑"32"bold"))66            val_label.pack()6768            tk.Label(cell, text=unit, bg="#16213e",69                     fg="#666", font=("微软雅黑"9)).pack(pady=(0,15))7071            self.value_labels[key] = val_label7273def _data_worker(self):74"""子线程:模拟串口读取(实际项目替换为serial.Serial)"""75while self.running:76try:77# 模拟数据,实际:data = ser.read(64); parsed = parse_modbus(data)78                data = {79"flow_rate"round(random.uniform(120180), 1),80"pressure":  round(random.uniform(280320), 1),81"ph_value":  round(random.uniform(6.87.4), 2),82"turbidity"round(random.uniform(0.52.0), 2),83                }84# 队列满了就丢弃旧数据,不阻塞子线程85if self.data_queue.full():86try:87                        self.data_queue.get_nowait()88except queue.Empty:89pass90                self.data_queue.put(data)91except Exception as e:92# 通信异常:推送一个错误标记93                self.data_queue.put({"__error__"str(e)})94            time.sleep(0.5)9596def _start_data_thread(self):97        t = threading.Thread(target=self._data_worker, daemon=True)98        t.start()99100def _schedule_refresh(self):101"""主线程心跳:每500ms从队列取数据刷新UI"""102try:103while not self.data_queue.empty():104                data = self.data_queue.get_nowait()105106if "__error__" in data:107                    self.status_label.config(text="● 通信异常", fg="#ff4444")108continue109110                self.status_label.config(text="● 通信正常", fg="#00ff88")111for key, label in self.value_labels.items():112if key in data:113                        label.config(text=str(data[key]))114115except Exception as e:116print(f"UI刷新异常: {e}")  # 生产环境换成日志117118finally:119# 无论如何都要续命,否则心跳停了120if self.running:121                self.root.after(500, self._schedule_refresh)122123def on_close(self):124        self.running = False125        self.root.destroy()126127if __name__ == "__main__":128    root = tk.Tk()129    app = HMIApp(root)130    root.protocol("WM_DELETE_WINDOW", app.on_close)131    root.mainloop()

踩坑预警daemon=True是关键。没有这个,主窗口关闭后子线程还在跑,进程无法退出,任务管理器里会看到僵尸Python进程。


🎨 方案二:Canvas绘制动态仪表盘

光有数字不够看。工业现场的操作员更喜欢仪表盘——一眼就能判断是否在正常范围。

python1import tkinter as tk2import math34class GaugeMeter(tk.Canvas):5"""6    圆弧仪表盘控件7    支持动态更新,内部复用Canvas item,无内存泄漏8    """9def __init__(self, parent, min_val=0, max_val=100,10                 title="参数", unit="", size=200, **kwargs):11super().__init__(parent, width=size, height=size,12                         bg="#16213e", highlightthickness=0, **kwargs)13        self.min_val = min_val14        self.max_val = max_val15        self.title = title16        self.unit = unit17        self.size = size18        self.cx = size / 219        self.cy = size / 2 + 1020        self.r = size * 0.382122        self._draw_static()   # 静态部分只画一次23        self._needle_id = None24        self._value_id = None25        self._arc_id = None26        self.set_value(min_val)2728def _draw_static(self):29        s = self.size30# 背景圆31        self.create_oval(32            self.cx - self.r - 8, self.cy - self.r - 8,33            self.cx + self.r + 8, self.cy + self.r + 8,34            fill="#0f3460", outline="#e94560", width=235        )36# 刻度线37for i in range(11):38            angle = math.radians(225 - i * 27)39            x1 = self.cx + (self.r - 5)  * math.cos(angle)40            y1 = self.cy - (self.r - 5)  * math.sin(angle)41            x2 = self.cx + (self.r - 18) * math.cos(angle)42            y2 = self.cy - (self.r - 18) * math.sin(angle)43            color = "#ff4444" if i >= 8 else "#888"44            self.create_line(x1, y1, x2, y2, fill=color, width=2)4546# 标题47        self.create_text(self.cx, self.cy + self.r * 0.55,48                         text=self.title, fill="#aaa",49                         font=("微软雅黑"9))5051def set_value(self, value):52        value = max(self.min_val, min(self.max_val, value))53        ratio = (value - self.min_val) / (self.max_val - self.min_val)54        angle = math.radians(225 - ratio * 270)5556# 指针端点57        nx = self.cx + self.r * 0.72 * math.cos(angle)58        ny = self.cy - self.r * 0.72 * math.sin(angle)5960# 复用指针item(关键!不是删了重建)61if self._needle_id is None:62            self._needle_id = self.create_line(63                self.cx, self.cy, nx, ny,64                fill="#00d4ff", width=3, capstyle=tk.ROUND65            )66else:67            self.coords(self._needle_id, self.cx, self.cy, nx, ny)6869# 复用数值文本70        display = f"{value:.1f} {self.unit}"71if self._value_id is None:72            self._value_id = self.create_text(73                self.cx, self.cy + self.r * 0.25,74                text=display, fill="#00d4ff",75                font=("微软雅黑"13"bold")76            )77else:78            self.itemconfig(self._value_id, text=display)7980# 超限变红81        needle_color = "#ff4444" if ratio >= 0.8 else "#00d4ff"82        self.itemconfig(self._needle_id, fill=needle_color)838485# 使用示例86if __name__ == "__main__":87    root = tk.Tk()88    root.configure(bg="#1a1a2e")89    root.title("仪表盘演示")9091    gauge = GaugeMeter(root, min_val=0, max_val=500,92                       title="管道压力", unit="kPa", size=220)93    gauge.pack(padx=30, pady=30)9495# 模拟动态更新96import random97def update():98        gauge.set_value(random.uniform(100480))99        root.after(800, update)100101update()102    root.mainloop()

我在项目里测过:同样更新1000次,复用item方案比删除重建快约8倍,内存占用也几乎不增长。这个差距在低配工控机上非常明显。


⚠️ 方案三:报警状态机

这是工业HMI里最容易被忽视、也最容易出事的部分。

报警不是简单地"超限就变红"。真实需求是:首次超限要闪烁提示,操作员确认后变为静态告警,恢复正常后自动消除。这就是一个状态机。

python1import tkinter as tk2from tkinter import font3from enum import Enum4from datetime import datetime5import math67# ==================== 状态枚举 ====================class AlarmState(Enum):8NORMAL = "normal"9ALERTING = "alerting"10CONFIRMED = "confirmed"11CLEARED = "cleared"1213# ==================== 报警指示灯 ====================class AlarmIndicator(tk.Frame):14"""带完整状态机的报警指示灯"""1516COLORS = {17AlarmState.NORMAL: ("#00ff88"False),18AlarmState.ALERTING: ("#ff4444"True),19AlarmState.CONFIRMED: ("#ff8800"False),20AlarmState.CLEARED: ("#00ff88"False),21    }2223def __init__(self, parent, alarm_name, on_state_change=None, **kwargs):24super().__init__(parent, **kwargs)25        self.alarm_name = alarm_name26        self.state = AlarmState.NORMAL27        self._blink_job = None28        self._blink_visible = True29        self._clear_job = None30        self._on_state_change = on_state_change3132        self.config(bg="#1a1a1a", relief=tk.FLAT, height=45)33        self._create_widgets()3435def _create_widgets(self):36"""创建UI元件"""37# 状态指示灯(圆形)38        self.status_label = tk.Label(39            self, text="●", font=("Arial"16),40            fg="#00ff88", bg="#1a1a1a"41        )42        self.status_label.pack(side=tk.LEFT, padx=12, pady=8)4344# 文本信息45        self.text_label = tk.Label(46            self, text=f"{self.alarm_name} 正常",47            font=("微软雅黑"10), fg="#00ff88", bg="#1a1a1a",48            justify=tk.LEFT, anchor="w"49        )50        self.text_label.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=5)5152# 确认按钮(初始隐藏)53        self.confirm_btn = tk.Button(54            self, text="[确认]", font=("微软雅黑"9),55            bg="#ff4444", fg="white", relief=tk.FLAT,56            padx=8, pady=4,57            command=self._on_confirm58        )59        self.confirm_btn.pack(side=tk.RIGHT, padx=10)60        self.confirm_btn.pack_forget()6162        self.bind("<Button-1>", self._on_confirm)63        self.text_label.bind("<Button-1>", self._on_confirm)6465def update_value(self, value, threshold_high, threshold_low=None):66"""更新数值,驱动状态转换"""67        over = value > threshold_high68        under = (threshold_low is not Noneand (value < threshold_low)69        in_alarm = over or under7071if in_alarm and self.state == AlarmState.NORMAL:72            self._set_state(AlarmState.ALERTING)73elif not in_alarm and self.state in (AlarmState.ALERTINGAlarmState.CONFIRMED):74            self._set_state(AlarmState.CLEARED)75            self._schedule_clear_reset()7677def _on_confirm(self, event=None):78if self.state == AlarmState.ALERTING:79            self._set_state(AlarmState.CONFIRMED)8081def _set_state(self, new_state):82if self._blink_job:83            self.after_cancel(self._blink_job)84            self._blink_job = None8586        old_state = self.state87        self.state = new_state88        self._render()8990if self._on_state_change:91            self._on_state_change(self.alarm_name, old_state, new_state)9293        color, need_blink = self.COLORS[new_state]94if need_blink:95            self._blink(color)9697def _blink(self, color):98        self._blink_visible = not self._blink_visible99        current_color = color if self._blink_visible else "#333333"100        self.status_label.config(fg=current_color)101        self.text_label.config(fg=current_color)102        self._blink_job = self.after(500lambda: self._blink(color))103104def _schedule_clear_reset(self):105if self._clear_job:106            self.after_cancel(self._clear_job)107        self._clear_job = self.after(1000, self._auto_reset)108109def _auto_reset(self):110        self._clear_job = None111if self.state == AlarmState.CLEARED:112            self._set_state(AlarmState.NORMAL)113114def _render(self):115        color, _ = self.COLORS[self.state]116117        state_icons = {118AlarmState.NORMAL"●",119AlarmState.ALERTING"⚠",120AlarmState.CONFIRMED"▲",121AlarmState.CLEARED"✓",122        }123124        state_texts = {125AlarmState.NORMALf"{self.alarm_name} 正常",126AlarmState.ALERTINGf"{self.alarm_name} 超限!",127AlarmState.CONFIRMEDf"{self.alarm_name} 已确认",128AlarmState.CLEAREDf"{self.alarm_name} 已恢复",129        }130131        self.status_label.config(text=state_icons[self.state], fg=color)132        self.text_label.config(text=state_texts[self.state], fg=color)133134if self.state == AlarmState.ALERTING:135            self.confirm_btn.pack(side=tk.RIGHT, padx=10)136else:137            self.confirm_btn.pack_forget()138139        self.config(bg="#1a1a1a", highlightthickness=1,140                    highlightbackground=color, highlightcolor=color)141142def get_state(self):143return self.state144145def force_reset(self):146if self._blink_job:147            self.after_cancel(self._blink_job)148            self._blink_job = None149if self._clear_job:150            self.after_cancel(self._clear_job)151            self._clear_job = None152        self._set_state(AlarmState.NORMAL)153154def destroy(self):155if self._blink_job:156            self.after_cancel(self._blink_job)157if self._clear_job:158            self.after_cancel(self._clear_job)159super().destroy()160161162# ==================== 圆形仪表盘 ====================class CircleGauge(tk.Canvas):163"""增强版圆形仪表盘"""164165def __init__(self, parent, width=200, height=200, sensor_name="", **kwargs):166super().__init__(parent, width=width, height=height,167                         bg="#1a1a1a", highlightthickness=0, **kwargs)168        self.width = width169        self.height = height170        self.center_x = width / 2171        self.center_y = height / 2172        self.radius = min(width, height) / 2 - 15173        self.sensor_name = sensor_name174        self._draw_gauge()175176def _draw_gauge(self):177"""绘制仪表盘"""178# 外圆背景179        self.create_oval(180            self.center_x - self.radius,181            self.center_y - self.radius,182            self.center_x + self.radius,183            self.center_y + self.radius,184            outline="#333333", width=3, fill="#0a0a0a"185        )186187# 刻度区间颜色(绿→黄→红)188        steps = 36189for i in range(steps):190            angle1 = math.pi * (1 + i / steps)191192            x1_inner = self.center_x + (self.radius - 18) * math.cos(angle1)193            y1_inner = self.center_y + (self.radius - 18) * math.sin(angle1)194            x1_outer = self.center_x + self.radius * math.cos(angle1)195            y1_outer = self.center_y + self.radius * math.sin(angle1)196197if i < 18:198                color = "#00ff88"199elif i < 27:200                color = "#ffaa00"201else:202                color = "#ff4444"203204            self.create_line(x1_inner, y1_inner, x1_outer, y1_outer,205                             fill=color, width=3)206207# 外圆边框208        self.create_oval(209            self.center_x - self.radius,210            self.center_y - self.radius,211            self.center_x + self.radius,212            self.center_y + self.radius,213            outline="#00ff88", width=2214        )215216def set_value(self, value, max_value=100):217"""设置指针"""218        self.delete("needle")219220        ratio = min(max(value / max_value, 0), 1)221        angle = math.pi * (1 + ratio)222223        needle_length = self.radius - 25224        x = self.center_x + needle_length * math.cos(angle)225        y = self.center_y + needle_length * math.sin(angle)226227# 绘制指针228        self.create_line(229            self.center_x, self.center_y, x, y,230            fill="#ffff00", width=3, tags="needle"231        )232233# 指针中心圆234        circle_radius = 6235        self.create_oval(236            self.center_x - circle_radius,237            self.center_y - circle_radius,238            self.center_x + circle_radius,239            self.center_y + circle_radius,240            fill="#ffff00", outline="#ffff00", tags="needle"241        )242243244# ==================== 主应用 ====================class AlarmMonitorApp(tk.Tk):245def __init__(self):246super().__init__()247        self.title("工业控制系统 - 报警监控面板")248        self.geometry("1400x850")249        self.config(bg="#0a0a0a")250251        self.sensor_data = {252"温度"50,253"压力"8.0,254"流量"300,255        }256        self.thresholds = {257"温度": {"high"100"low"10},258"压力": {"high"12.0"low"5.0},259"流量": {"high"500"low"50},260        }261262        self._create_widgets()263        self._start_data_updates()264265def _create_widgets(self):266"""创建界面"""267# ===== 标题栏 =====268        header_frame = tk.Frame(self, bg="#0a0a0a", height=60)269        header_frame.pack(fill=tk.X, padx=20, pady=(1510))270271        header_label = tk.Label(272            header_frame, text="🔧 工业控制系统 - 报警监控面板",273            font=("微软雅黑"20"bold"),274            fg="#00ff88", bg="#0a0a0a"275        )276        header_label.pack()277278# ===== 分隔线 =====279        sep1 = tk.Frame(self, bg="#00ff88", height=1)280        sep1.pack(fill=tk.X, padx=20)281282# ===== 主内容区 =====283        content_frame = tk.Frame(self, bg="#0a0a0a")284        content_frame.pack(fill=tk.BOTH, expand=True, padx=20, pady=15)285286# ========= 上部:仪表盘 + 报警指示灯 =========287        top_frame = tk.Frame(content_frame, bg="#0a0a0a")288        top_frame.pack(fill=tk.BOTH, expand=True)289290# 左侧:仪表盘(并排排列)291        gauge_frame = tk.Frame(top_frame, bg="#0a0a0a")292        gauge_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)293294# 温度仪表295        temp_box = tk.Frame(gauge_frame, bg="#1a1a1a", relief=tk.FLAT,296                           borderwidth=1, highlightthickness=1,297                           highlightbackground="#00ff88")298        temp_box.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=5)299300        temp_label = tk.Label(temp_box, text="温度 (°C)",301                             font=("微软雅黑"11"bold"),302                             fg="#00ff88", bg="#1a1a1a")303        temp_label.pack(pady=(80))304305        self.temp_gauge = CircleGauge(temp_box, width=180, height=180, sensor_name="温度")306        self.temp_gauge.pack(pady=5)307308        self.temp_value_label = tk.Label(temp_box, text="50°C",309                                         font=("Arial"16"bold"),310                                         fg="#00ff88", bg="#1a1a1a")311        self.temp_value_label.pack(pady=(08))312313# 压力仪表314        pressure_box = tk.Frame(gauge_frame, bg="#1a1a1a", relief=tk.FLAT,315                               borderwidth=1, highlightthickness=1,316                               highlightbackground="#00ff88")317        pressure_box.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=5)318319        pressure_label = tk.Label(pressure_box, text="压力 (MPa)",320                                 font=("微软雅黑"11"bold"),321                                 fg="#00ff88", bg="#1a1a1a")322        pressure_label.pack(pady=(80))323324        self.pressure_gauge = CircleGauge(pressure_box, width=180, height=180, sensor_name="压力")325        self.pressure_gauge.pack(pady=5)326327        self.pressure_value_label = tk.Label(pressure_box, text="8.0 MPa",328                                            font=("Arial"16"bold"),329                                            fg="#00ff88", bg="#1a1a1a")330        self.pressure_value_label.pack(pady=(08))331332# 流量仪表333        flow_box = tk.Frame(gauge_frame, bg="#1a1a1a", relief=tk.FLAT,334                           borderwidth=1, highlightthickness=1,335                           highlightbackground="#00ff88")336        flow_box.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=5)337338        flow_label = tk.Label(flow_box, text="流量 (L/min)",339                             font=("微软雅黑"11"bold"),340                             fg="#00ff88", bg="#1a1a1a")341        flow_label.pack(pady=(80))342343        self.flow_gauge = CircleGauge(flow_box, width=180, height=180, sensor_name="流量")344        self.flow_gauge.pack(pady=5)345346        self.flow_value_label = tk.Label(flow_box, text="300 L/min",347                                        font=("Arial"16"bold"),348                                        fg="#00ff88", bg="#1a1a1a")349        self.flow_value_label.pack(pady=(08))350351# 右侧:报警指示灯352        alarm_frame = tk.Frame(top_frame, bg="#0a0a0a")353        alarm_frame.pack(side=tk.RIGHT, fill=tk.BOTH, padx=(150))354355        alarm_title = tk.Label(alarm_frame, text="🚨 报警指示",356                              font=("微软雅黑"13"bold"),357                              fg="#ff4444", bg="#0a0a0a")358        alarm_title.pack(pady=10)359360        self.alarms = {}361for sensor_name in ["温度""压力""流量""电源"]:362            alarm = AlarmIndicator(alarm_frame, sensor_name,363                                  on_state_change=self._log_state)364            alarm.pack(fill=tk.X, pady=4)365            self.alarms[sensor_name] = alarm366367# ===== 分隔线 =====368        sep2 = tk.Frame(content_frame, bg="#00ff88", height=1)369        sep2.pack(fill=tk.X, pady=15)370371# ===== 中部:控制按钮 =====372        button_frame = tk.Frame(content_frame, bg="#0a0a0a")373        button_frame.pack(fill=tk.X, pady=10)374375        btn_style = {376"font": ("微软雅黑"10),377"relief": tk.FLAT,378"padx"16,379"pady"8,380"width"12,381        }382383        tk.Button(button_frame, text="◆ 温度超限",384                  command=lambda: self._trigger_alarm("温度"110),385                  bg="#ff4444", fg="white", **btn_style).pack(side=tk.LEFT, padx=5)386387        tk.Button(button_frame, text="◆ 压力超限",388                  command=lambda: self._trigger_alarm("压力"15.0),389                  bg="#ff4444", fg="white", **btn_style).pack(side=tk.LEFT, padx=5)390391        tk.Button(button_frame, text="◆ 流量异常",392                  command=lambda: self._trigger_alarm("流量"600),393                  bg="#ff4444", fg="white", **btn_style).pack(side=tk.LEFT, padx=5)394395        tk.Button(button_frame, text="✓ 清除告警",396                  command=self._clear_all_alarms,397                  bg="#00ff88", fg="#000000", **btn_style).pack(side=tk.LEFT, padx=5)398399        tk.Button(button_frame, text="⟳ 系统复位",400                  command=self._system_reset,401                  bg="#4488ff", fg="white", **btn_style).pack(side=tk.LEFT, padx=5)402403# ===== 分隔线 =====404        sep3 = tk.Frame(content_frame, bg="#00ff88", height=1)405        sep3.pack(fill=tk.X, pady=10)406407# ===== 下部:日志区域 =====408        log_frame = tk.Frame(content_frame, bg="#0a0a0a")409        log_frame.pack(fill=tk.BOTH, expand=True)410411        log_title = tk.Label(log_frame, text="📋 状态变化日志",412                            font=("微软雅黑"11"bold"),413                            fg="#00ff88", bg="#0a0a0a")414        log_title.pack(anchor="w", pady=(08))415416# 日志框架417        log_box = tk.Frame(log_frame, bg="#1a1a1a", relief=tk.FLAT,418                          borderwidth=1, highlightthickness=1,419                          highlightbackground="#00ff88")420        log_box.pack(fill=tk.BOTH, expand=True)421422        self.log_text = tk.Text(log_box, height=8, bg="#000000",423                               fg="#00ff88", font=("Courier New"9),424                               relief=tk.FLAT, insertbackground="#00ff88",425                               padx=10, pady=8)426        self.log_text.pack(fill=tk.BOTH, expand=True)427428# 滚动条429        scrollbar = tk.Scrollbar(log_box, command=self.log_text.yview,430                                bg="#333333", troughcolor="#0a0a0a")431        scrollbar.pack(side=tk.RIGHT, fill=tk.Y)432        self.log_text.config(yscrollcommand=scrollbar.set)433434        self._log_message("✓ 系统启动就绪")435436def _trigger_alarm(self, sensor_name, value):437        self.sensor_data[sensor_name] = value438        self._log_message(f"⚠ 手动触发 {sensor_name} 值为 {value}")439440def _clear_all_alarms(self):441for alarm in self.alarms.values():442            alarm.force_reset()443        self.sensor_data = {"温度"50"压力"8.0"流量"300}444        self._log_message("✓ 已清除所有告警")445446def _system_reset(self):447        self._clear_all_alarms()448        self._log_message("⟳ 系统已复位")449450def _log_state(self, alarm_name, old_state, new_state):451        self._log_message(f"{alarm_name}: {old_state.value} → {new_state.value}")452453def _log_message(self, message):454        time_str = datetime.now().strftime("%H:%M:%S")455        log_msg = f"[{time_str}] {message}\n"456        self.log_text.insert(tk.END, log_msg)457        self.log_text.see(tk.END)458459def _start_data_updates(self):460        self._update_gauges()461462def _update_gauges(self):463# 温度464        temp = self.sensor_data["温度"]465        self.temp_value_label.config(text=f"{temp}°C")466        self.temp_gauge.set_value(temp, max_value=120)467        t_thrs = self.thresholds["温度"]468        self.alarms["温度"].update_value(469            temp, threshold_high=t_thrs["high"], threshold_low=t_thrs["low"]470        )471# 压力472        pressure = self.sensor_data["压力"]473        self.pressure_value_label.config(text=f"{pressure:.1f} MPa")474        self.pressure_gauge.set_value(pressure, max_value=15)475        p_thrs = self.thresholds["压力"]476        self.alarms["压力"].update_value(477            pressure, threshold_high=p_thrs["high"], threshold_low=p_thrs["low"]478        )479# 流量480        flow = self.sensor_data["流量"]481        self.flow_value_label.config(text=f"{flow} L/min")482        self.flow_gauge.set_value(flow, max_value=600)483        f_thrs = self.thresholds["流量"]484        self.alarms["流量"].update_value(485            flow, threshold_high=f_thrs["high"], threshold_low=f_thrs["low"]486        )487        self.after(500, self._update_gauges)488489490if __name__ == "__main__":491    app = AlarmMonitorApp()492    app.mainloop()

真实业务影响:没有"确认"机制的报警系统,操作员往往会直接忽略持续闪烁的警告。加上状态机之后,每一条报警都有明确的"处置记录",审计和追责都有据可查。


📊 性能数据对比

基于实际项目测试环境(Windows 7,赛扬J1900,4GB内存):

方案
内存占用
启动时间
1小时后响应延迟
错误写法(主线程IO+Canvas堆积)
240MB+
2.1s
800ms+
本文方案(多线程+item复用)
82MB
1.1s
<50ms

差距不是一点点。82MB对240MB,在4GB的工控机上意味着系统还有足够余量跑其他服务。


💬 互动话题

话题一:你在工业或嵌入式项目里用过哪些Python GUI方案?Tkinter、wxPython、PyQt还是别的?踩过什么坑,欢迎评论区分享。

话题二:如果要在这套HMI里加入历史曲线回放功能(从SQLite读取历史数据,Canvas绘制折线图),你会怎么设计?这是个很好的进阶练习题,有想法的同学可以留言讨论。


🎯 三句话总结

多线程是命,队列是桥,Canvas item复用是钱。 这三件事做对了,Tkinter在工业场景里完全够用。

报警系统没有状态机,就是在给自己埋雷。 超限-确认-恢复,三个状态缺一不可。

工控机资源有限,每一行代码都在和内存抢地盘。 轻量不是妥协,是专业。


📚 学习路线图

掌握本文内容之后,建议按这个方向继续深入:

  • • 串口通信pyserial + Modbus RTU协议解析(pymodbus库)
  • • 数据持久化:SQLite存储历史数据 + matplotlib绘制历史曲线
  • • 网络化扩展:OPC-UA协议(opcua库),对接西门子/三菱PLC
  • • 打包部署PyInstaller单文件打包,解决工控机无Python环境问题
  • • 界面美化ttkthemes + 自定义ttk样式,摆脱Tkinter默认土味

如果这篇文章帮你解决了实际问题,转发给同样在做工业项目的朋友——他们大概率也正在被Tkinter卡界面的问题折磨。


#Python开发#工业HMI#Tkinter实战#编程技巧#性能优化

最新文章

随机文章

基本 文件 流程 错误 SQL 调试
  1. 请求信息 : 2026-07-04 08:12:39 HTTP/2.0 GET : https://f.mffb.com.cn/a/490250.html
  2. 运行时间 : 0.222657s [ 吞吐率:4.49req/s ] 内存消耗:4,753.64kb 文件加载:140
  3. 缓存信息 : 0 reads,0 writes
  4. 会话信息 : SESSION_ID=e453e43dfa8aa12a45d6bea506d5f24b
  1. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/public/index.php ( 0.79 KB )
  2. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/autoload.php ( 0.17 KB )
  3. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/composer/autoload_real.php ( 2.49 KB )
  4. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/composer/platform_check.php ( 0.90 KB )
  5. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/composer/ClassLoader.php ( 14.03 KB )
  6. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/composer/autoload_static.php ( 4.90 KB )
  7. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-helper/src/helper.php ( 8.34 KB )
  8. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-validate/src/helper.php ( 2.19 KB )
  9. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/helper.php ( 1.47 KB )
  10. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/stubs/load_stubs.php ( 0.16 KB )
  11. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Exception.php ( 1.69 KB )
  12. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-container/src/Facade.php ( 2.71 KB )
  13. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/symfony/deprecation-contracts/function.php ( 0.99 KB )
  14. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/symfony/polyfill-mbstring/bootstrap.php ( 8.26 KB )
  15. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/symfony/polyfill-mbstring/bootstrap80.php ( 9.78 KB )
  16. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/symfony/var-dumper/Resources/functions/dump.php ( 1.49 KB )
  17. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-dumper/src/helper.php ( 0.18 KB )
  18. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/symfony/var-dumper/VarDumper.php ( 4.30 KB )
  19. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/App.php ( 15.30 KB )
  20. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-container/src/Container.php ( 15.76 KB )
  21. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/psr/container/src/ContainerInterface.php ( 1.02 KB )
  22. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/provider.php ( 0.19 KB )
  23. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Http.php ( 6.04 KB )
  24. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-helper/src/helper/Str.php ( 7.29 KB )
  25. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Env.php ( 4.68 KB )
  26. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/common.php ( 0.03 KB )
  27. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/helper.php ( 18.78 KB )
  28. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Config.php ( 5.54 KB )
  29. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/app.php ( 0.95 KB )
  30. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/cache.php ( 0.78 KB )
  31. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/console.php ( 0.23 KB )
  32. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/cookie.php ( 0.56 KB )
  33. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/database.php ( 2.48 KB )
  34. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/facade/Env.php ( 1.67 KB )
  35. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/filesystem.php ( 0.61 KB )
  36. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/lang.php ( 0.91 KB )
  37. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/log.php ( 1.35 KB )
  38. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/middleware.php ( 0.19 KB )
  39. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/route.php ( 1.89 KB )
  40. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/session.php ( 0.57 KB )
  41. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/trace.php ( 0.34 KB )
  42. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/view.php ( 0.82 KB )
  43. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/event.php ( 0.25 KB )
  44. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Event.php ( 7.67 KB )
  45. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/service.php ( 0.13 KB )
  46. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/AppService.php ( 0.26 KB )
  47. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Service.php ( 1.64 KB )
  48. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Lang.php ( 7.35 KB )
  49. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/lang/zh-cn.php ( 13.70 KB )
  50. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/initializer/Error.php ( 3.31 KB )
  51. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/initializer/RegisterService.php ( 1.33 KB )
  52. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/services.php ( 0.14 KB )
  53. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/service/PaginatorService.php ( 1.52 KB )
  54. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/service/ValidateService.php ( 0.99 KB )
  55. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/service/ModelService.php ( 2.04 KB )
  56. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-trace/src/Service.php ( 0.77 KB )
  57. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Middleware.php ( 6.72 KB )
  58. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/initializer/BootService.php ( 0.77 KB )
  59. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/Paginator.php ( 11.86 KB )
  60. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-validate/src/Validate.php ( 63.20 KB )
  61. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/Model.php ( 23.55 KB )
  62. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/concern/Attribute.php ( 21.05 KB )
  63. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/concern/AutoWriteData.php ( 4.21 KB )
  64. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/concern/Conversion.php ( 6.44 KB )
  65. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/concern/DbConnect.php ( 5.16 KB )
  66. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/concern/ModelEvent.php ( 2.33 KB )
  67. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/concern/RelationShip.php ( 28.29 KB )
  68. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-helper/src/contract/Arrayable.php ( 0.09 KB )
  69. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-helper/src/contract/Jsonable.php ( 0.13 KB )
  70. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/contract/Modelable.php ( 0.09 KB )
  71. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Db.php ( 2.88 KB )
  72. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/DbManager.php ( 8.52 KB )
  73. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Log.php ( 6.28 KB )
  74. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Manager.php ( 3.92 KB )
  75. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/psr/log/src/LoggerTrait.php ( 2.69 KB )
  76. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/psr/log/src/LoggerInterface.php ( 2.71 KB )
  77. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Cache.php ( 4.92 KB )
  78. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/psr/simple-cache/src/CacheInterface.php ( 4.71 KB )
  79. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-helper/src/helper/Arr.php ( 16.63 KB )
  80. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/cache/driver/File.php ( 7.84 KB )
  81. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/cache/Driver.php ( 9.03 KB )
  82. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/contract/CacheHandlerInterface.php ( 1.99 KB )
  83. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/Request.php ( 0.09 KB )
  84. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Request.php ( 55.78 KB )
  85. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/middleware.php ( 0.25 KB )
  86. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Pipeline.php ( 2.61 KB )
  87. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-trace/src/TraceDebug.php ( 3.40 KB )
  88. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/middleware/SessionInit.php ( 1.94 KB )
  89. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Session.php ( 1.80 KB )
  90. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/session/driver/File.php ( 6.27 KB )
  91. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/contract/SessionHandlerInterface.php ( 0.87 KB )
  92. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/session/Store.php ( 7.12 KB )
  93. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Route.php ( 23.73 KB )
  94. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/RuleName.php ( 5.75 KB )
  95. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/Domain.php ( 2.53 KB )
  96. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/RuleGroup.php ( 22.43 KB )
  97. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/Rule.php ( 26.95 KB )
  98. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/RuleItem.php ( 9.78 KB )
  99. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/route/app.php ( 1.72 KB )
  100. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/facade/Route.php ( 4.70 KB )
  101. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/dispatch/Controller.php ( 4.74 KB )
  102. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/Dispatch.php ( 10.44 KB )
  103. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/controller/Index.php ( 4.81 KB )
  104. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/BaseController.php ( 2.05 KB )
  105. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/facade/Db.php ( 0.93 KB )
  106. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/connector/Mysql.php ( 5.44 KB )
  107. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/PDOConnection.php ( 52.47 KB )
  108. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/Connection.php ( 8.39 KB )
  109. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/ConnectionInterface.php ( 4.57 KB )
  110. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/builder/Mysql.php ( 16.58 KB )
  111. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/Builder.php ( 24.06 KB )
  112. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/BaseBuilder.php ( 27.50 KB )
  113. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/Query.php ( 15.71 KB )
  114. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/BaseQuery.php ( 45.13 KB )
  115. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/TimeFieldQuery.php ( 7.43 KB )
  116. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/AggregateQuery.php ( 3.26 KB )
  117. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/ModelRelationQuery.php ( 20.07 KB )
  118. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/ParamsBind.php ( 3.66 KB )
  119. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/ResultOperation.php ( 7.01 KB )
  120. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/WhereQuery.php ( 19.37 KB )
  121. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/JoinAndViewQuery.php ( 7.11 KB )
  122. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/TableFieldInfo.php ( 2.63 KB )
  123. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/Transaction.php ( 2.77 KB )
  124. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/log/driver/File.php ( 5.96 KB )
  125. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/contract/LogHandlerInterface.php ( 0.86 KB )
  126. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/log/Channel.php ( 3.89 KB )
  127. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/event/LogRecord.php ( 1.02 KB )
  128. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-helper/src/Collection.php ( 16.47 KB )
  129. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/facade/View.php ( 1.70 KB )
  130. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/View.php ( 4.39 KB )
  131. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Response.php ( 8.81 KB )
  132. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/response/View.php ( 3.29 KB )
  133. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Cookie.php ( 6.06 KB )
  134. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-view/src/Think.php ( 8.38 KB )
  135. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/contract/TemplateHandlerInterface.php ( 1.60 KB )
  136. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-template/src/Template.php ( 46.61 KB )
  137. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-template/src/template/driver/File.php ( 2.41 KB )
  138. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-template/src/template/contract/DriverInterface.php ( 0.86 KB )
  139. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/runtime/temp/067d451b9a0c665040f3f1bdd3293d68.php ( 11.98 KB )
  140. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-trace/src/Html.php ( 4.42 KB )
  1. CONNECT:[ UseTime:0.000388s ] mysql:host=127.0.0.1;port=3306;dbname=f_mffb;charset=utf8mb4
  2. SHOW FULL COLUMNS FROM `fenlei` [ RunTime:0.000544s ]
  3. SELECT * FROM `fenlei` WHERE `fid` = 0 [ RunTime:0.007025s ]
  4. SELECT * FROM `fenlei` WHERE `fid` = 63 [ RunTime:0.000285s ]
  5. SHOW FULL COLUMNS FROM `set` [ RunTime:0.000500s ]
  6. SELECT * FROM `set` [ RunTime:0.001016s ]
  7. SHOW FULL COLUMNS FROM `article` [ RunTime:0.000626s ]
  8. SELECT * FROM `article` WHERE `id` = 490250 LIMIT 1 [ RunTime:0.007127s ]
  9. UPDATE `article` SET `lasttime` = 1783123959 WHERE `id` = 490250 [ RunTime:0.000657s ]
  10. SELECT * FROM `fenlei` WHERE `id` = 66 LIMIT 1 [ RunTime:0.000340s ]
  11. SELECT * FROM `article` WHERE `id` < 490250 ORDER BY `id` DESC LIMIT 1 [ RunTime:0.016752s ]
  12. SELECT * FROM `article` WHERE `id` > 490250 ORDER BY `id` ASC LIMIT 1 [ RunTime:0.000961s ]
  13. SELECT * FROM `article` WHERE `id` < 490250 ORDER BY `id` DESC LIMIT 10 [ RunTime:0.020362s ]
  14. SELECT * FROM `article` WHERE `id` < 490250 ORDER BY `id` DESC LIMIT 10,10 [ RunTime:0.043674s ]
  15. SELECT * FROM `article` WHERE `id` < 490250 ORDER BY `id` DESC LIMIT 20,10 [ RunTime:0.039303s ]
0.225228s