3、项目ProgressBar进度条、StatusStrip底部状态栏、ToolTip悬浮提示

完整分步教学:第三梯队项目 03_TestFunction_Demo

GitHub地址

一、整体规划

1. 文件夹命名(遵循你仓库序号规范)

总仓库根目录:SFP-Test-UI-Learning

新建文件夹:03_TestFunction_Demo

用途:进度条、状态栏、文件读写、CSV测试报告导出

2. 项目定位

基于前两阶段知识(布局容器+串口),新增测试业务配套功能 ,不依赖硬件,纯代码可自测;

可两种开发方式:

  1. 独立新项目(推荐,知识点隔离,学习清晰)
  2. 直接在 02_SerialPort_LearnDemo 扩展(适合整合完整上位机)

下面以独立新项目教学,兼容VS2019 .NET Framework 4.8

3. 最终实现全部功能清单

  1. 界面新增控件:ProgressBar进度条、StatusStrip底部状态栏、ToolTip悬浮提示
  2. 模拟批量自动测试,进度条自动走完
  3. 状态栏实时显示:串口状态、当前时间、测试状态
  4. Path类自动按日期创建日志文件夹、自动生成文件名
  5. StreamWriter 读写TXT日志文件
  6. 原生代码导出CSV测试报告(无第三方库,光模块行业通用)

第一阶段:新建项目 + 拖拽布局(复用阶段1布局知识)

步骤1:新建项目

  1. VS2019 创建「Windows窗体应用(.NET Framework)」
  2. 项目名:03_TestFunction_Demo
  3. 存放路径:SFP-Test-UI-Learning\03_TestFunction_Demo
  4. 项目属性修改:
    • 程序集名称:_03_TestFunction_Demo
    • 默认命名空间:_03_TestFunction_Demo

步骤2:拖拽全套控件(结合Dock/Anchor布局)

1)底部状态栏 StatusStrip

  1. 工具箱拖 StatusStrip 到窗体,默认 Dock=Bottom
  2. 点击状态栏右侧下拉,添加3个 ToolStripLabel
    • Name:lblDevStatus,Text:设备:未连接
    • Name:lblTestStatus,Text:测试:空闲
    • Name:lblTime,Text:时间:00:00:00
  3. 新增 Timer 控件(后台实时刷新时间)
    • Name:timerStatus,Interval=1000(1秒刷新一次),Enabled=true

一步一步跟着操作,超详细图文式步骤,你现在空白窗体直接照着点

第一步:操作底部已经出现的 StatusStrip(状态栏)

你截图左下角已经自动出现 statusStrip1 控件,就是底部那条细长条:

  1. 用鼠标左键单击选中窗体最下方这个 StatusStrip 长条
  2. 你会看到长条最右侧出现一个小小的 ▼ 下拉箭头,点一下这个下拉小三角
  3. 在弹出的下拉菜单里,选择第一项:StatusLabel(也就是 ToolStripLabel)

👉 添加第一个标签并改属性

  1. 点完之后状态栏多出一块文字区域,选中这个新添加的标签
  2. 看窗口右侧【属性】面板 ,修改两个值:
    • Name 属性:改成 lblDevStatus
    • Text 属性:改成 设备:未连接

👉 添加第二个标签

再次点状态栏右侧的 ▼ 下拉箭头 → 继续选 StatusLabel

修改属性:

  • NamelblTestStatus
  • Text测试:空闲

👉 添加第三个标签

第三次点下拉 ▼ → 选 StatusLabel

修改属性:

  • NamelblTime
  • Text时间:00:00:00

做完效果:状态栏从左到右并排三个文字标签。


第二步:添加 Timer 定时器控件(用来每秒刷新时间)

  1. 看软件最左边侧边栏,找到 工具箱 (你截图左边能看到「工具箱」三个字)
    如果工具箱隐藏了:顶部菜单 视图 → 工具箱 调出来
  2. 在工具箱里面找到分类:组件
  3. 在组件列表里找到 Timer,鼠标按住它,拖到窗体任意空白位置松开
    ✅ 注意:Timer 是后台不可见控件,拖动后会自动出现在窗体最下方组件托盘区,不会画在窗体表面

修改 Timer 属性

选中下方托盘里刚放的 timer1,右侧属性面板修改:

  1. Name:改为 timerStatus
  2. Interval:填 1000 (单位毫秒,1000ms = 1秒执行一次)
  3. Enabled:设置为 True (程序一运行就自动启动计时)

第三步:给Timer写刷新时间的事件(后续代码要用)

  1. 双击下方托盘里的 timerStatus 控件
  2. VS会自动跳转到代码页面,生成 timerStatus_Tick 空方法,后面我们直接粘贴刷新时间代码即可

