//

如何利用AI生成python可执行的带界面的软件工具

可以使用deepseek或者豆包,对话方式生成软件工具

举例实操:

第一步安装python软件 https://www.python.org/

生成一个综合了串口接收、实时曲线、一阶低通/移动平均滤波、FFT 频谱对比、导出 Excel 的桌面工具

代码如下,保存为ddp.py

 

import tkinter as tk
from tkinter import ttk, filedialog, messagebox
import numpy as np
import threading, queue, struct, os

import matplotlib
matplotlib.use("TkAgg")
from matplotlib.figure import Figure
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg

matplotlib.rcParams['font.sans-serif'] = ['Microsoft YaHei', 'SimHei',
                                          'PingFang SC', 'DejaVu Sans']
matplotlib.rcParams['axes.unicode_minus'] = False

try:
    import serial, serial.tools.list_ports
    HAS_SERIAL = True
except ImportError:
    HAS_SERIAL = False

try:
    import openpyxl
    HAS_OPENPYXL = True
except ImportError:
    HAS_OPENPYXL = False


# ---------------- 算法函数 ----------------
def compute_fft(x, fs):
    """返回 (频率轴, 幅值谱)"""
    n = len(x)
    if n < 16:
        return np.array([]), np.array([])
    win = np.hanning(n)
    amp = np.abs(np.fft.rfft(x * win)) / (n / 2.0)
    freq = np.fft.rfftfreq(n, 1.0 / fs)
    return freq, amp


def lowpass_iir(x, fs, fc):
    """一阶 IIR 低通滤波"""
    x = np.asarray(x, dtype=float)
    if len(x) == 0:
        return x
    fc = min(max(fc, 0.05), fs * 0.49)
    alpha = 1.0 - np.exp(-2.0 * np.pi * fc / fs)
    y = np.empty_like(x)
    acc = x[0]
    for i in range(len(x)):
        acc += alpha * (x[i] - acc)
        y[i] = acc
    return y


def moving_average(x, w):
    """滑动平均滤波"""
    x = np.asarray(x, dtype=float)
    w = max(1, int(w))
    if w == 1:
        return x.copy()
    kernel = np.ones(w) / w
    return np.convolve(x, kernel, mode='same')


