news 2026/10/17 2:54:16

泰克示波器自动化测试:PyQt5与VISA/SCPI工程实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
泰克示波器自动化测试:PyQt5与VISA/SCPI工程实战

简介:基于Python与PyQt5开发的泰克示波器自动化测试工程,面向需要对接MDO3000系列示波器的硬件测试或上位机开发人员,用于解决仪器数据实时显示、图像保存及日志监控等重复性工作。工程内包含主要程序文件、界面设计文件、位图资源、日志样本与工程配置信息,共16个文件,压缩包约139KB,结构紧凑,便于快速定位源码、UI布局和演示素材。其中GUI_Main.py和MainWindow.py演示了PyQt5中常用控件(如按钮、表格、日志窗口)的调用方式,配合MainWindow.ui可直接查看界面设计,log相关代码实现了图像与日志信息实时刷新、窗口内容保存,测试用例支持单次循环执行,具体用例逻辑可由使用者自行扩展。整体兼顾示例性与实用性,已有3657人学习下载,适合具备一定Python基础、希望快速上手PyQt5与仪器自动化控制的开发者参考。

1. 泰克示波器自动化测试:为什么我放弃“手动截屏”转投 PyQt5

做电子测量的人都有过这种经历:被测板子上电后,要在示波器上反复调节时基、触发电平,盯着屏幕上的波形判断是否正确,再把合格波形一张张截图存档。如果被测点是 8 路甚至 16 路,一次测试流程下来光截屏和整理数据就要花掉大半天。我一开始也觉得“示波器截图能有多费事”,直到某次评审会上发现 30 张波形图里有 5 张的时基设置不一致,数据根本没法横向对比,才意识到手动操作不只是费时间,更是测试结果失真的大坑。

这就是 PyQt5 + Python + Tektronix 3000 系列示波器自动化测试要解决的问题:把示波器的实时波形图片、测量参数、打印信息统一拉回到上位机界面里,按测试用例自动完成激励、采集、比对和存档。它面向的不是科研场景里调一次波形,而是产线验证、硬件测试、研发自测这类需要反复执行的场景。做完这套东西之后,测试时间能从小时级压缩到分钟级,而且每次测试的示波器参数都会固化在配置文件里,再也不存在“上次时基调了多少”这种玄学问题。下面我直接按这套资源里最核心的实现路径拆开讲。

2. Tektronix 3000 的通信链路:VISA 与 SCPI 的选型与连接实操

2.1 为什么选 VISA 而不是直接走 Socket

泰克 3000 系列示波器(Tektronix 3000 泛指 TDS3000、MSO3000 等常用型号)支持多种程控方式,包括 GPIB、RS-232、LAN 和 USB,但应用层统一走 SCPI(Standard Commands for Programmable Instruments)指令集。常见的做法是用 VISA(Virtual Instrument Software Architecture)做中间层,因为 VISA 把 GPIB、串口、网口全部封装成了统一的资源字符串(VISA Resource String),我在代码里写连接逻辑时不用关心物理链路是网线还是 USB。

有人会问:既然示波器支持 LAN 口,为什么不直接用 socket 发 SCPI 命令?我自己试过,socket 直连 TDS3000 的问题在于:示波器固件对 TCP 连接有会话限制,如果上位机在测试中途异常退出,socket 没有正常关闭,示波器端会保持半开连接,重连时就容易出现VI_ERROR_RSRC_BUSY。VISA 库自带资源管理机制,在连接释放和会话清理上比裸 socket 稳妥得多。实测在产线环境下连续跑 200 次连接/断开,VISA 方式没有出现资源泄漏。

2.2 建立连接:地址探测与超时参数

先安装 Tektronix 提供的 VISA 运行时(常见的是 TekVISA 或 NI-VISA,两者接口兼容)。连接的第一步是枚举设备,代码里用pyvisa库最顺手,它是 National Instruments VISA 的 Python 绑定,泰克自己的 PyVISA 也是基于这层封装。

import pyvisa rm = pyvisa.ResourceManager() resources = rm.list_resources() print("VISA 资源列表:", resources) # 如果资源列表为空,先检查 TekVISA 服务是否启动 # 常见资源字符串示例: # TCPIP0::192.168.1.100::inst0::INSTR # USB0::0x0699::0x0368::C012345::INSTR instr = rm.open_resource('TCPIP0::192.168.1.100::inst0::INSTR') instr.timeout = 3000 # 毫秒 instr.encoding = 'latin-1' # 验证连接 idn = instr.query('*IDN?') print("示波器响应:", idn)