补充小避坑

  1. 如果看不到属性面板:顶部菜单 视图 → 属性窗口(快捷键 F4)
  2. Name 是后台代码名字,大小写不能写错;Text 是界面显示文字
  3. StatusStrip 默认 Dock=Bottom,会一直吸附在窗体最底部,窗口缩放位置不会乱

完整分步拖拽布局(第三梯队项目 03_TestFunction_Demo)

顺着你已经做好的底部StatusStrip+Timer继续往下做,全程复用第一阶段 Dock / Anchor 布局知识点,做完界面再写代码。

整体窗体分层规划(从上到下)

  1. 顶部按钮区域(FlowLayoutPanel)
  2. 进度条区域(Panel + ProgressBar)
  3. 主体日志显示区域(SplitContainer + RichTextBox)
  4. 底部状态栏(你已经做完的StatusStrip)

步骤1:放置顶部按钮容器 FlowLayoutPanel

  1. 左侧工具箱找到 FlowLayoutPanel,拖到窗体空白处

  2. 选中这个控件,右侧属性修改:

    Name = flowBtnTop
    Dock = Top
    Height = 100
    BackColor = LightGray

作用:自动横向排布三个功能按钮,窗口拉宽自动自适应。

在 flowBtnTop 内部添加3个按钮

点击进入面板内部,依次拖入3个 Button,逐个改属性:

控件Name Text文字 配套ToolTip说明(后面设置悬浮提示用)
btnStartTest 开始批量测试 模拟10路SFP自动测试,进度条同步展示测试进度
btnSaveTxtLog 保存TXT日志 将当前窗口全部运行日志保存到当日日志文件夹
btnSaveCsvReport 导出CSV测试报告 导出所有测试数据,可用Excel直接打开查看表格

三个按钮会自动横向排队,不用手动对齐位置。


步骤2:放置进度条 Panel + ProgressBar

  1. 工具箱拖一个 Panel 放到窗体(会自动排在按钮面板下方)
    属性设置:

    Name = panelProgress
    Dock = Top
    Height = 60

  2. 点击进入 panelProgress 内部,拖拽 ProgressBar 进度条控件
    属性设置:

    Name = progressTest
    Dock = Fill
    Margin = 10
    Minimum = 0
    Maximum = 100
    Value = 0

效果:进度条四周留边、铺满整个Panel,窗口拉伸自动跟着变宽。


步骤3:主体日志区域 SplitContainer(剩余全部窗口空间)

  1. 工具箱拖拽 SplitContainer 到窗体,默认自动填充剩余空间
    属性设置:

    Name = splitMain
    Dock = Fill
    Orientation = Horizontal (上下分割)
    SplitterDistance = 80

  • 上半部分:预留以后放测试参数显示(本次暂时不用)
  • 下半部分:放日志框

在 splitMain.Panel2 下半区放日志框

  1. 点开 Panel2,拖拽 RichTextBox
    属性设置:

    Name = rtbLog
    Dock = Fill
    ReadOnly = True
    ScrollBars = Vertical

ReadOnly=True 禁止用户手动修改日志内容,只能程序追加写入。


步骤4:添加 ToolTip 悬浮提示控件

  1. 工具箱 → 组件分类,找到 ToolTip,拖动到窗体任意位置

  2. 它会自动跑到窗体下方组件托盘,修改名称:

    Name = toolTipHelp

给三个按钮绑定悬浮提示

  1. 选中 btnStartTest,右侧属性找到 ToolTip on toolTipHelp,填入:
    模拟多通道SFP自动测试,进度条实时更新测试进度
  2. 选中 btnSaveTxtLog,填入:
    将本次运行全部日志保存到按日期分类的本地文件夹
  3. 选中 btnSaveCsvReport,填入:
    导出本次所有模块电压、温度测试数据,生成Excel可打开的CSV表格

运行后鼠标悬停按钮,自动弹出说明文字,提升软件易用性。

布局完成整体结构核对

复制代码
Form1 主窗口
├─ flowBtnTop(Dock=Top)   三个功能按钮
├─ panelProgress(Dock=Top)进度条
├─ splitMain(Dock=Fill)   上下分割容器
│  ├─ Panel1 预留参数区
│  └─ Panel2 → rtbLog 日志框
└─ statusStrip1(Dock=Bottom) 状态栏(你已完成)
下方组件托盘:timerStatus、toolTipHelp

下一步安排

布局全部拖拽完成后,我给你完整带逐行详细注释的 Form1.cs 全部代码,包含:

  1. 自动创建日志文件夹(Path、Directory文件操作)
  2. Timer状态栏时间刷新逻辑
  3. 批量测试+进度条走动模拟
  4. TXT日志保存代码
  5. CSV报告导出代码

你现在拖拽过程遇到哪个控件找不到、属性不会改,随时截图发我,我当场给你指点。