# ---------------- 主程序 ----------------
class App(tk.Tk):
    def __init__(self):
        super().__init__()
        self.title("数据采集与分析工具 — 滤波 / FFT / Excel 导出")
        self.geometry("1200x800")
        self.minsize(900, 620)

        self.raw = []              # 原始数据缓冲
        self.max_points = 50000
        self.q = queue.Queue()

        self.running = False
        self.ser = None
        self.ser_thread = None
        self.sim_job = None
        self.sim_t = 0.0

        self._build_ui()
        self._refresh_ports()
        self.after(80, self._tick)

    # ---------- UI ----------
    def _build_ui(self):
        top = ttk.Frame(self, padding=8)
        top.pack(side=tk.TOP, fill=tk.X)

        # 第一行:数据源 / 串口 / 波特率 / 按钮
        r1 = ttk.Frame(top)
        r1.pack(fill=tk.X, pady=2)

        ttk.Label(r1, text="数据源:").pack(side=tk.LEFT)
        self.source_var = tk.StringVar(value="模拟信号")
        ttk.Combobox(r1, textvariable=self.source_var, width=10, state="readonly",
                     values=["模拟信号", "串口"]).pack(side=tk.LEFT, padx=(4, 12))

        ttk.Label(r1, text="串口:").pack(side=tk.LEFT)
        self.port_var = tk.StringVar()
        self.port_combo = ttk.Combobox(r1, textvariable=self.port_var,
                                       width=16, state="readonly")
        self.port_combo.pack(side=tk.LEFT, padx=4)
        ttk.Button(r1, text="刷新", command=self._refresh_ports).pack(side=tk.LEFT, padx=2)

        ttk.Label(r1, text="波特率:").pack(side=tk.LEFT, padx=(12, 0))
        self.baud_var = tk.StringVar(value="115200")
        ttk.Combobox(r1, textvariable=self.baud_var, width=9,
                     values=["9600", "19200", "38400", "57600",
                             "115200", "230400", "460800", "921600"]
                     ).pack(side=tk.LEFT, padx=4)

        self.run_btn = ttk.Button(r1, text="开始", command=self.toggle_run)
        self.run_btn.pack(side=tk.LEFT, padx=(14, 4))
        ttk.Button(r1, text="加载CSV", command=self.load_csv).pack(side=tk.LEFT, padx=2)
        ttk.Button(r1, text="清空", command=self.clear_data).pack(side=tk.LEFT, padx=2)

        # 第二行:分析参数
        r2 = ttk.Frame(top)
        r2.pack(fill=tk.X, pady=2)

        ttk.Label(r2, text="采样率(Hz):").pack(side=tk.LEFT)
        self.fs_var = tk.StringVar(value="1000")
        ttk.Entry(r2, textvariable=self.fs_var, width=8).pack(side=tk.LEFT, padx=4)

        ttk.Label(r2, text="滤波方式:").pack(side=tk.LEFT, padx=(12, 0))
        self.filter_var = tk.StringVar(value="一阶低通")
        ttk.Combobox(r2, textvariable=self.filter_var, width=10, state="readonly",
                     values=["无", "一阶低通", "移动平均"]
                     ).pack(side=tk.LEFT, padx=4)

        ttk.Label(r2, text="截止频率(Hz):").pack(side=tk.LEFT, padx=(12, 0))
        self.fc_var = tk.StringVar(value="20")
        ttk.Entry(r2, textvariable=self.fc_var, width=8).pack(side=tk.LEFT, padx=4)

        ttk.Label(r2, text="窗口点数:").pack(side=tk.LEFT, padx=(12, 0))
        self.win_var = tk.StringVar(value="5")
        ttk.Entry(r2, textvariable=self.win_var, width=6).pack(side=tk.LEFT, padx=4)

        ttk.Label(r2, text="显示点数:").pack(side=tk.LEFT, padx=(12, 0))
        self.disp_var = tk.StringVar(value="2000")
        ttk.Entry(r2, textvariable=self.disp_var, width=8).pack(side=tk.LEFT, padx=4)

        ttk.Button(r2, text="导出 Excel", command=self.export_excel
                   ).pack(side=tk.LEFT, padx=(14, 2))

        self.status = ttk.Label(top, text="就绪", foreground="#555")
        self.status.pack(fill=tk.X, pady=(4, 0))

        # 图表
        fig = Figure(figsize=(9, 5), dpi=100)
        self.ax1 = fig.add_subplot(2, 1, 1)
        self.ax2 = fig.add_subplot(2, 1, 2)
        fig.subplots_adjust(hspace=0.45, left=0.08, right=0.98,
                            top=0.94, bottom=0.10)

        self.ax1.set_title("时域波形对比(原始 vs 滤波后)")
        self.ax1.set_xlabel("时间 (s)")
        self.ax1.set_ylabel("幅值")
        self.ax1.grid(alpha=0.3)

        self.ax2.set_title("频谱对比(FFT)")
        self.ax2.set_xlabel("频率 (Hz)")
        self.ax2.set_ylabel("幅值")
        self.ax2.grid(alpha=0.3)

        self.canvas = FigureCanvasTkAgg(fig, master=self)
        self.canvas.get_tk_widget().pack(side=tk.BOTTOM, fill=tk.BOTH,
                                         expand=True, padx=8, pady=(4, 8))

    # ---------- 参数 ----------
    def _get_fs(self):
        try:
            fs = float(self.fs_var.get())
            return fs if fs > 0 else 1000.0
        except ValueError:
            return 1000.0

    def apply_filter(self, x):
        mode = self.filter_var.get()
        fs = self._get_fs()
        if mode == "一阶低通":
            try:
                fc = float(self.fc_var.get())
            except ValueError:
                fc = 20.0
            return lowpass_iir(x, fs, fc)
        if mode == "移动平均":
            try:
                w = int(float(self.win_var.get()))
            except ValueError:
                w = 5
            return moving_average(x, w)
        return np.asarray(x, dtype=float)

    # ---------- 刷新循环 ----------
    def _tick(self):
        while True:
            try:
                v = self.q.get_nowait()
            except queue.Empty:
                break
            self.raw.append(v)
            if len(self.raw) > self.max_points:
                del self.raw[:len(self.raw) - self.max_points]

        if self.raw:
            self._update_plot()
        self.after(80, self._tick)

    def _update_plot(self):
        fs = self._get_fs()
        try:
            disp = max(16, int(float(self.disp_var.get())))
        except ValueError:
            disp = 2000

        if len(self.raw) < 8:
            return

        x = np.array(self.raw[-disp:], dtype=float)
        y = self.apply_filter(x)
        t = np.arange(len(x)) / fs

        self.ax1.clear()
        self.ax1.plot(t, x, color="#888888", lw=1.0, label="原始")
        self.ax1.plot(t, y, color="#d62828", lw=1.4, label="滤波后")
        self.ax1.set_title("时域波形对比(原始 vs 滤波后)")
        self.ax1.set_xlabel("时间 (s)")
        self.ax1.set_ylabel("幅值")
        self.ax1.grid(alpha=0.3)
        self.ax1.legend(loc="upper right", fontsize=8)

        f1, a1 = compute_fft(x, fs)
        f2, a2 = compute_fft(y, fs)

        self.ax2.clear()
        if len(f1):
            self.ax2.plot(f1, a1, color="#888888", lw=1.0, label="原始")
            self.ax2.plot(f2, a2, color="#d62828", lw=1.2, label="滤波后")
            self.ax2.set_xlim(0, min(fs / 2.0, 200))
        self.ax2.set_title("频谱对比(FFT)")
        self.ax2.set_xlabel("频率 (Hz)")
        self.ax2.set_ylabel("幅值")
        self.ax2.grid(alpha=0.3)
        self.ax2.legend(loc="upper right", fontsize=8)

        self.canvas.draw_idle()

    # ---------- 数据源 ----------
    def _refresh_ports(self):
        if not HAS_SERIAL:
            self.port_combo["values"] = ["(未安装 pyserial)"]
            return
        ports = [p.device for p in serial.tools.list_ports.comports()]
        self.port_combo["values"] = ports
        if ports and not self.port_var.get():
            self.port_combo.current(0)

    def toggle_run(self):
        if self.running:
            self.stop()
        else:
            self.start()

    def start(self):
        src = self.source_var.get()

        if src == "串口":
            if not HAS_SERIAL:
                messagebox.showerror("缺少依赖",
                                     "未安装 pyserial\n请执行: pip install pyserial")
                return
            port = self.port_var.get()
            if not port or "未安装" in port:
                messagebox.showwarning("提示", "请先选择串口")
                return
            try:
                baud = int(self.baud_var.get())
            except ValueError:
                baud = 115200
            try:
                self.ser = serial.Serial(port, baud, timeout=1)
            except Exception as e:
                messagebox.showerror("打开串口失败", str(e))
                return
            self.running = True
            self.ser_thread = threading.Thread(target=self._serial_worker,
                                               daemon=True)
            self.ser_thread.start()
        else:
            self.running = True
            self.sim_t = 0.0
            self._sim_tick()

        self.run_btn.config(text="停止")
        self.status.config(text="运行中 · " + src)

    def stop(self):
        self.running = False
        if self.sim_job:
            try:
                self.after_cancel(self.sim_job)
            except Exception:
                pass
            self.sim_job = None
        if self.ser:
            try:
                self.ser.close()
            except Exception:
                pass
            self.ser = None
        self.run_btn.config(text="开始")
        self.status.config(text="已停止")

    def _serial_worker(self):
        """串口线程:每 4 字节解析一个小端 float32"""
        while self.running and self.ser:
            try:
                chunk = self.ser.read(4)
            except Exception:
                break
            if len(chunk) == 4:
                v = struct.unpack("<f", chunk)[0]
                if np.isfinite(v):
                    self.q.put(float(v))

    def _sim_tick(self):
        """模拟信号:5Hz 有用信号 + 60Hz 干扰 + 白噪声"""
        if not self.running:
            return
        fs = self._get_fs()
        n = min(max(1, int(fs * 0.05)), 1000)
        tt = self.sim_t + np.arange(n) / fs
        sig = (np.sin(2 * np.pi * 5 * tt)
               + 0.5 * np.sin(2 * np.pi * 60 * tt)
               + 0.25 * np.random.randn(n))
        for v in sig:
            self.q.put(float(v))
        self.sim_t = self.sim_t + n / fs
        self.sim_job = self.after(50, self._sim_tick)

    # ---------- CSV 导入 ----------
    def load_csv(self):
        path = filedialog.askopenfilename(
            filetypes=[("CSV", "*.csv"), ("文本", "*.txt"), ("所有文件", "*.*")])
        if not path:
            return
        vals = []
        with open(path, "r", encoding="utf-8-sig") as f:
            for line in f:
                line = line.strip()
                if not line:
                    continue
                for p in line.replace(";", ",").split(","):
                    try:
                        vals.append(float(p))
                        break
                    except ValueError:
                        continue
        if not vals:
            messagebox.showwarning("提示", "未从文件中读取到数值")
            return
        self.raw = vals[-self.max_points:]
        self.status.config(text=f"已加载 {len(vals)} 个点:{os.path.basename(path)}")
        self._update_plot()

    # ---------- Excel 导出 ----------
    def export_excel(self):
        if not self.raw:
            messagebox.showwarning("提示", "暂无数据可导出")
            return
        if not HAS_OPENPYXL:
            messagebox.showerror("缺少依赖",
                                 "未安装 openpyxl\n请执行: pip install openpyxl")
            return

        path = filedialog.asksaveasfilename(
            defaultextension=".xlsx",
            filetypes=[("Excel 工作簿", "*.xlsx")],
            initialfile="数据分析结果.xlsx")
        if not path:
            return

        fs = self._get_fs()
        x = np.array(self.raw, dtype=float)
        y = self.apply_filter(x)
        t = np.arange(len(x)) / fs
        f1, a1 = compute_fft(x, fs)
        f2, a2 = compute_fft(y, fs)

        try:
            wb = openpyxl.Workbook()

            ws = wb.active
            ws.title = "时域数据"
            ws.append(["序号", "时间(s)", "原始值", "滤波后"])
            for i in range(len(x)):
                ws.append([i, round(float(t[i]), 6),
                           round(float(x[i]), 6), round(float(y[i]), 6)])

            ws2 = wb.create_sheet("频域数据")
            ws2.append(["频率(Hz)", "原始幅值", "滤波后幅值"])
            m = min(len(f1), len(f2))
            for i in range(m):
                ws2.append([round(float(f1[i]), 4),
                            round(float(a1[i]), 6),
                            round(float(a2[i]), 6)])

            ws3 = wb.create_sheet("参数")
            ws3.append(["项目", "值"])
            ws3.append(["采样率(Hz)", fs])
            ws3.append(["滤波方式", self.filter_var.get()])
            ws3.append(["截止频率(Hz)", self.fc_var.get()])
            ws3.append(["窗口点数", self.win_var.get()])
            ws3.append(["数据点数", len(x)])

            wb.save(path)
            messagebox.showinfo("完成", "已导出:\n" + path)
        except Exception as e:
            messagebox.showerror("导出失败", str(e))

    # ---------- 其他 ----------
    def clear_data(self):
        self.raw.clear()
        while not self.q.empty():
            try:
                self.q.get_nowait()
            except queue.Empty:
                break
        self.ax1.clear()
        self.ax2.clear()
        self.canvas.draw_idle()
        self.status.config(text="已清空数据")

    def on_close(self):
        self.stop()
        self.destroy()