这段代码里有个关键点:encoding必须设置。泰克 3000 系列在返回 SCPI 响应时默认编码不是 UTF-8,如果用默认编码解析带单位符号的字符串(比如mV、kHz),偶尔会出现 UnicodeDecodeError。timeout参数我习惯设 3000 毫秒,因为示波器在做自动测量时如果触发电平设置不合理,响应会变慢,超时太短会导致误报。连接成功后,用*IDN?验证身份是通用习惯,返回的字符串一般包含厂商、型号、序列号和固件版本,我拿到后会做一次字符串比较,确保连接的是预期的 3000 系列而非其他型号。

2.3 读取实时波形:从 CURVE? 到 numpy 数组

拿到连接句柄之后的下一件事,是把波形数据从示波器里拉回来。TDS3000 和 MSO3000 的波形读取用的都是CURVE?命令,但在读取之前必须先设置要读取的通道和传输格式,否则返回的数据可能不是你想要的通道。

# 读取 CH1 的完整波形数据 instr.write('DATA:SOURce CH1') instr.write('DATA:WIDth 1') # 1 字节宽度,速度快 instr.write('DATA:ENCdg RIBinary') # 有符号整数,高位在前 # 获取水平参数,用于后续坐标映射 x_inc = float(instr.query('WFMPre:XINcr?')) x_zero = float(instr.query('WFMPre:XZEro?')) y_mult = float(instr.query('WFMPre:YMULT?')) y_zero = float(instr.query('WFMPre:YZEro?')) # 读波形 raw_data = instr.query_binary_values('CURVE?', datatype='b', container=list) print(f"采样点数: {len(raw_data)}")

这段代码有几个值得注意的细节。DATA:WIDth 1表示每个采样点占 1 字节,前提是示波器当前的分辨率不超过 8 位,对于 3000 系列的 8 位 ADC 来说够用;如果做高精度测量,改成DATA:WIDth 2并配合datatype='h'即可,代价是传输时间翻倍。RIBinary是有符号整数的二进制传输格式,相比 ASCII 格式传输效率高很多,一次CURVE?返回 10000 个采样点用时不到 100 毫秒。

拿到原始数据后,还要把 ADC 码值换算成实际的电压值。公式是y_actual = (raw_data - y_zero) * y_mult,而时间轴则是x_actual = x_zero + i * x_inc。我通常直接转成 numpy 数组做运算,因为后续无论是画波形还是算峰峰值都离不开向量化计算。

2.4 配置界面:PyQt5 里的定时刷新架构

波形数据从示波器透过 VISA 到了 Python,接着就是 PyQt5 的活。最忌讳的做法是在 UI 线程里直接同步读示波器,因为CURVE?即使再快也有几十毫秒的阻塞,界面会出现明显卡顿。这套资源里的做法是典型的「工作线程 + 信号槽」架构:示波器读取放在 QThread 里,拿到波形数组后用 PyQt5 的 Signal 发给主线程,主线程只负责刷新 QLabel 上的图片或 QTableWidget 里的测量数据。

import sys import numpy as np from PyQt5.QtCore import QThread, pyqtSignal, QTimer from PyQt5.QtWidgets import QApplication, QLabel, QVBoxLayout, QWidget class ScopeWorker(QThread): wave_ready = pyqtSignal(object) # 波形数组 info_ready = pyqtSignal(str) # 打印信息 def __init__(self, instr): super().__init__() self.instr = instr self.running = True def run(self): while self.running: # 读取波形并解析 data = self.read_waveform() self.wave_ready.emit(data) # 读取测到的频率和幅值 info = self.instr.query('MEASUREMENT:MEAS1:VALUE?') self.info_ready.emit(f"CH1 频率: {info.strip()} Hz") self.msleep(50) # 20FPS 刷新 def read_waveform(self): # 省略 CURVE 读取细节,参考上文代码 pass class MainWindow(QWidget): def __init__(self): super().__init__() self.label = QLabel("等待波形...") lay = QVBoxLayout() lay.addWidget(self.label) self.setLayout(lay) def update_wave(self, data): # 把数组转成 QPixmap 再刷新 pass app = QApplication(sys.argv) win = MainWindow() win.show() sys.exit(app.exec_())