需要我现在直接把完整可粘贴的代码提前发给你吗?

2)进度条 ProgressBar

  1. 拖拽 Panel Dock=Top,Height=60
  2. Panel内拖 ProgressBar
    • Name:progressTest
    • Dock=Fill,Margin=10
    • Minimum=0,Maximum=100,Value=0

3)功能按钮区(FlowLayoutPanel)

FlowLayoutPanel Dock=Top,Height=100

依次放入按钮:

  1. btnStartTest:开始批量测试
  2. btnSaveTxtLog:保存日志TXT
  3. btnSaveCsvReport:导出CSV测试报告

4)ToolTip 悬浮提示控件

直接拖拽 ToolTip 到窗体(自动在底部组件栏)

Name:toolTipHelp

选中每个按钮,属性 ToolTip on toolTipHelp 填写提示文字:

  • 开始批量测试:模拟多通道SFP自动测试,进度条实时更新
  • 保存日志TXT:将本次运行日志保存到本地日期文件夹
  • 导出CSV报告:导出所有模块电压、温度测试数据,可用Excel打开

5)日志框(复用阶段2)

SplitContainer上下分栏,下半区放RichTextBox rtbLog

最终顶层布局结构(全是第一阶段知识点)

复制代码
Form1
├─ StatusStrip(Dock=Bottom)底部状态栏
├─ FlowLayoutPanel(Dock=Top)按钮区
├─ Panel(Dock=Top)进度条区域
└─ SplitContainer(Dock=Fill) 中间日志区域

第二阶段:完整代码分步编写(逐段带详细注释,小白友好)

1. 顶部引用命名空间(新增文件操作必备)

csharp 复制代码
using System;
using System.IO; // 文件、文件夹、路径操作核心命名空间(本阶段重点)
using System.Windows.Forms;

2. 全局变量定义

csharp 复制代码
namespace _03_TestFunction_Demo
{
    public partial class Form1 : Form
    {
        // 存储模拟测试数据列表,用于导出CSV报告
        // 每条数据:模块编号、电压、温度、测试结果
        private List<string[]> testDataList = new List<string[]>();

        public Form1()
        {
            InitializeComponent();

            // 初始化:自动创建今日日志文件夹
            CreateLogFolder();

            // 启动计时器,刷新状态栏时间
            timerStatus.Start();
        }
    }
}

3. 工具方法1:CreateLogFolder 按日期自动创建日志文件夹(Path类核心)

csharp 复制代码
/// <summary>
/// 程序启动自动生成日志文件夹,格式:Logs/2026-06-23
/// Path类:专门处理文件路径,跨系统兼容,不用手动拼接斜杠
/// </summary>
private void CreateLogFolder()
{
    // 获取程序运行根目录
    string basePath = Application.StartupPath;
    // 拼接Logs总文件夹路径
    string logRoot = Path.Combine(basePath, "Logs");
    // 获取当天日期字符串 yyyy-MM-dd
    string today = DateTime.Now.ToString("yyyy-MM-dd");
    // 拼接当日日志文件夹完整路径
    string todayLogPath = Path.Combine(logRoot, today);

    // 判断文件夹不存在则创建
    if (!Directory.Exists(todayLogPath))
    {
        Directory.CreateDirectory(todayLogPath);
    }

    // 存储全局,后续保存文件使用
    GlobalLogPath = todayLogPath;
}
// 全局存储当日日志路径
private string GlobalLogPath = "";

知识点讲解

  1. Path.Combine():自动处理路径斜杠\,不会出现 D:\\Logs\2026 这种斜杠错乱问题
  2. Directory.Exists() 判断文件夹是否存在;Directory.CreateDirectory() 创建文件夹
  3. Application.StartupPath 获取exe程序所在文件夹,相对路径,迁移项目不用改路径

4. 计时器事件:实时刷新状态栏时间

双击设计器timerStatus自动生成方法

csharp 复制代码
/// <summary>
/// 每秒执行一次,刷新状态栏系统时间
/// </summary>
private void timerStatus_Tick(object sender, EventArgs e)
{
    string nowTime = DateTime.Now.ToString("HH:mm:ss");
    lblTime.Text = $"时间:{nowTime}";
}

5. 【开始批量测试】按钮:模拟自动测试 + 进度条更新