if __name__ == "__main__":
    app = App()
    app.protocol("WM_DELETE_WINDOW", app.on_close)
    app.mainloop()
View Code

 

第二步,双击ddp.py发现闪退,没有执行

双击闪退,是因为窗口在脚本崩溃后立刻关闭了。排查的第一步,是让错误信息停留在屏幕上。

打开命令提示符(CMD),用 cd 命令进入你保存 py 文件的目录,然后输入以下命令运行:

pip install numpy matplotlib pyserial openpyxl 

如果下载慢,可以加上国内镜像:推荐

pip install numpy matplotlib pyserial openpyxl -i https://pypi.tuna.tsinghua.edu.cn/simple

之后可正常运行软件,界面如下,此时还必须依赖python的环境还能运行

image

 第三步,如果想要发给别人直接打开使用,还需要变成可直接运行的exe文件

Python 代码打包成 Windows 可执行 exe,最常用的是 PyInstaller

确认版本:

python --version

如果显示 3.15.0bX 之类的,先换稳定版。然后再继续下面操作

 

1.安装 PyInstaller

pip install pyinstaller

如果下载慢,加国内镜像:推荐

pip install pyinstaller -i https://pypi.tuna.tsinghua.edu.cn/simple

2.打包命令

pyinstaller -F -w --name 程序名 你的脚本.py