这里特别说明一下刷新频率的设计。msleep(50)对应 20 FPS 的刷新率,实际使用中我发现在 3000 系列上CURVE?一次约 50~80 毫秒,加上数据解析,实际帧率只能到 10FPS 左右。如果追求更高刷新率,可以打开示波器的 FastFrame 模式或缩短采集长度。对于大多数测试场景,10FPS 已经足够“实时”:屏幕上看起来就是示波器画面在动态更新,没有明显的“一帧一帧跳变”感。

3. PyQt5 界面与数据的交互设计:信号槽、线程与刷新时序

3.1 为什么把界面刷新和仪器交互分开

刚写这个项目的时候,我在一个新手常见的坑里卡了两天:直接在QTimer的timeout回调里调用instr.query(),结果界面不仅卡顿,还出现了“程序未响应”的假死状态。这是因为query()在等示波器返回时是阻塞的,而 Qt 的 UI 事件循环也被这个阻塞卡住,界面消息无法得到处理。实际上示波器的响应时间不是固定的,触发模式、测量项数量、波形长度都会影响响应时长,极端情况下能达到秒级。

正确的解法是把仪器交互丢给 QThread,线程内部跑一个while True循环,不断执行「读取波形 → 发射信号 → 休眠」三步。PyQt5 的信号槽机制会把wave_ready信号投递到主线程的事件循环中,主线程拾取信号后执行update_wave滑润刷新界面。这样即使示波器响应慢,卡住的只是工作线程,界面依然能响应用户的按钮点击。

3.2 打印信息的实时刷新:从串口控制台到 UI 的日志窗口

示波器只有一个屏幕,但自动化测试场景里除了波形,还有测到的频率、峰峰值、上升沿时间等批量测量结果。这些信息如果只是在 Python 控制台里 print,测完一排数据根本没法回看;如果写文件,又不能实时观察。这套资源里用的是 QPlainTextEdit 作为滚动日志窗口,每收到一条新数据就 append 一行,日志窗口自动滚动到底部。

from PyQt5.QtWidgets import QPlainTextEdit class LogPanel(QPlainTextEdit): MAX_BLOCKS = 500 # 限制最大行数,防止内存增长 def append_log(self, text): self.appendPlainText(text) # 控制行数 doc = self.document() if doc.blockCount() > self.MAX_BLOCKS: cursor = self.textCursor() cursor.movePosition(cursor.Start) cursor.select(cursor.LineUnderCursor) cursor.removeSelectedText() cursor.deleteChar() # 滚动到底部 sb = self.verticalScrollBar() sb.setValue(sb.maximum())

日志窗口里的每一行我都会加上时间戳前缀,格式是[HH:MM:SS.mmm] 消息内容,因为产线测试经常需要按时间回溯故障现场。加时间戳有个细节:不要在收到信号时才去调用datetime.now(),那反应的是界面处理信号的时间,和示波器实际采集时间会差几百毫秒;更可靠的做法是在工作线程读出数据的那一刻就打上时间戳,把(timestamp, message)作为一个元组通过信号传出来。

3.3 截图保存:波形图自动按用例命名归档

测试完成之后,波形数据不能看完就扔。最常见的归档需求是把当前波形保存为 PNG 图片,命名规则包含测试用例编号和实际测量值。在这个项目里,我用QPixmap.grab()抓取绘制波形图片,然后保存到一个以日期分层的目录结构里。

import os from datetime import datetime def save_wave_image(self, case_id, value): date_str = datetime.now().strftime("%Y%m%d") hour_str = datetime.now().strftime("%H%M%S") folder = os.path.join("reports", date_str, case_id) os.makedirs(folder, exist_ok=True) filename = f"{hour_str}_{value:.3f}V.png" filepath = os.path.join(folder, filename) self.label.pixmap().save(filepath, "PNG") self.log_panel.append_log(f"已保存: {filepath}")

这里有个版本兼容问题:QLabel.pixmap()在 PyQt6 里改成了pixmap()方法返回QPixmap的拷贝,在 PyQt5 里同样可用,但如果你用的是 PySide2/PySide6,接口略有差别。这套资源基于 PyQt5,所以直接用label.pixmap()就行。保存图片这一步虽然看起来简单,但容易被忽略的是:如果波形还没绘制完成(第一帧还没到)就去保存,保存下来的是一张空白占位图。我一般会在工作线程发出first_frame_ready信号后才启用保存按钮。

4. 自动化测试时序:激励、采集、比对与报表输出

4.1 测试流程的状态机设计

