感谢:
串口助手(简洁版)上位机软件零基础教程( C# + visual studio2017 )(二)_SWPU_机器人实验室-CSDN博客
在上篇博客中,我们完成了串口助手(简洁版)可视化窗体的设计,并且单击启动后可以运行。但是光有外壳,没有灵魂。所以接下来我们将继续一步一步来编写上位机软件的程序部分。
首先,我谈一下自己所理解的C#上位机软件程序编写的中心思想。以串口助手(简洁版)为例,所有程序都是以 “事件” 为核心来进行的,对应的 “事件”发生了,那软件就去执行 我们自己编写的 对应事件中的 程序。
- 按键按下,算一个事件吧 ----> 执行这个按键按下对应的程序;
- 串口突然收到数据 ,算一个事件吧 -----> 执行串口收到数据时对应的程序
- 定时器中断来了,也是个事件 ----> 执行定时器中断发生时对应的程序
不知道这样讲符不符合C#上位机真正的编程方式,但是对于初学者应该会有种豁然开朗的感觉。如果错了,先就错着看吧,哈哈。。。咱们下面就一个事件,一个事件的写代码,代码后都有详细的注释,大家不要心虚。。。。。
1、窗口加载时事件(程序)
窗体刚刚加载出来,就是弹出软件窗口的一瞬间,算个事件吧。那怎么去编写这一段的程序呢?我们到上一章编好的可视化窗体中,去双击窗体的空白处,就跳转到对应的事件发生程序中去了。比如双击这里:
然后就跳转到这个代码段中来了:
、、、 C#
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace 串口助手_简洁版_
{
public partial class Form1 : Form
{
/*默认存在*/
public Form1()
{
InitializeComponent();
}
/*窗体加载时被调用*/
private void Form1_Load(object sender, EventArgs e)
{
}
}
}
函数 Form1_Load() 就会在窗体加载的时候被调用一次。上方的 Form1() 函数会默认存在,暂时不用理会。在其中加入代码:
''' C#
/*窗体加载时被调用*/
private void Form1_Load(object sender, EventArgs e)
{
Updata_Serialport_Name(comboBox1); //调用更新可用串口函数,comboBox1为 端口 组合框名字
radioButton1.Checked = true; //函数中选择发送模式 为“数值”发送模式。 radioButton1为单选按钮属性(name)名字
radioButton3.Checked = true; //函数中选择接收模式 为“数值”接收模式。 radioButton3为单选按钮属性(name)名字
button2.Text = "打开串口"; // 确保 “打开串口”按键文本属性为 “打开串口”
}
/*用户自定义更新可用串口函数*/
private void Updata_Serialport_Name(ComboBox MycomboBox)
{
string[] ArryPort = SerialPort.GetPortNames(); //定义字符串数组,数组名为 ArryPort
//SerialP ort.GetPortNames()函数功能为获取计算机所有可用串口,以字符串数组形式输出
MycomboBox.Items.Clear(); //清除当前组合框下拉菜单内容
for (int i = 0; i < ArryPort.Length; i++)
{
MycomboBox.Items.Add(ArryPort[i]); //将所有的可用串口号添加到 端口 对应的组合框中
}
}
总结:
- 在窗体加载事件中,我们设置 发送和接收模式都默认选择 “”数值“”模式, 同时 调用自定义的串口号更新函数 Updata_Serialport_Name (), 来更新一下 “端口” 组合框下拉菜单中的串口信息。
- SerialPort.GetPortNames() 函数,用来查询所有的可用串口号。
2.定时器中断事件(程序)
上一章在可视化窗体编辑过程中,我们添加并使能了定时器控件,并设定时周期为500ms。所以在程序中,我们需要为其添加定时器中断处理程序。同上一样,我们需要正在GUI窗口中双击Timer图标。便可创建 和 进入 定时器中断事件代码段。
双击此处:
编辑函数如下:
''' C#
/*定时器500ms中断调用*/
private void timer1_Tick(object sender, EventArgs e)
{
Updata_Serialport_Name(comboBox1); //依然是调用更新可用串口函数
//目的是在软件使用过程中,时刻刷新串口信息
}
总结: 间隔500ms产生一次定时器中断事件,在中断事件中调用用户自定义的更新串口号函数,来实现串口号的动态更新。
3.串口开关按键按下事件(程序)
在GUI界面中,我们做了一个“”打开串口“”按键,用于打开上位机串口。同样双击按键图标进入事件对应的程序段。事件对应的代码如下:
''' C#
/*打开串口按键按下调用*/
private void button2_Click(object sender, EventArgs e)
{
if (button2.Text == "打开串口") //如当前是串口设备是关闭状态
{
try //try 是尝试部分 ,如果尝试过程中出现问题,进入 catch部分,执行错误处理代码
{
serialPort1.PortName = comboBox1.Text; //将串口设备的串口号属性设置为 comboBox1复选框中选择的串口号
serialPort1.BaudRate = Convert.ToInt32(comboBox2.Text); //将串口设备的波特率属性设置为 comboBox2复选框中选择的波特率
serialPort1.Open(); //打开串口,如果打开了继续向下执行,如果失败了,跳转至catch部分
comboBox1.Enabled = false; //串口已经打开了,将comboBox1设置为不可操作
comboBox2.Enabled = false; //串口已经打开了,将comboBox2设置为不可操作
button2.BackColor = Color.Red; //将串口开关按键的颜色,改为红色
button2.Text = "关闭串口"; //将串口开关按键的文字改为 “关闭串口”
}
catch
{
MessageBox.Show("打开串口失败,请检查串口", "错误"); //弹出错误对话框
}
}
else //如果当前串口设备是打开状态
{
try
{
serialPort1.Close(); //关闭串口
comboBox1.Enabled = true; //串口已经关闭了,将comboBox1设置为可操作
comboBox2.Enabled = true; //串口已经关闭了,将comboBox2设置为可操作
button2.BackColor = Color.Lime; //将串口开关按键的颜色,改为青绿色
button2.Text = "打开串口"; //将串口开关按键的文字改为 “打开串口”
}
catch
{
MessageBox.Show("关闭串口失败,请检查串口", "错误"); //弹出错误对话框
}
}
}
总结:
- Convert.ToInt32(comboBox2.Text); 意思是将comboBox2中选中的波特率字符串,转化为int 32位的数值数据。
- try 和 catch 是C#中特有的,用来进行错误处理的语句。出错了就跳至catch部分补救处理。如果没有catch,一旦出错程序就崩掉了。
- 打开串口后,串口开关按键的颜色和文字就已经在程序中被修改了,而且还让两个组合框发灰,变成不可操作状态
4.发送按钮按下事件(程序)
当按下串口发送按键后,就需要将发送文本框中的数据通过串口发出。并且要通过判断单选按钮的按下状态来判断该以什么样的数据形式发送。同样双击发送按键 ,进入发送按钮按下代码段,并写入代码如下:
'''C#
/*发送按键按下调用*/
private void button1_Click(object sender, EventArgs e)
{
if (serialPort1.IsOpen) //如果串口设备已经打开了
{
if (radioButton2.Checked) //如果是以字符的形式发送数据
{
char[] str = new char[1]; //定义一个字符数组,只有一位
try
{
for (int i = 0; i < textBox1.Text.Length; i++)
{
str[0] = Convert.ToChar(textBox1.Text.Substring(i, 1)); //取待发送文本框中的第i个字符
serialPort1.Write(str, 0, 1); //写入串口设备进行发送
}
}
catch
{
MessageBox.Show("串口字符写入错误!", "错误"); //弹出发送错误对话框
serialPort1.Close(); //关闭串口
button2.BackColor = Color.Lime; //将串口开关按键的颜色,改为青绿色
button2.Text = "打开串口"; //将串口开关按键的文字改为 “打开串口”
}
}
else //如果以数值的形式发送
{
byte[] Data = new byte[1]; //定义一个byte类型数据,相当于单片机中的 unsigned char 类型
int flag = 0; //定义一个标志,标志这是第几位
try
{
for (int i = 0; i < textBox1.Text.Length; i++)
{
if (textBox1.Text.Substring(i, 1) == " " && flag == 0) //如果是第一位,并且为空字符
{
continue;
}
if (textBox1.Text.Substring(i, 1) != " " && flag == 0) //如果是第一位,但不为空字符
{
flag = 1; //标志转到第二位数据去
if (i == textBox1.Text.Length - 1) //如果这是文本框字符串的最后一个字符
{
Data[0] = Convert.ToByte(textBox1.Text.Substring(i, 1), 16); //转化为byte类型数据,以16进制显示
serialPort1.Write(Data, 0, 1); //通过串口发送
flag = 0; //标志回到第一位数据去
}
continue;
}
else if (textBox1.Text.Substring(i, 1) == " " && flag == 1) //如果是第二位 ,且第二位字符为空
{
Data[0] = Convert.ToByte(textBox1.Text.Substring(i - 1, 1), 16); //只将第一位字符转化为byte类型数据,以十六进制显示
serialPort1.Write(Data, 0, 1); //通过串口发送
flag = 0; //标志回到第一位数据去
continue;
}
else if (textBox1.Text.Substring(i, 1) != " " && flag == 1) //如果是第二位字符,且第一位字符不为空
{
Data[0] = Convert.ToByte(textBox1.Text.Substring(i - 1, 2), 16); //将第一,二位字符转化为byte类型数据,以十六进制显示
serialPort1.Write(Data, 0, 1); //通过串口发送
flag = 0; //标志回到第一位数据去
continue;
}
}
}
catch
{
MessageBox.Show("串口数值写入错误!", "错误");
serialPort1.Close();
button2.BackColor = Color.Lime; //将串口开关按键的颜色,改为青绿色
button2.Text = "打开串口"; //将串口开关按键的文字改为 “打开串口”
}
}
}
}
总结:
- serialPort1.Write(str, 0, 1); 是向串口发送缓冲区中写数据,第一位是待写入数组名,第二位是数组起始位置,第三位是数据长度。
- textBox1.Text.Substring(i, 1);是字符串截取函数,截取完后形成一个子字符串,第一位是首字符地址,第二位是字符串长度。
- 串口发送函数分为按数值方式,和按字符方式。需要根据单选按钮控件的按下状态来决定。
- .数值发送方式写的有点复杂,目的是要从字符串中,两两抠出16进制数据,如0x46,但同时又要考虑空格作为分隔符的存在。虽然代码复杂了,但是相对用户来说,就没有了输入格式要求,随心所欲的输都可以。
5. 串口接收到数据事件(程序)
当串口接收到数据时,会调用串口接收到数据的事件函数,串口接收代码段的打开方式和其他的有所不同,双击图标并不会进行代码段的创建,需要注意一下。这里需要这样来进行创建和打开。
在串口属性窗口中,单击这个像闪电一样的图标。
然后双击 DataREceived 属性栏,才会创建 并打开串口接收代码段:
填入代码如下:
''' C#
/*串口接收到*/
private void serialPort1_DataReceived(object sender, SerialDataReceivedEventArgs e)
{
if (radioButton4.Checked) //如果以字符串形式读取
{
string str = serialPort1.ReadExisting(); //读取串口接收缓冲区字符串
textBox2.AppendText(str + ""); //在接收文本框中进行显示
}
else //以数值形式读取
{
int length = serialPort1.BytesToRead; // 读取串口接收缓冲区字节数
byte[] data = new byte[length]; //定义相同字节的数组
serialPort1.Read(data, 0, length); //串口读取缓冲区数据到数组中
for (int i = 0; i < length; i++)
{
string str = Convert.ToString(data[i], 16).ToUpper(); //将数据转换为字符串格式
textBox2.AppendText("0X" + (str.Length == 1 ? "0" + str + " " : str + " ")); //添加到串口接收文本框中
}
}
}
总结:
1.serialPort1.ReadExisting();以字符串的形式读取串口接收缓冲区内的数据。
2. (str.Length == 1 ? "0" + str + " " : str + " ")三目运算符,跟C语言一样。
6.清空数据按键按下事件(代码)
现在来说这个就比较简单了,直接上代码:
''' C#
/*清除按键按下*/
private void button3_Click(object sender, EventArgs e)
{
textBox2.Text = "";
}
到此为止,串口助手就已经开发完毕。按启动按钮后,可以对软件进行测试。如果想要将软件拷贝出来,直接定位到工程文件夹下的obj目录,将其中的.exe文件拷贝出来即可。
再附上程序整图:
''' C#
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.IO.Ports;
namespace 串口助手_简洁版_
{
public partial class Form1 : Form
{
/*默认存在*/
public Form1()
{
InitializeComponent();
}
/*窗体加载时被调用*/
private void Form1_Load(object sender, EventArgs e)
{
Updata_Serialport_Name(comboBox1); //调用更新可用串口函数,comboBox1为 端口 组合框名字
radioButton1.Checked = true; //函数中选择发送模式 为“数值”发送模式。 radioButton1为单选按钮属性(name)名字
radioButton3.Checked = true; //函数中选择接收模式 为“数值”接收模式。 radioButton3为单选按钮属性(name)名字
button2.Text = "打开串口"; // 确保 “打开串口”按键文本属性为 “打开串口”
}
/*用户自定义更新可用串口函数*/
private void Updata_Serialport_Name(ComboBox MycomboBox)
{
string[] ArryPort = SerialPort.GetPortNames(); //定义字符串数组,数组名为 ArryPort
//SerialP ort.GetPortNames()函数功能为获取计算机所有可用串口,以字符串数组形式输出
MycomboBox.Items.Clear(); //清除复选框
for (int i = 0; i < ArryPort.Length; i++)
{
MycomboBox.Items.Add(ArryPort[i]); //将所有的可用串口号添加到 端口 对应的组合框中
}
}
/*定时器500ms中断调用*/
private void timer1_Tick(object sender, EventArgs e)
{
Updata_Serialport_Name(comboBox1); //依然是调用更新可用串口函数
//目的是在软件使用过程中,时刻刷新串口信息
}
/*开关串口按键按下调用*/
private void button2_Click(object sender, EventArgs e)
{
if (button2.Text == "打开串口") //如当前是串口设备是关闭状态
{
try //try 是尝试部分 ,如果尝试过程中出现问题,进入 catch部分,执行错误处理代码
{
serialPort1.PortName = comboBox1.Text; //将串口设备的串口号属性设置为 comboBox1复选框中选择的串口号
serialPort1.BaudRate = Convert.ToInt32(comboBox2.Text); //将串口设备的波特率属性设置为 comboBox2复选框中选择的波特率
serialPort1.Open(); //打开串口,如果打开了继续向下执行,如果失败了,跳转至catch部分
comboBox1.Enabled = false; //串口已经打开了,将comboBox1设置为不可操作
comboBox2.Enabled = false; //串口已经打开了,将comboBox2设置为不可操作
button2.BackColor = Color.Red; //将串口开关按键的颜色,改为红色
button2.Text = "关闭串口"; //将串口开关按键的文字改为 “关闭串口”
}
catch
{
MessageBox.Show("打开串口失败,请检查串口", "错误"); //弹出错误对话框
}
}
else //如果当前串口设备是打开状态
{
try
{
serialPort1.Close(); //关闭串口
comboBox1.Enabled = true; //串口已经关闭了,将comboBox1设置为可操作
comboBox2.Enabled = true; //串口已经关闭了,将comboBox2设置为可操作
button2.BackColor = Color.Lime; //将串口开关按键的颜色,改为青绿色
button2.Text = "打开串口"; //将串口开关按键的文字改为 “打开串口”
}
catch
{
MessageBox.Show("关闭串口失败,请检查串口", "错误"); //弹出错误对话框
}
}
}
/*发送按键按下调用*/
private void button1_Click(object sender, EventArgs e)
{
if (serialPort1.IsOpen) //如果串口设备已经打开了
{
if (radioButton2.Checked) //如果是以字符的形式发送数据
{
char[] str = new char[1]; //定义一个字符数组,只有一位
try
{
for (int i = 0; i < textBox1.Text.Length; i++)
{
str[0] = Convert.ToChar(textBox1.Text.Substring(i, 1)); //取待发送文本框中的第i个字符
serialPort1.Write(str, 0, 1); //写入串口设备进行发送
}
}
catch
{
MessageBox.Show("串口字符写入错误!", "错误"); //弹出发送错误对话框
serialPort1.Close(); //关闭串口
button2.BackColor = Color.Lime; //将串口开关按键的颜色,改为青绿色
button2.Text = "打开串口"; //将串口开关按键的文字改为 “打开串口”
}
}
else //如果以数值的形式发送
{
byte[] Data = new byte[1]; //定义一个byte类型数据,相当于单片机中的 unsigned char 类型
int flag = 0; //定义一个标志,标志这是第几位
try
{
for (int i = 0; i < textBox1.Text.Length; i++)
{
if (textBox1.Text.Substring(i, 1) == " " && flag == 0) //如果是第一位,并且为空字符
{
continue;
}
if (textBox1.Text.Substring(i, 1) != " " && flag == 0) //如果是第一位,但不为空字符
{
flag = 1; //标志转到第二位数据去
if (i == textBox1.Text.Length - 1) //如果这是文本框字符串的最后一个字符
{
Data[0] = Convert.ToByte(textBox1.Text.Substring(i, 1), 16); //转化为byte类型数据,以16进制显示
serialPort1.Write(Data, 0, 1); //通过串口发送
flag = 0; //标志回到第一位数据去
}
continue;
}
else if (textBox1.Text.Substring(i, 1) == " " && flag == 1) //如果是第二位 ,且第二位字符为空
{
Data[0] = Convert.ToByte(textBox1.Text.Substring(i - 1, 1), 16); //只将第一位字符转化为byte类型数据,以十六进制显示
serialPort1.Write(Data, 0, 1); //通过串口发送
flag = 0; //标志回到第一位数据去
continue;
}
else if (textBox1.Text.Substring(i, 1) != " " && flag == 1) //如果是第二位字符,且第一位字符不为空
{
Data[0] = Convert.ToByte(textBox1.Text.Substring(i - 1, 2), 16); //将第一,二位字符转化为byte类型数据,以十六进制显示
serialPort1.Write(Data, 0, 1); //通过串口发送
flag = 0; //标志回到第一位数据去
continue;
}
}
}
catch
{
MessageBox.Show("串口数值写入错误!", "错误");
serialPort1.Close();
button2.BackColor = Color.Lime; //将串口开关按键的颜色,改为青绿色
button2.Text = "打开串口"; //将串口开关按键的文字改为 “打开串口”
}
}
}
}
/*串口接收到*/
private void serialPort1_DataReceived(object sender, SerialDataReceivedEventArgs e)
{
if (radioButton4.Checked) //如果以字符串形式读取
{
string str = serialPort1.ReadExisting(); //读取串口接收缓冲区字符串
textBox2.AppendText(str + ""); //在接收文本框中进行显示
}
else //以数值形式读取
{
int length = serialPort1.BytesToRead; // 读取串口接收缓冲区字节数
byte[] data = new byte[length]; //定义相同字节的数组
serialPort1.Read(data, 0, length); //串口读取缓冲区数据到数组中
for (int i = 0; i < length; i++)
{
string str = Convert.ToString(data[i], 16).ToUpper(); //将数据转换为字符串格式
textBox2.AppendText("0X" + (str.Length == 1 ? "0" + str + " " : str + " ")); //添加到串口接收文本框中
}
}
}
/*清除按键按下*/
private void button3_Click(object sender, EventArgs e)
{
textBox2.Text = "";
}
}
}
看看最后效果:
重要:如果程序运行时出现线程间访问出错,记得在Form1()函数中加一段代码,即可完美解决:
'''C#
/*默认*/
public Form1()
{
InitializeComponent();
System.Windows.Forms.Control.CheckForIllegalCrossThreadCalls = false;//设置该属性 为false
}