今日继续完善更新我的上位机助手,这次完善多字符串发送的部分:
目前上位机助手支持以下功能:
1、 普通的16进制\ASCLL显示收发
2、 全页更新HEX显示(会自动断串口)
3、 日志辅助显示报错
4、 必要的清除日志区、接收区的逻辑
5、 后台线程刷新查找串口,自动选择CH340串口
6、 串口数据绘图,单位1ms,精度0.0001ms
7、 多字符串发送(单击编辑,双击发送),发送计数成功次数
8、 多字符串文本可以通过写入\读取文件进行加载
文章提供完整代码解释、设计点解释、测试效果图、测试工程下载
目录
开发学习目标:
动态添加TextBox到Panel:
打开Panel控件的AutoScroll属性:
编写代码:
效果如下:
TextBox的双击事件生成:
效果如下:
保存所有TextBox到文件:
加载所有TextBox到进程:
完整测试效果图如下:
1、最初打开状态:
2、初始化加载多字符串:
3、加载之前保存的条目状态:
4、修改后保存目前条目状态:
5、先全删,再加载上次条目状态:
测试工程下载:
遇到的问题记录:
解决后的源码如下:
开发学习目标:
完善之前写的上位机助手,可以在页面中生成众多TextBox控件,双击即可发送数据
动态添加TextBox到Panel:
打开Panel
控件的AutoScroll
属性:
AutoScroll
属性被设置为true,
允许Panel
在内容超出其可视范围时显示滚动条。
这里我设置了一个加载按键,在它按下后开始加载界面的Textbox组件:
编写代码:
在点击事件中添加如下代码:
//开始加载多字符串发送助手 private void Load_multi_textbox_Click(object sender, EventArgs e) { int yPosition = 0; // 初始Y位置 int textBoxHeight = 25; // 假设每个TextBox的高度为20 for (int i = 0; i < 36; i++) // 假设我们要添加10个TextBox { System.Windows.Forms.TextBox textBox = new System.Windows.Forms.TextBox(); textBox.Text = $"TextBox {i + 1}"; // 为TextBox设置文本 textBox.Location = new Point(10, yPosition); // 设置TextBox的位置 textBox.Width = 1144; // 设置TextBox的宽度 textBox.Height = textBoxHeight; // 设置TextBox的高度 panel1.Controls.Add(textBox); // 将TextBox添加到Panel中 yPosition += textBoxHeight + 5; // 更新Y位置,为下一个TextBox留出空间 } // 如需要可调整Panel的大小确保所有TextBox都可见 // 由于设置了AutoScroll,这一步通常是可选的 // 例如:panel1.Height = yPosition; }
效果如下:
点击按钮后:
TextBox的双击事件生成:
只需添加在循环中为每个
TextBox
控件添加一个事件处理程序。由于事件处理程序需要引用到具体的TextBox
控件,可以使用匿名方法或 lambda 表达式来捕获当前的TextBox
实例这个代码我并未将双击事件的调用写成串口发送,而是打印哪个编号的TextBox被双击来测试代码是否正常运行:
//开始加载多字符串发送助手 private void Load_multi_textbox_Click(object sender, EventArgs e) { int yPosition = 0; // 初始Y位置 int textBoxHeight = 25; // 假设每个TextBox的高度为20 for (int i = 0; i < 36; i++) // 假设我们要添加10个TextBox { System.Windows.Forms.TextBox textBox = new System.Windows.Forms.TextBox(); textBox.Text = $"TextBox {i + 1}"; // 为TextBox设置文本 textBox.Location = new Point(10, yPosition); // 设置TextBox的位置 textBox.Width = 1144; // 设置TextBox的宽度 textBox.Height = textBoxHeight; // 设置TextBox的高度 // 添加双击事件处理程序 textBox.DoubleClick += (senderTextBox, eArgs) => { // 但由于lambda表达式的作用域,它实际上捕获了当前迭代的textBox实例 MessageBox.Show($"Double-clicked: {textBox.Text}"); // 显示被双击的TextBox的文本 }; panel1.Controls.Add(textBox); // 将TextBox添加到Panel中 yPosition += textBoxHeight + 5; // 更新Y位置,为下一个TextBox留出空间 } // 如需要可调整Panel的大小确保所有TextBox都可见 // 由于设置了AutoScroll,这一步通常是可选的 // 例如:panel1.Height = yPosition; }
效果如下:
双击不同的Textbox,打印它们各自的编号:
保存所有TextBox到文件:
由于我是在循环中动态创建
TextBox
控件,并且每个TextBox
都没有被单独存储为一个变量(除了循环中的临时变量textBox
),因此直接通过变量名来访问每个TextBox
的内容是不可行的我选择遍历panel控件中的所有textbox来进行保存:
这里如果发现没有这个文本文件,那就会自己创建一个进行保存
//保存条目输入状态 private void Save_multi_textbox_Click(object sender, EventArgs e) { // 定义文件路径 string filePath = "Multi_String.txt"; // 可选:显式地清空文件内容(实际上不需要,因为接下来会用StreamWriter覆盖) // File.WriteAllText(filePath, string.Empty); // 使用StringWriter来构建要写入文件的字符串(可选,但更灵活) StringWriter stringWriter = new StringWriter(); foreach (Control control in panel1.Controls) { // 检查控件是否是TextBox if (control is System.Windows.Forms.TextBox textBox) { // 将TextBox的内容写入StringWriter // stringWriter.WriteLine(textBox.Text); // 或者,如果您想要一些前缀或格式 stringWriter.WriteLine($"TextBox Content:{textBox.Text}"); } } // 现在,将StringWriter的内容一次性写入文件 // 注意:这实际上会覆盖文件,所以不需要先清空文件 File.WriteAllText(filePath, stringWriter.ToString()); // 可选:显示消息框通知用户 MessageBox.Show("所有TextBox内容已成功保存到Multi_String.txt文件中!"); // 释放StringWriter资源(虽然在这个简单的例子中,它会在GC时被自动处理) stringWriter.Dispose(); }
加载所有TextBox到进程:
依旧是遍历所有Panel进行遍历并用文本中内容进行覆盖,这里我之前的保存文件代码中添加了标识头,需要去除:
// 去除前缀(如果存在) if (line.StartsWith("TextBox Content:")) { line = line.Substring("TextBox Content:".Length).Trim(); }
完整代码如下:
//加载条目状态: private void Load_Muiti_string_Click(object sender, EventArgs e) { // 定义文件路径 string filePath = "Multi_String.txt"; // 尝试读取文件 if (File.Exists(filePath)) { int textBoxIndex = 0; // 用于跟踪当前应该填充哪个TextBox的索引 using (StreamReader reader = new StreamReader(filePath)) { string line; while ((line = reader.ReadLine()) != null) { // 去除前缀(如果存在) if (line.StartsWith("TextBox Content:")) { line = line.Substring("TextBox Content:".Length).Trim(); } // 尝试将读取的文本设置回TextBox(如果索引在范围内) if (textBoxIndex < panel1.Controls.Count && panel1.Controls[textBoxIndex] is System.Windows.Forms.TextBox textBox) { textBox.Text = line; } // 移动到下一个TextBox textBoxIndex++; // 如果TextBox的数量少于文件中的行数,则停止读取(可选) // if (textBoxIndex >= panel1.Controls.OfType<TextBox>().Count()) break; } } } else { MessageBox.Show("文件Multi_String.txt不存在!"); } }
完整测试效果图如下:
1、最初打开状态:
2、初始化加载多字符串:
3、加载之前保存的条目状态:
4、修改后保存目前条目状态:
5、先全删,再加载上次条目状态:
测试工程下载:
https://download.csdn.net/download/qq_64257614/89642319
遇到的问题记录:
这是我想在串口接收中断进行一系列数据处理分流遇到的问题,
我想在串口接收中断中进行以下操作:
1、在串口缓冲区按格式处理数据,处理好的存在一个字典列表中,供表格曲线控件打印
2、原始数据根据勾选框来转化为ASCLL或HEX形式打印在接收区文本框中
起初,我直接想在串口接收中断这个线程直接更新文本框UI的内容,但不行,因为串口接收中断线程与文本框UI线程不属于一个线程,不能直接互相传递函数变量等:
因此需要使用一些方法来确保我对UI的更新,是在UI线程上进行:
在
serialPort1_DataReceived
事件处理程序中,使用Invoke
或BeginInvoke
来确保 UI 更新在正确的线程(UI 线程)上执行起初我使用了
Invoke方法,因为比较简单,是同步更新的逻辑,不需要额外写函数委托,可以正常更新UI的文本内容
但是后来,在我频繁开关串口时发生了以下报错:
这导致了程序的卡死!
System.ObjectDisposedException:“无法访问已释放的对象。 ObjectDisposed_ObjectName_Name” Invoke((MethodInvoker)delegate {Serial_Receiving_area.AppendText(displayText); });发生在我频繁开关串口时
在串口接收逻辑中,当串口被关闭时,如果仍然有数据在
serialPort1_DataReceived
事件处理程序中处理,或者程序试图从已经关闭的串口读取数据,这可能会导致程序挂起或卡死。
System.ObjectDisposedException
异常通常发生在尝试访问一个已经被释放(或销毁)的对象时。在串口通信的上下文中,这通常意味着在serialPort1_DataReceived
事件处理程序中,SerialPort
对象sp
或其他相关对象(如 UI 控件)在事件处理过程中被关闭了这里的底层逻辑是:
使用
Control.Invoke
或Control.BeginInvoke
方法来在 UI 线程上执行对控件的更新。这两个方法都用于在控件的拥有线程(即 UI 线程)上执行代码委托。
Invoke
是同步的,它会等待委托执行完毕才继续执行后面的代码;而
BeginInvoke
是异步的,它会立即返回,让委托在后台执行。因此在这个接收中断里更新UI文本显示的逻辑里,明显是
BeginInvoke
更适合这个委托调用
解决后的源码如下:
//串口接收逻辑 private void serialPort1_DataReceived(object sender, SerialDataReceivedEventArgs e) { SerialPort sp = (SerialPort)sender; string indata = sp.ReadExisting(); // 读取所有可用的数据 string[] lines = indata.Split('\n'); bool autoLinefeed = Auto_linefeed.Checked; // 获取Auto_linefeed的选中状态 // 这是你想要显示的文本 // string displayText = "Hello, World!"; if (!isSerialPortOpen) { return; }// 如果串口已关闭,则直接返回 //Receiving_Area_Write(); foreach (string line in lines) { userCurve.Curve_Main(line); } // 直接在UI线程上更新文本区(假设这个方法在UI线程上被调用) 或者使用Invoke/BeginInvoke来确保在UI线程上执行 // 遍历每一行数据 foreach (string line in lines) { userCurve.Curve_Main(line); // 处理每行数据(假设这个方法不需要UI线程) if (HEX_radio.Checked) { // 将字符串转换为字节数组,然后转换为十六进制字符串 // 这里对每行数据单独进行转换 byte[] bytes = Encoding.Default.GetBytes(line); string hexLine = UserInterface.byteToHexStr(bytes); if (Showtime_check.Checked) { displayText = "[" + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + "]收→" + hexLine; } else {displayText = hexLine; } } else if (ASCII_radio.Checked) { if (Showtime_check.Checked) {displayText = "[" + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + "]收→" + line;} else { displayText = line; } } // 如果启用了自动换行,则在文本末尾添加换行符 if (autoLinefeed){ displayText += "\r\n"; } //使用Invoke来确保在UI线程上更新文本区 //Invoke((MethodInvoker)delegate { Serial_Receiving_area.AppendText(displayText); }); UpdateTextBox(displayText); } } // Serial_Receiving_area 是一个 TextBox 控件 // 定义一个方法,用于在 UI 线程上安全地更新文本框 private void UpdateTextBox(string text) { if (Serial_Receiving_area.InvokeRequired) { // 如果需要跨线程操作,则使用 Invoke 来在 UI 线程上执行 Serial_Receiving_area.BeginInvoke(new Action<string>(UpdateTextBox), text); } else { // 直接在 UI 线程上更新文本框 Serial_Receiving_area.AppendText(text); } }