参数含义:

 
参数作用
-F(--onefile) 打包成单个 exe,方便分发
-w(--noconsole/--windowed) 隐藏黑色控制台窗口,GUI 程序必加
--name 指定 exe 名称
-i xxx.ico 指定 exe 图标(可选)

打包命令实例

pyinstaller -F -w --name ddp ddp.py

或者

如果不需要单文件,去掉 -F 生成文件夹版,启动更快、体积也更好控制:

pyinstaller -w --name ddp_tools ddp.py

打包完成后,exe 在脚本目录下的 dist/ 文件夹里。中间生成的 build/ 和 *.spec 文件可以删掉,只有 dist/ 是最终产物。

🚨 常见问题与解决

1. 双击 exe 闪退,看不到报错

先去掉 -w 重新打包,用带控制台的版本运行,错误信息就会打印在窗口里。也可以从 CMD 里直接运行 exe 查看输出。

2. 杀毒软件报毒 / 被拦截

PyInstaller 打包的程序被误报是常见现象(因为自解压行为类似病毒)。解决方式:

  • 把 exe 加入杀毒白名单;

  • 不要用 -F 单文件模式,改用文件夹模式,误报率会低很多;

  • 给 exe 做代码签名(需要证书)。

3. 打包后提示 ModuleNotFoundError