csharp 复制代码
private void btnStartTest_Click(object sender, EventArgs e)
{
    // 重置进度条、测试数据
    progressTest.Value = 0;
    testDataList.Clear();
    lblTestStatus.Text = "测试:运行中";
    AddLog("===== 开始批量SFP模块自动测试 =====");

    // 模拟10个光模块循环测试
    int totalModule = 10;
    for (int i = 1; i <= totalModule; i++)
    {
        // 模拟读取硬件数据
        string moduleId = $"SFP-{i:D2}";
        string volt = (3.2 + i * 0.05).ToString("0.00");
        string temp = (25 + i).ToString("0.0");
        string result = "合格";

        // 存入测试数据集合,用于导出报告
        testDataList.Add(new string[] { moduleId, volt, temp, result });

        // 更新进度条
        progressTest.Value = i * 100 / totalModule;
        AddLog($"测试模块{moduleId} | 电压:{volt}V | 温度:{temp}℃ | {result}");

        // 模拟硬件读取延时,肉眼可见进度条走动
        System.Threading.Thread.Sleep(200);
        // 刷新界面,防止窗口假死
        Application.DoEvents();
    }

    lblTestStatus.Text = "测试:完成";
    AddLog("===== 全部模块测试完成 =====");
}

6. 保存TXT日志文件(StreamWriter文本写入)

csharp 复制代码
private void btnSaveTxtLog_Click(object sender, EventArgs e)
{
    try
    {
        // 拼接日志文件名:时:分:秒.txt
        string fileName = DateTime.Now.ToString("HH-mm-ss") + "_运行日志.txt";
        string fullPath = Path.Combine(GlobalLogPath, fileName);

        // 创建写入流,写入日志框全部内容
        using (StreamWriter sw = new StreamWriter(fullPath, false, Encoding.UTF8))
        {
            sw.Write(rtbLog.Text);
        }

        MessageBox.Show($"TXT日志保存成功!\n路径:{fullPath}", "保存完成");
        AddLog($"已保存日志文件:{fileName}");
    }
    catch (Exception ex)
    {
        MessageBox.Show($"保存失败:{ex.Message}", "错误");
    }
}

核心知识点

  • using(StreamWriter):自动释放文件资源,不用手动Close,防止文件占用
  • Encoding.UTF8:中文不乱码(不加中文会变成问号)

7. 导出CSV测试报告(本阶段重点,Excel直接打开)

csharp 复制代码
private void btnSaveCsvReport_Click(object sender, EventArgs e)
{
    if (testDataList.Count == 0)
    {
        MessageBox.Show("暂无测试数据,请先执行批量测试!");
        return;
    }

    try
    {
        string fileName = DateTime.Now.ToString("HH-mm-ss") + "_测试报告.csv";
        string fullPath = Path.Combine(GlobalLogPath, fileName);

        using (StreamWriter sw = new StreamWriter(fullPath, false, Encoding.UTF8))
        {
            // 写入CSV表头(逗号分隔)
            sw.WriteLine("模块编号,电压(V),温度(℃),测试结果");
            // 循环写入每条测试数据
            foreach (string[] dataRow in testDataList)
            {
                sw.WriteLine($"{dataRow[0]},{dataRow[1]},{dataRow[2]},{dataRow[3]}");
            }
        }

        MessageBox.Show($"CSV报告导出成功!可使用Excel打开\n路径:{fullPath}", "导出完成");
        AddLog($"已导出CSV测试报告:{fileName}");
    }
    catch (Exception ex)
    {
        MessageBox.Show($"导出失败:{ex.Message}", "错误");
    }
}

CSV原理

CSV是纯文本表格,逗号分隔单元格,微软Excel原生支持,无需引用任何第三方dll,工业上位机首选报表格式。

8. 通用日志AddLog方法(复用前阶段)

csharp 复制代码
private void AddLog(string msg)
{
    string time = DateTime.Now.ToString("HH:mm:ss");
    string logText = $"[{time}] {msg}\r\n";
    rtbLog.AppendText(logText);
    rtbLog.SelectionStart = rtbLog.TextLength;
    rtbLog.ScrollToCaret();
}

第三阶段:自测运行步骤(不用硬件,纯软件模拟)

  1. 运行程序,自动在exe目录生成 Logs/当日日期 文件夹
  2. 鼠标悬浮按钮,查看ToolTip悬浮提示
  3. 底部状态栏自动刷新系统时间
  4. 点击【开始批量测试】:进度条逐步填满,生成10组SFP模拟数据
  5. 点击【保存日志TXT】:日志框全部内容保存到当日文件夹
  6. 点击【导出CSV报告】:生成表格文件,双击可用Excel打开查看测试数据