自动化测试不能只是“把示波器画面搬到电脑上”,核心价值在于可编排的测试流程。以电源板纹波测试为例,标准流程是:上位机设置示波器通道、自动设置触发、等待波形稳定、读取峰峰值、与规格值比对、生成报告。这个流程里有明确的时序依赖,上一动作完成才能开始下一个动作,所以用简单的线性顺序执行不够健壮——如果第 2 步因为信号没接好导致示波器一直没有触发,第 3 步就会卡死等待。

推荐的做法是写一个轻量级状态机。这里给出一个实用的骨架:

class TestState: IDLE = 0 CONFIG = 1 WAIT_TRIGGER = 2 ACQUIRE = 3 COMPARE = 4 REPORT = 5 ERROR = 99 class AutoTestRunner(QThread): state_changed = pyqtSignal(int, str) def __init__(self, instr, spec: dict): super().__init__() self.instr = instr self.spec = spec self.state = TestState.IDLE def run(self): self.state = TestState.CONFIG self.state_changed.emit(self.state, "配置示波器") self.configure_scope() self.state = TestState.WAIT_TRIGGER self.state_changed.emit(self.state, "等待信号触发") if not self.wait_for_trigger(timeout=5000): self.state = TestState.ERROR self.state_changed.emit(self.state, "触发超时") return self.state = TestState.ACQUIRE self.state_changed.emit(self.state, "采集 100 帧波形") waveforms = self.acquire_frames(100) self.state = TestState.COMPARE self.state_changed.emit(self.state, "计算峰峰值并比对") pv = self.compute_peak_to_peak(waveforms) result = "PASS" if pv < self.spec["max_ripple"] else "FAIL" self.state = TestState.REPORT self.state_changed.emit(self.state, f"生成报告: {result}") self.generate_report(result, pv)

这套状态机的好处是每一步都能发信号通知界面更新进度,状态卡住时也能明确知道卡在哪一步。wait_for_trigger的实现我一般用TRIGger:STATUS?查询,而不是傻等一定时间。3000 系列支持返回TRIGGER或READY等状态,轮询间隔 100 毫秒,超时 5 秒就判定异常。

4.2 批量测量参数配置与固化

测试要可重复,示波器的设置必须可固化。在这个项目的配置模块里,所有示波器参数以 YAML 文件保存,包括通道开启状态、垂直刻度、水平时基、触发电平、测量项列表。

channel: CH1: enabled: true scale: 0.5 # V/div position: -2 # div CH2: enabled: false scale: 0.1 position: 0 horizontal: scale: 0.00001 # s/div,对应 10us position: 5 trigger: source: CH1 level: 0.1 # V slope: RISING measurements: - PK2Pk - FREQuency - RISeTime

应用这份配置到示波器的代码本质是把 YAML 逐项翻译成 SCPI 写入命令。CH1:SCAle 0.5、HORizontal:MAIn:SCAle 1e-5、TRIGger:MAIn:LEVel 0.1等等。这里要提醒一个 3000 系列的特殊性:水平时基的写法和垂直不一样,水平分成HORizontal:MAIn和HORizontal:FAST两个子空间,普通采集用MAIn,FastAcq 模式才涉及FAST,配置时如果不带MAIn直接写HORIZONTAL:SCALE,部分固件版本会报错。

4.3 报告生成:CSV + HTML 双份输出

测试记录不能只留在上位机界面上,对工程师来说,一份可追溯的测试报告比一堆截图要有用得多。这个项目里报告输出分两路:一路是 CSV,方便用 Excel 打开做二次统计分析;另一路是 HTML,可以直接打印或者归档到内部系统。

import csv import html rows = [ {"case_id": "PWR-001", "ripple_mv": 18.5, "result": "PASS"}, {"case_id": "PWR-002", "ripple_mv": 35.2, "result": "FAIL"}, ] csv_path = "reports/20240520_ripple_test.csv" with open(csv_path, "w", newline="") as f: writer = csv.DictWriter(f, fieldnames=["case_id", "ripple_mv", "result"]) writer.writeheader() writer.writerows(rows) html_path = "reports/20240520_ripple_test.html" tbl = "<table border=1><tr><th>用例</th><th>纹波</th><th>结果</th></tr>" for r in rows: tbl += f"<tr><td>{r['case_id']}</td><td>{r['ripple_mv']}</td>" tbl += f"<td>{html.escape(r['result'])}</td></tr>" tbl += "</table>" with open(html_path, "w") as f: f.write(f"<html><body><h2>纹波测试报告</h2>{tbl}</body></html>")