有些库是动态导入的,PyInstaller 没自动识别。用 --hidden-import=模块名 手动补上,比如:

pyinstaller -F -w --hidden-import=serial.tools.list_ports --name 数据分析工具 data_tool.py

4. matplotlib 打包后启动很慢

这是正常的,matplotlib 首次加载字体缓存较慢。可以加 --exclude-module 排除用不到的模块减小体积,比如:

pyinstaller -F -w --exclude-module PyQt5 --exclude-module PySide2 --name 数据分析工具 data_tool.py

5. tkinter 相关报错

官方 Python 安装包默认带 tkinter,PyInstaller 一般能自动处理。如果报缺 tcl/tk,重装 Python 时勾选 tcl/tk and IDLE,再重新打包。

🔄 替代方案

如果 PyInstaller 不满足需求,还有两个选择:

  • Nuitka:把 Python 编译成 C 再编译,生成的 exe 启动更快、反编译更难,但打包时间较长。命令示例:

python -m nuitka --standalone --onefile --enable-plugin=tk-inter --windows-console-mode=disable gomoku.py
  • cx_Freeze:配置 setup.py 后打包,灵活性高,适合复杂项目。

✅ 建议的操作顺序

  1. 确认 Python 是稳定版;

  2. 在脚本目录装好 pyinstaller 和程序依赖;

  3. 先用不加 -w 的方式打包测试,确认能跑;

  4. 没问题后再加 -w 和图标出正式版;

  5. 把 dist/ 里的 exe 复制到别的 Windows 电脑上测试,确认没有遗漏 DLL。

这样打出来的 exe 在 64 位 Windows 10/11 上可以直接双击运行,不需要目标机器装 Python。  

posted @ 2026-10-26 09:56  一只鱼在水杯里  阅读(11)  评论(0)    收藏  举报