csharp 复制代码
# 全套代码逐行超详细小白注释版(每一段、每一行都解释用途、原理)
```csharp
// ===================== 第一部分:引入命名空间 =====================
// System:C#最基础核心库,日期、基础类型、异常、字符串都依赖它
using System;
// List、Dictionary 等集合容器专用,本项目用来存测试数据
using System.Collections.Generic;
// 窗体组件设计器自动生成依赖,不用手动修改
using System.ComponentModel;
// 表格数据类,本Demo暂时没用到,VS新建窗体自带
using System.Data;
// 颜色、绘图、窗体尺寸相关,本Demo暂时没用到
using System.Drawing;
// Linq查询语法,本Demo暂时没用到
using System.Linq;
// 字符串编码、文字处理
using System.Text;
// 异步线程任务,本Demo暂时没用到
using System.Threading.Tasks;
// 所有窗体控件:按钮、文本框、状态栏、进度条、RichTextBox全部依赖
using System.Windows.Forms;
// 【本阶段核心】文件、文件夹、路径操作专用库,第三梯队重点学习内容
using System.IO;

// 项目命名空间,所有代码都包裹在这个空间内,区分不同项目代码
namespace _03_TestFunction_Demo
{
    // Form1主窗体类,partial代表代码拆分:界面拖拽的控件代码在另一个Designer文件
    public partial class Form1 : Form
    {
        // ==============================================
        // 全局私有集合变量:存储所有批量测试产生的数据
        // List<string[]>:
        //  1.List<> 动态列表,长度可以自动增加/删除,不用固定数组长度
        //  2.string[] 字符串数组,一条测试数据固定4列:模块编号、电压、温度、结果
        //  整体作用:测试时临时缓存数据,导出CSV表格时统一读取
        // ==============================================
        private List<string[]> testDataList = new List<string[]>();

        // 窗体构造函数:程序打开窗口时,第一个自动执行的方法
        public Form1()
        {
            // VS自动生成固定代码,加载你拖拽的所有界面控件(按钮、状态栏、进度条等),不能删除
            InitializeComponent();

            // 调用自定义方法:程序一打开就自动创建今日日志文件夹,不用手动新建
            CreateLogFolder();

            // 启动定时器timerStatus,每秒自动刷新底部状态栏的系统时间
            timerStatus.Start();
        }

        /// <summary>
        /// 自定义方法:自动按日期创建日志文件夹
        /// 存储规则:程序目录/Logs/2026-06-23
        /// Path类:专门处理文件路径,自动适配Windows斜杠\,不会出现路径符号错乱
        /// </summary>
        private void CreateLogFolder()
        {
            // Application.StartupPath:获取程序exe文件所在的文件夹(相对路径,复制项目不用改路径)
            string basePath = Application.StartupPath;

            // Path.Combine:拼接路径,自动补充分隔符,拼接出总日志文件夹Logs
            string logRoot = Path.Combine(basePath, "Logs");

            // 获取当前系统日期,格式:年-月-日,用来区分每日日志文件夹
            string today = DateTime.Now.ToString("yyyy-MM-dd");

            // 拼接当日专属日志文件夹完整路径 Logs/2026-06-23
            string todayLogPath = Path.Combine(logRoot, today);

            // Directory.Exists:判断这个日期文件夹是否已经存在
            // ! 代表取反:如果文件夹不存在,进入if内部创建
            if (!Directory.Exists(todayLogPath))
            {
                // Directory.CreateDirectory:创建文件夹,多层文件夹不存在会一次性全部生成
                Directory.CreateDirectory(todayLogPath);
            }

            // 将当日文件夹路径赋值给全局变量,后面保存TXT/CSV文件直接使用,不用重复拼接路径
            GlobalLogPath = todayLogPath;
        }

        // 全局字符串变量:保存当天日志文件夹完整路径,所有保存文件方法共用
        private string GlobalLogPath = "";

        /// <summary>
        /// 定时器触发事件:每1000毫秒(1秒)自动执行一次
        /// 功能:刷新底部状态栏lblTime显示当前日期
        /// </summary>
        private void timerStatus_Tick(object sender, EventArgs e)
        {
            // 获取当前系统日期,格式 年-月-日
            string nowTime = DateTime.Now.ToString("yyyy-MM-dd");
            // 修改状态栏标签文字,展示时间
            lblTime.Text = $"时间:{nowTime}";
        }

        /// <summary>
        /// 【开始批量测试】按钮点击事件
        /// 模拟10个SFP光模块自动化测试,更新进度条、生成测试数据、打印日志
        /// </summary>
        private void btnStartTest_Click(object sender, EventArgs e)
        {
            // 1.进度条数值归零,新一轮测试从头开始,清除上一次进度残留
            progressTest.Value = 0;

            // 2.清空测试数据集合,防止本次导出混入上一轮旧测试数据
            testDataList.Clear();

            // 3.修改底部状态栏文字,提示用户当前程序正在运行测试
            lblTestStatus.Text = "测试:运行中";

            // 调用日志方法,打印测试开始提示
            AddLog("===== 开始批量SFP模块自动测试 =====");

            // 定义本次一共测试10个光模块
            int totalModule = 10;

            // for循环:i从1循环到10,依次测试每一个模块
            for (int i = 1; i <= totalModule; i++)
            {
                // ========== 模拟硬件读取模块数据 ==========
                // $字符串插值,D2代表数字固定2位,不足前面补0
                // i=1 → SFP-01  i=10 → SFP-10,编号整齐统一
                string moduleId = $"SFP-{i:D2}";

                // 模拟电压计算:基准3.2V,每多一个模块+0.5V,ToString("0.0")保留1位小数
                string volt = (3.2 + i * 0.5).ToString("0.0");

                // 模拟温度计算:基准25℃,每多一个模块温度+1,保留1位小数
                string temp = (25 + i).ToString("0.0");

                // 模拟测试结果,全部默认合格
                string result = "合格";

                // 将当前模块4项数据存入测试集合List,循环结束后统一导出CSV
                testDataList.Add(new string[] { moduleId, volt, temp, result });

                // 计算当前测试进度:当前序号/总数量*100,赋值给进度条
                progressTest.Value = i * 100 / totalModule;

                // 打印本条模块测试信息到日志框
                AddLog($"测试模块{moduleId}|电压:{volt}V|温度{temp}℃|{result}");

                // 线程休眠200毫秒 = 0.2秒,模拟真实硬件读取数据的延迟
                // 如果不加这行,循环瞬间跑完,进度条直接拉满,看不到动态效果
                System.Threading.Thread.Sleep(200);

                // 强制刷新界面,防止Sleep休眠时窗口卡死、拖动无响应
                Application.DoEvents();
            }

            // 循环全部结束,修改状态栏提示测试完成
            lblTestStatus.Text = "测试:完成";
            AddLog("===== 全部模块测试完成 =====");
        }

        /// <summary>
        /// 通用日志打印工具方法,所有日志统一调用,统一格式、自动滚屏
        /// 参数msg:需要打印到日志框的文字内容
        /// </summary>
        private void AddLog(string msg)
        {
            // 获取当前日期作为日志前缀
            string time = DateTime.Now.ToString("yyyy-MM-dd");
            // 拼接完整日志文本,\r\n代表换行,每条日志单独一行
            string logText = $"[{time}]{msg}\r\n";

            // AppendText:在日志框原有文字末尾追加新日志,不会清空历史日志
            rtbLog.AppendText(logText);

            // TextLength 获取日志框全部文字总长度,也就是文本最后一个字符的位置
            // SelectionStart 将光标移动到文本最末尾
            rtbLog.SelectionStart = rtbLog.TextLength;

            // ScrollToCaret:自动滚动滚动条,把最新日志显示在可视区域,不用手动下拉
            rtbLog.ScrollToCaret();
        }

        /// <summary>
        /// 【保存TXT日志】按钮点击事件
        /// 将日志框内所有文字保存到当日文件夹,生成txt文档
        /// try-catch捕获异常,文件保存失败不会直接崩溃程序
        /// </summary>
        private void btnSaveTxtLog_Click(object sender, EventArgs e)
        {
            try
            {
                // 生成文件名:当前日期_运行日志.txt
                string fileName = DateTime.Now.ToString("yyyy-MM-dd") + "_运行日志.txt";
                // 拼接完整保存路径:当日日志文件夹 + 文件名
                string fullPath = Path.Combine(GlobalLogPath, fileName);

                // using语法 + StreamWriter 文件写入流
                // StreamWriter专门用来向本地文件写入文本
                // 参数1 fullPath:文件完整路径
                // 参数2 false:覆盖模式,存在同名文件直接清空重写;true=追加续写
                // 参数3 Encoding.UTF8:文件编码,解决中文乱码
                using (StreamWriter sw = new StreamWriter(fullPath, false, Encoding.UTF8))
                {
                    // Write:把日志框全部文字一次性写入txt文件
                    sw.Write(rtbLog.Text);
                }
                // using代码块结束自动关闭文件、释放资源,不用手动写Close()

                // 弹出提示框告知保存成功,附带文件完整路径
                MessageBox.Show($"TXT日志保存成功!\n路径:{fullPath}", "保存成功");
                // 日志框记录保存操作
                AddLog($"已保存成功文件{fileName}");
            }
            catch(Exception ex)
            {
                // 捕获所有文件操作异常:权限不足、文件夹被删除、文件占用等
                // ex.Message 系统自带错误详情
                MessageBox.Show($"保存失败{ex.Message}", "错误");
            }
        }

        /// <summary>
        /// 【导出CSV测试报告】按钮点击事件
        /// CSV是逗号分隔纯文本表格,Excel可直接打开,无需第三方控件
        /// 将批量测试存储的所有数据导出为表格文件
        /// </summary>
        private void btnSaveCsvReport_Click(object sender, EventArgs e)
        {
            // 判断测试数据列表长度,如果等于0代表没有做过测试,直接弹窗提示并终止方法
            if(testDataList.Count==0)
            {
                MessageBox.Show("暂无数据,请先执行批量测试");
                // return:直接退出当前方法,不再执行下方导出代码
                return;
            }
            try
            {
                // 文件名:时-分-秒_测试报告.csv,精确到秒避免同名覆盖
                string fileName = DateTime.Now.ToString("HH-mm-ss") + "_测试报告.csv";
                // 拼接完整保存路径
                string fullPath = Path.Combine(GlobalLogPath, fileName);

                // 创建文件写入流
                using (StreamWriter sw = new StreamWriter(fullPath,false,Encoding.UTF8))
                {
                    // 写入CSV表头,逗号分隔4列标题,WriteLine自带换行
                    sw.WriteLine("模块编号,电压(V),温度(℃),测试结果");

                    // foreach循环遍历所有测试数据行
                    foreach(string[] dataRow in testDataList )
                    {
                        // 按逗号分割4个单元格写入一行表格数据
                        // dataRow[0]模块编号 dataRow[1]电压 dataRow[2]温度 dataRow[3]结果
                        sw.WriteLine($"{dataRow[0]},{dataRow[1]},{dataRow[2]},{dataRow[3]}");
                    }
                }

                // 导出成功弹窗提示
                MessageBox.Show($"CSV报告导出成功!可使用Excel打开\n路径:{fullPath}", "导出完成");
                AddLog($"已导出CSV测试报告:{fileName}");
            }
            catch(Exception ex)
            {
                // 捕获导出时所有异常并弹窗提示
                MessageBox.Show($"导出失败{ex.Message}", "错误");
            }
        }
    }
}

补充小白必看核心知识点总结

1. using(StreamWriter) 核心好处

文件属于系统资源,打开后会被程序锁定;

using{} 执行完毕自动关闭文件、释放资源,不会出现「文件被占用无法删除/修改」的bug,是C#文件操作标准写法。

2. CSV文件原理

CSV本质纯文本,用英文逗号,区分单元格,Excel原生支持打开,不用安装任何插件,工业上位机导出测试报告首选。

3. Thread.Sleep + Application.DoEvents配套作用

  • Thread.Sleep(200):模拟硬件读取延迟,肉眼看到进度条动态走动
  • Application.DoEvents():休眠时刷新界面,防止窗口变白、拖动卡顿

4. List<string\[\]> 存储逻辑

相当于内存里的临时表格:

  • List = 所有数据行集合
  • string\[\] = 单行4列数据,固定结构方便批量循环导出表格

5. Path.Combine 优势

Windows文件路径分隔符是\,手动拼接容易多写/少写斜杠报错;

Path.Combine自动处理分隔符,跨文件夹拼接不会出错。

6. 定时器Timer逻辑

Interval=1000 → 1000毫秒=1秒执行一次Tick事件,专门用来做状态栏实时时间、设备轮询。

复制代码
# 逐个问题超详细小白拆解讲解
## 1、`ScrollBars = Vertical` 含义
控件是你的日志框 `RichTextBox`
- `ScrollBars` 是滚动条配置属性
- `Vertical` = **只显示垂直滚动条(上下拖动)**
- 配套可选值:
  - `Horizontal`:仅横向滚动条
  - `Both`:横向+纵向都出现
  - `None`:没有滚动条

### 作用
当日志内容太多,超出文本框可视高度时,自动出现**上下拖动的滚动条**查看全部日志;
设置成 `Vertical` 就是禁止横向滚动,排版更整洁,上位机日志框标准写法。

---

## 2、`private List<string[]> testDataList = new List<string[]>();`
### 拆分理解
1. `List<>`:C# 动态泛型集合(动态数组),可以自动增减长度,不用提前固定容量,适合存不确定条数的测试数据
2. `string[]`:字符串数组,一组固定结构文本 `{模块编号,电压,温度,测试结果}`
3. `List<string[]>` = **一个表格结构容器**
类比Excel:
- 外层 `List` = 整张表格所有行
- 内部 `string[]` = 表格**单行4列单元格**

### 举例存入一条数据
```csharp
testDataList.Add(new string[]{"SFP-01","3.25","26.0","合格"});