这里代码本身不难,但有一个工程细节:CSV 写成 UTF-8 格式时,如果测试用例编号里包含中文(比如“电源模块-001”),用 Excel 默认打开会乱码,因为 Excel 默认按 ANSI 解析。解决方法是写 CSV 时用utf-8-sig编码,具体做法是在open()时指定encoding="utf-8-sig"。这个小坑我同事踩过一次,后来我把这个细节写进了配置说明文档。

5. 泰克 3000 自动化测试避坑指南:五个真实翻车记录

5.1 示波器连不上:IP 地址变了但程序还在用缓存

现象:资源列表里能看到示波器,但open_resource一直报超时,程序重启后又能连上。

原因:泰克 3000 在 DHCP 环境下 IP 会变化,而 VISA 的资源管理器在进程内会缓存设备描述符。第一次连接成功后,如果示波器断电重连或网络重新分配了 IP,旧的 resource string 可能指向失效的地址。

解决:不要在程序启动时一次性打开资源然后永久持有。每次测试序列开始前重新调用list_resources()并匹配型号名称,连接失败时自动重试。如果示波器是固定 IP,直接在路由上做 MAC 绑定,这是产线最省事的方式。

5.2 波形数据全是同一数值:DATA:SOURce 没有重设

现象:通道 CH1 有正常波形,但读取CURVE?返回的数据全是 128 附近的一个死数,而且点数也异常。

原因:上一个测试用例设置了DATA:SOURce CH2,CH2 没接信号,采样出来就是基线电平。重新连接示波器或者切换用例后,没有把数据源重新指向 CH1,示波器只是忠实地返回了 CH2 的数据。

解决:在每次采集前强制写一遍DATA:SOURce CH1,不要依赖默认状态。同理,DATA:WIDth和DATA:ENCdg也需要在采集前重写,因为其他代码块可能改过这些参数。

5.3 界面假死:阻塞查询放进了 UI 线程

现象:连接示波器后,PyQt5 窗口拖不动、按钮点击没反应,但过几秒又恢复正常。

原因:某个按钮的槽函数里直接调用了instr.query(),示波器响应慢时阻塞了 UI 线程。

