完整分步教学:第三梯队项目 03_TestFunction_Demo
一、整体规划
1. 文件夹命名(遵循你仓库序号规范)
总仓库根目录:SFP-Test-UI-Learning
新建文件夹:03_TestFunction_Demo
用途:进度条、状态栏、文件读写、CSV测试报告导出
2. 项目定位
基于前两阶段知识(布局容器+串口),新增测试业务配套功能 ,不依赖硬件,纯代码可自测;
可两种开发方式:
- 独立新项目(推荐,知识点隔离,学习清晰)
- 直接在
02_SerialPort_LearnDemo扩展(适合整合完整上位机)
下面以独立新项目教学,兼容VS2019 .NET Framework 4.8
3. 最终实现全部功能清单
- 界面新增控件:
ProgressBar进度条、StatusStrip底部状态栏、ToolTip悬浮提示 - 模拟批量自动测试,进度条自动走完
- 状态栏实时显示:串口状态、当前时间、测试状态
- Path类自动按日期创建日志文件夹、自动生成文件名
- StreamWriter 读写TXT日志文件
- 原生代码导出CSV测试报告(无第三方库,光模块行业通用)
第一阶段:新建项目 + 拖拽布局(复用阶段1布局知识)
步骤1:新建项目
- VS2019 创建「Windows窗体应用(.NET Framework)」
- 项目名:
03_TestFunction_Demo - 存放路径:
SFP-Test-UI-Learning\03_TestFunction_Demo - 项目属性修改:
- 程序集名称:
_03_TestFunction_Demo - 默认命名空间:
_03_TestFunction_Demo
- 程序集名称:
步骤2:拖拽全套控件(结合Dock/Anchor布局)
1)底部状态栏 StatusStrip
- 工具箱拖
StatusStrip到窗体,默认Dock=Bottom - 点击状态栏右侧下拉,添加3个
ToolStripLabel- Name:lblDevStatus,Text:设备:未连接
- Name:lblTestStatus,Text:测试:空闲
- Name:lblTime,Text:时间:00:00:00
- 新增
Timer控件(后台实时刷新时间)- Name:timerStatus,Interval=1000(1秒刷新一次),Enabled=true
一步一步跟着操作,超详细图文式步骤,你现在空白窗体直接照着点
第一步:操作底部已经出现的 StatusStrip(状态栏)
你截图左下角已经自动出现 statusStrip1 控件,就是底部那条细长条:
- 用鼠标左键单击选中窗体最下方这个 StatusStrip 长条
- 你会看到长条最右侧出现一个小小的 ▼ 下拉箭头,点一下这个下拉小三角
- 在弹出的下拉菜单里,选择第一项:StatusLabel(也就是 ToolStripLabel)
👉 添加第一个标签并改属性
- 点完之后状态栏多出一块文字区域,选中这个新添加的标签
- 看窗口右侧【属性】面板 ,修改两个值:
Name属性:改成lblDevStatusText属性:改成设备:未连接
👉 添加第二个标签
再次点状态栏右侧的 ▼ 下拉箭头 → 继续选 StatusLabel
修改属性:
Name:lblTestStatusText:测试:空闲
👉 添加第三个标签
第三次点下拉 ▼ → 选 StatusLabel
修改属性:
Name:lblTimeText:时间:00:00:00
做完效果:状态栏从左到右并排三个文字标签。
第二步:添加 Timer 定时器控件(用来每秒刷新时间)
- 看软件最左边侧边栏,找到 工具箱 (你截图左边能看到「工具箱」三个字)
如果工具箱隐藏了:顶部菜单视图 → 工具箱调出来 - 在工具箱里面找到分类:组件
- 在组件列表里找到
Timer,鼠标按住它,拖到窗体任意空白位置松开
✅ 注意:Timer 是后台不可见控件,拖动后会自动出现在窗体最下方组件托盘区,不会画在窗体表面
修改 Timer 属性
选中下方托盘里刚放的 timer1,右侧属性面板修改:
Name:改为timerStatusInterval:填1000(单位毫秒,1000ms = 1秒执行一次)Enabled:设置为True(程序一运行就自动启动计时)
第三步:给Timer写刷新时间的事件(后续代码要用)
- 双击下方托盘里的
timerStatus控件 - VS会自动跳转到代码页面,生成
timerStatus_Tick空方法,后面我们直接粘贴刷新时间代码即可
补充小避坑
- 如果看不到属性面板:顶部菜单
视图 → 属性窗口(快捷键 F4) - Name 是后台代码名字,大小写不能写错;Text 是界面显示文字
- StatusStrip 默认 Dock=Bottom,会一直吸附在窗体最底部,窗口缩放位置不会乱
完整分步拖拽布局(第三梯队项目 03_TestFunction_Demo)
顺着你已经做好的底部StatusStrip+Timer继续往下做,全程复用第一阶段 Dock / Anchor 布局知识点,做完界面再写代码。
整体窗体分层规划(从上到下)
- 顶部按钮区域(FlowLayoutPanel)
- 进度条区域(Panel + ProgressBar)
- 主体日志显示区域(SplitContainer + RichTextBox)
- 底部状态栏(你已经做完的StatusStrip)
步骤1:放置顶部按钮容器 FlowLayoutPanel
-
左侧工具箱找到
FlowLayoutPanel,拖到窗体空白处 -
选中这个控件,右侧属性修改:
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
-
工具箱拖一个
Panel放到窗体(会自动排在按钮面板下方)
属性设置:Name = panelProgress
Dock = Top
Height = 60 -
点击进入 panelProgress 内部,拖拽
ProgressBar进度条控件
属性设置:Name = progressTest
Dock = Fill
Margin = 10
Minimum = 0
Maximum = 100
Value = 0
效果:进度条四周留边、铺满整个Panel,窗口拉伸自动跟着变宽。
步骤3:主体日志区域 SplitContainer(剩余全部窗口空间)
-
工具箱拖拽
SplitContainer到窗体,默认自动填充剩余空间
属性设置:Name = splitMain
Dock = Fill
Orientation = Horizontal (上下分割)
SplitterDistance = 80
- 上半部分:预留以后放测试参数显示(本次暂时不用)
- 下半部分:放日志框
在 splitMain.Panel2 下半区放日志框
-
点开 Panel2,拖拽
RichTextBox
属性设置:Name = rtbLog
Dock = Fill
ReadOnly = True
ScrollBars = Vertical
ReadOnly=True 禁止用户手动修改日志内容,只能程序追加写入。
步骤4:添加 ToolTip 悬浮提示控件
-
工具箱 → 组件分类,找到
ToolTip,拖动到窗体任意位置 -
它会自动跑到窗体下方组件托盘,修改名称:
Name = toolTipHelp
给三个按钮绑定悬浮提示
- 选中
btnStartTest,右侧属性找到ToolTip on toolTipHelp,填入:
模拟多通道SFP自动测试,进度条实时更新测试进度 - 选中
btnSaveTxtLog,填入:
将本次运行全部日志保存到按日期分类的本地文件夹 - 选中
btnSaveCsvReport,填入:
导出本次所有模块电压、温度测试数据,生成Excel可打开的CSV表格
运行后鼠标悬停按钮,自动弹出说明文字,提升软件易用性。
布局完成整体结构核对
Form1 主窗口
├─ flowBtnTop(Dock=Top) 三个功能按钮
├─ panelProgress(Dock=Top)进度条
├─ splitMain(Dock=Fill) 上下分割容器
│ ├─ Panel1 预留参数区
│ └─ Panel2 → rtbLog 日志框
└─ statusStrip1(Dock=Bottom) 状态栏(你已完成)
下方组件托盘:timerStatus、toolTipHelp
下一步安排
布局全部拖拽完成后,我给你完整带逐行详细注释的 Form1.cs 全部代码,包含:
- 自动创建日志文件夹(Path、Directory文件操作)
- Timer状态栏时间刷新逻辑
- 批量测试+进度条走动模拟
- TXT日志保存代码
- CSV报告导出代码
你现在拖拽过程遇到哪个控件找不到、属性不会改,随时截图发我,我当场给你指点。
需要我现在直接把完整可粘贴的代码提前发给你吗?
2)进度条 ProgressBar
- 拖拽
PanelDock=Top,Height=60 - Panel内拖
ProgressBar- Name:progressTest
- Dock=Fill,Margin=10
- Minimum=0,Maximum=100,Value=0
3)功能按钮区(FlowLayoutPanel)
FlowLayoutPanel Dock=Top,Height=100
依次放入按钮:
- btnStartTest:开始批量测试
- btnSaveTxtLog:保存日志TXT
- 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 = "";
知识点讲解
Path.Combine():自动处理路径斜杠\,不会出现D:\\Logs\2026这种斜杠错乱问题Directory.Exists()判断文件夹是否存在;Directory.CreateDirectory()创建文件夹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();
}
第三阶段:自测运行步骤(不用硬件,纯软件模拟)
- 运行程序,自动在exe目录生成
Logs/当日日期文件夹 - 鼠标悬浮按钮,查看ToolTip悬浮提示
- 底部状态栏自动刷新系统时间
- 点击【开始批量测试】:进度条逐步填满,生成10组SFP模拟数据
- 点击【保存日志TXT】:日志框全部内容保存到当日文件夹
- 点击【导出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 = "测试:运行中";
progressTest.Value = 0;
进度条当前数值归0,新一轮测试从头开始走进度,避免上一轮进度残留。testDataList.Clear();
清空之前所有旧测试数据,防止本次导出CSV混入上一轮测试记录,保证数据纯净。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")
- 先计算模拟电压:基准3.2V,每多一个模块+0.05V
.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(文件路径, 是否追加写入, 编码格式)
fullPath:完整保存文件路径false:写入模式false= 覆盖模式:新建文件,清空原有内容从头写true= 追加模式:在文件末尾续写,不删除旧内容
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);
}
- 在指定路径创建/覆盖一个txt文件
- 把日志框
rtbLog里面所有全部文字一次性写入这个文件 - 自动关闭文件、释放资源,完成日志本地保存功能
第四阶段: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后,可以继续拓展:
- 串口+测试流程整合:串口读取真实硬件数据填入CSV
- 测试阈值判断(电压超标标记不合格)
- 历史日志读取、查询功能(StreamReader读取旧TXT文件)
需要我把上面完整整合好的无缺失全套代码一次性给你吗?