专门用来缓存批量测试结果,最后统一导出CSV表格用。


3、三行代码逐行解释

csharp 复制代码
progressTest.Value = 0;
testDataList.Clear();
lblTestStatus.Text = "测试:运行中";
  1. progressTest.Value = 0;
    进度条当前数值归0,新一轮测试从头开始走进度,避免上一轮进度残留。
  2. testDataList.Clear();
    清空之前所有旧测试数据,防止本次导出CSV混入上一轮测试记录,保证数据纯净。
  3. lblTestStatus.Text = "测试:运行中";
    修改底部状态栏文字,给用户直观提示:程序正在执行自动测试。

4、三段赋值代码 + ToString("0.0") 格式讲解

csharp 复制代码
string moduleId = $"SFP-{i:D2}";
string volt = (3.2 + i * 0.05).ToString("0.00");
string temp = (25 + i).ToString("0.0");

$"SFP-{i:D2}"

  • i 是循环序号 1、2、3...10
  • :D2 格式化:固定2位数字,不足前面补0
    例:
    i=1 → SFP-01
    i=10 → SFP-10
    作用:模块编号排版整齐。

(3.2 + i * 0.05).ToString("0.00")

  1. 先计算模拟电压:基准3.2V,每多一个模块+0.05V
  2. .ToString("0.00") 数字格式化规则:
    0.00 = 强制保留2位小数,不足补0
    计算结果 3.2 → 转字符串 3.20
    3.251 → 自动四舍五入成 3.25
    适合电压这种高精度参数。