解决:全局搜索代码里所有.query(和.write(,确认是否都在 QThread 中执行。界面上只需要拿到结果信号后再更新 UI。如果只是偶发查询,也可以用QTimer.singleShot(0, ...)丢到下一次事件循环,但根治方案还是线程分离。

5.4 FPS 上不去:示波器传输模式不对

现象:实时刷新界面显示只有 3~5 FPS,看起来像幻灯片。

原因:默认的 ASCII 波形传输模式,每个采样点要转成数字字符串再传回电脑,10000 个点就要传几万字节,加上字符串解析开销,速度极慢。

解决:改用二进制传输DATA:ENCdg RIBinary,配合DATA:WIDth 1,单帧传输时间能从 500 毫秒降到 80 毫秒左右。如果还不满足,可以缩短采集长度,比如只采集 5000 点。对于“实时预览”需求,5000 点画出的波形在屏幕上已经完全足够。

5.5 测量值读到NaN:触发条件不满足

现象:MEASUREMENT:MEAS1:VALUE?返回的是文本NaN,程序转 float 时报错。

原因:当示波器当前没有稳定触发时,自动测量项无法计算有效值,返回 NaN 是正常行为。

解决:在自动测量之前先检查触发状态,轮询TRIGger:STATE?,返回READY或TRIGGER时才发起测量查询;同时把响应字符串做一次正则判断,匹配到 NaN 就丢弃这帧数据,不影响到界面的实时刷新。

6. 数据生图:把波形数组转成 QPixmap 的高效管线

6.1 从 numpy 到 QImage 的两种路径

前面的信号槽把波形数组发射到主线程,但还没讲怎么把它变成屏幕上看到的那张波形曲线图。最常见的路径有两种:一是用 QPainter 一笔一笔地画折线,二是先把 n umpy 数组转成 QImage 再贴到 QLabel 上。第一种的优点是曲线抗锯齿效果好而且可以叠加网格和光标,缺点是绘制耗时约 10~20 毫秒;第二种速度快但缩放失真。我在这套资源里用的方案是“折中”:波形曲线用 QPainter 绘制,背景网格提前渲染成静态 QPixmap 缓存,每次刷新只有曲线是动态画的。

from PyQt5.QtGui import QPainter, QImage, QPixmap, QColor, QPen from PyQt5.QtCore import Qt class WaveformWidget(QLabel): def __init__(self): super().__init__() self._wave = None self._pixmap = QPixmap(800, 400) self.setFixedSize(800, 400) def set_wave(self, wave_data): self._wave = wave_data self.update() # 触发重绘 def paintEvent(self, event): super().paintEvent(event) pixmap = QPixmap(800, 400) pixmap.fill(QColor("#1e1e1e")) painter = QPainter(pixmap) pen = QPen(QColor("#00ff00"), 1.5) painter.setPen(pen) if self._wave is not None and len(self._wave) > 1: n = len(self._wave) w, h = 800, 400 step = w / (n - 1) points = [QtCore.QPointF(i * step, h / 2 - (self._wave[i] * 50)) for i in range(n)] # 对点数做降采样,防止画笔开销过大 if n > 800: points = points[:: n // 800] painter.drawPolyline(points) painter.end() self.setPixmap(pixmap)

这段代码里有个容易忽略的参数:垂直缩放因子50。示波器的电压值如果是 ±0.5V,像素范围只有 ±25 像素,波形看起来是一条扁平线。更好的做法是根据当前通道垂直刻度动态计算缩放:pixels_per_volt = h / (2 * vertical_scale),把垂直刻度从配置文件读出来一路传到这里。我一般在set_wave时一起把刻度传进来,避免硬编码。

6.2 波形的自动归档与命名规范

实时刷新只是第一步,测试记录还需要按规则归档。实际产线验证中,我习惯按「被测板编号 + 用例号 + 实际测量值」的规范命名图片,例如PWR03_CASE01_ripple18.5mV.png。这样在多人协作时,评审人员从文件名里就能直接读出关键信息,不用打开图片再仔细看。这个自动化测试资源里已经把命名逻辑写成了独立的工具函数,接入新的测试项目时直接调用即可。

从那以后,我每次打开示波器上位机程序,都会强制先看一眼日志窗口里的VISA 资源连接成功和触发状态: READY这两条消息,确认通信链路和触发条件都没问题再开始跑用例。这套 PyQt5 + Tektronix 3000 的自动化框架,帮我彻底摆脱了一遍遍手动调示波器的重复劳动,也希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/17 2:52:42

PHP微信支付V3实战:从签名验签到回调解密完整案例

简介&#xff1a;这是一份面向PHP开发者的微信支付V3接口接入案例&#xff0c;适合具备PHP面向对象基础、需要为项目快速集成微信支付能力的开发者。资源围绕V3版本核心支付功能展开&#xff0c;同时兼容V2部分能力&#xff0c;覆盖订单创建、支付验证、退款申请、支付结果异步…

作者头像 李华
网站建设 2026/10/17 2:52:37

盗亦有道:Python爬虫robots协议与合规抓取实战解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/17 2:51:58

SSM整合实战:机票/火车票预订系统设计与部署详解

简介&#xff1a;基于SSM框架的机票/火车票预定系统课程设计完整项目包&#xff0c;面向软件工程专业学生以及需要完成类似课设的开发者&#xff0c;旨在解决预订系统从需求分析到编码实现的全流程参考问题&#xff0c;帮助读者掌握Spring、SpringMVC、MyBatis三大框架的整合与…

作者头像 李华
网站建设 2026/10/17 2:51:25

兰亭妙微原创作品| 竞业达智慧教室大屏端 UI/UX 设计解析

今天介绍一例兰亭妙微的原创案例——竞业达智慧课程平台的另一个核心终端——教室端&#xff08;大屏触控一体机&#xff09;。在智慧课堂中&#xff0c;大屏不仅是展示工具&#xff0c;更是师生互动的中枢。我们针对“远距离观看”和“触控操作”的双重特性&#xff0c;打造了…

作者头像 李华
网站建设 2026/10/17 2:50:50

微信PC端二维码自定义样式实现方案

1. 项目概述&#xff1a;为什么改微信PC端内嵌二维码不是“换个图片”那么简单“微信PC内嵌二维码的自定义样式更改”——这十个字背后&#xff0c;藏着大量开发者在实际协作、产品落地和用户运营中反复踩坑的真实场景。我接触过不下二十个团队&#xff0c;从某高校教务系统对接…

作者头像 李华