(25 + i).ToString("0.0")

.ToString("0.0") = 强制保留1位小数

计算值 26 → 变成 26.0

27.34 → 四舍五入为 27.3

温度只需要一位小数足够展示,所以格式和电压区分开。

总结:ToString("0.x") 不是单纯转文字,是控制小数位数、统一数据格式,导出CSV表格排版整齐。


5、System.Threading.Thread.Sleep(200);

作用

让当前主线程暂停休眠200毫秒(0.2秒),模拟真实硬件读取数据的耗时。

不加会发生什么?

循环瞬间一口气跑完10次测试,进度条肉眼看不到中间走动过程,直接瞬间拉满100%,看不到动态效果,失去模拟测试的意义。

补充配套代码 Application.DoEvents();

休眠后搭配这句,防止窗口卡顿变白假死,能实时刷新界面、拖动窗口。


6、using(StreamWriter sw=new StreamWriter(fullPath,false,Encoding.UTF8)) 整体拆解

1)StreamWriter

专门用来向本地文件写入文本内容的类,用来创建/覆盖txt、csv文件。

2)三个参数含义

csharp 复制代码
new StreamWriter(文件路径, 是否追加写入, 编码格式)
  1. fullPath:完整保存文件路径
  2. false:写入模式
    • false = 覆盖模式:新建文件,清空原有内容从头写
    • true = 追加模式:在文件末尾续写,不删除旧内容
  3. Encoding.UTF8:指定UTF-8编码,解决中文乱码问题,不写默认编码中文会问号/乱码。

3)using( ){ } 语法核心意义

StreamWriter 操作文件会占用系统文件句柄资源,用完必须释放,否则文件被锁住删不掉、改不了。

using 语法特点:

  • 大括号内执行文件写入逻辑
  • 代码执行出 } 大括号瞬间,自动释放文件资源、自动调用sw.Close()关闭流
    不用你手动写 sw.Dispose(); sw.Close();,杜绝文件占用bug,C#文件操作行业标准写法。

7、整块代码完整含义

csharp 复制代码
using (StreamWriter sw = new StreamWriter(fullPath, false, Encoding.UTF8))
{
    sw.Write(rtbLog.Text);
}
  1. 在指定路径创建/覆盖一个txt文件
  2. 把日志框 rtbLog 里面所有全部文字一次性写入这个文件
  3. 自动关闭文件、释放资源,完成日志本地保存功能

第四阶段:README.md 新增Task03介绍

打开总仓库README,追加第三梯队内容:

markdown 复制代码
### Task03:03_TestFunction_Demo 测试业务功能模块
# 核心学习知识点
1. 界面配套控件:ProgressBar进度条、StatusStrip底部状态栏、ToolTip悬浮提示、Timer定时器
2. 文件系统全套操作:Directory文件夹创建、Path路径处理类
3. StreamWriter 文本文件读写,TXT日志本地持久化保存
4. CSV表格文件导出(无第三方库,Excel兼容,光模块测试报告标准格式)
5. 模拟批量自动测试流程,进度实时同步UI
6. 按日期自动分层归档日志文件,项目标准化存储方案

## 功能说明
1. 底部状态栏实时展示:设备状态、测试运行状态、系统时间
2. 批量自动测试模拟,进度条动态更新测试进度
3. 鼠标悬浮按钮弹出操作提示,优化软件易用性
4. 程序启动自动创建当日日志文件夹,自动规范文件命名
5. 一键保存完整运行日志为TXT文档
6. 一键导出全部测试数据为CSV表格,支持Excel直接编辑查看

后续拓展衔接(第四梯队)

写完这个Demo后,可以继续拓展:

  1. 串口+测试流程整合:串口读取真实硬件数据填入CSV
  2. 测试阈值判断(电压超标标记不合格)
  3. 历史日志读取、查询功能(StreamReader读取旧TXT文件)

需要我把上面完整整合好的无缺失全套代码一次性给你吗?