零基础开始学习使用C#,做一个基于串口的配置工具
开发环境:VS2010,Framework.NET 4.0,Windows7 64位
技巧1:启动软件后COM3和波特率的ComboBox怎么改为不可编辑状态?
ComboBox的DropDownStyle属性改为:DropDownList后控件不能被编辑.
技巧2:启动软件后串口号的ComboBox如何列举出当系通存在的串口,并显示最大的串口号?
1) 包含.NET的IO类
using System.IO.Ports; //使用IO口
2) 新建一个枚举串口的函数private bool serial_list(), 注:TS_ComboBox_com即串口号控件, TS_ComboBox_bps即波特率控件
private bool serial_list()
{
//使用系统枚举串口
string[] port = System.IO.Ports.SerialPort.GetPortNames(); //获取存在的串口字符数组
string def_bps = ConfigurationManager.AppSettings["SerialDefBps"];
TS_ComboBox_com.Items.Clear(); //清ComboBox
foreach(string str in port) //将串口号显示到ComboBox
{
TS_ComboBox_com.Items.Add(str);
}
int i = TS_ComboBox_com.Items.Count;
if(0 != i)
{
//有串口则显示最新的一个串口号
//若配置文件已定义波特率填写配置文件的波特率,若无则使用115200
TS_ComboBox_com.Text = TS_ComboBox_com.Items[i - 1].ToString();
if (null != def_bps) TS_ComboBox_bps.Text = def_bps;
else TS_ComboBox_bps.Text = "115200";
return true;
}
else
{
return false;
}
}
技巧3: 怎么将"12345"等字符串转为数值?
程序中会经常用到这些装换,常用的装换有:
int result = 65535; //先定义result未一个已知值
//使用Convert转换字符串
result = Convert.ToInt32(""); //发生异常,后续程序无法执行
result = Convert.ToInt32("12345"); //正常:result=12345
result = Convert.ToInt32("123ab"); //发生异常,后续程序无法执行
result = Convert.ToInt32("ab123"); //发生异常后续程序无法执行
result = Convert.ToInt32("123 a"); //发生异常后续程序无法执行
//使用int.Parse转换字符转
result = int.Parse(""); //发生异常,后续程序无法执行
result = int.Parse("12345"); //正常:result=12345
result = int.Parse("123ab"); //发生异常,后续程序无法执行
result = int.Parse("ab123"); //发生异常,后续程序无法执行
result = int.Parse("123 a"); //发生异常,后续程序无法执行
//使用int.TryParse转换
int.TryParse("", out result); //执行正常:result=0,函数返回false
int.TryParse("12345", out result); //执行正常:result=1235,函数返回true
int.TryParse("123ab", out result); //执行正常:result=0,函数返回false
int.TryParse("ab123", out result); //执行正常:result=0,函数返回false
int.TryParse("123 a", out result); //执行正常:result=0,函数返回false
int.TryParse("1234567890123456789",out result);//明显字符串超出int范围,result=0,函数返回
故而这里建议新手朋友们使用TryParse将字符串转为数值, 还可使用如UInt16.TryParse(), UInt64.TryParse(),float.TryParse()等, 举一反三
技巧4: 如何将数值转为指定格式的字符串,比如数值0x4F转为字符窜"00004F"?
//使用ToString方法:
DateTime dt =new DateTime(2003,6,07);
MessageBox.Show(2.5.ToString("C") + "\n" + //货币型: 显示"¥2.50"
25.ToString("D5") + "\n" + //十进制: 显示"00025"
25000.ToString("E") + "\n" + //科学计数: 显示"2.500000E+005"
25.ToString("F2") + "\n" + //浮点固定: 显示"25.00"
2.5.ToString("G") + "\n" + //常规型: 显示"2.5"
2500000.ToString("N") + "\n" + //N数字: 显示"2,500,000.00"
2500000.ToString("N0") + "\n" + //N数字: 显示"2,500,000"
0xF.ToString("X") + "\n" + //十六进制: 显示"F"
0xFF.ToString("X0") + "\n" + //十六进制: 显示"FF"
0xF.ToString("X4") + "\n" + //十六进制: 显示"000F"
0.123.ToString("p")+ "\n" + //百分比: 显示"12.30%"
0.123.ToString("P0")+ "\n" + //百分比: 显示"12%"
1.123.ToString("P3")+ "\n" + //百分比: 显示"112.230%"
dt.ToString("yy.M.d")+"\n" + //日期: 显示"03.6.7"
dt.ToString("yyyy年MM月dd日")+"\n" + //日期: 显示"2003年6月07日"
""); //十六进制: 显示"000F"
//使用string.Format方法
MessageBox.Show(string.Format("the value is {0,4:d}\n", 122) + //显示:"the value is 122"
string.Format("the value is {0,4:f1}\n", 123.45) + //显示:"the value is 123.4"
string.Format("the value is {0,4:f3}\n", 123.45) + //显示:"the value is 123.450"
string.Format("the value is {0,4:f3}\n", 123.45) + //显示:"the value is 123.450"
string.Format("the value is {0,4:X4}\n", 0x4f) + //显示:"the value is 004F"
"");
技巧5: 如何使用.NET自带的Configuration配置文件功能
然后在需要使用的cs源文件中添加引用:using System.Configuration; //使用配置文件
假设配置文件中有配置项:
在CS源文件中读取将配置文件的配置项:
string def_bps = ConfigurationManager.AppSettings["SerialDefBps"];
注意:若配置文件中不存在配置项SerialDefBps项,而使用了以上调用,则def_bps的值仍是null
技巧6: 如何添加/修改/移除配置文件的配置项
1)判断是否存在配置项"RunMode"
if (ConfigurationManager.AppSettings["RunMode"] == null) { }
2) 不同于读取,当需要添加/修改/移除配置文件的配置项时,需要先定义一个配置项操作对象,才能编辑配置项.
Configuration cfa = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None);
cfa.AppSettings.Settings.Remove("RunMode"); //移除一个配置
cfa.AppSettings.Settings.Add("RunMode", "NEW_TEST"); //新增一个配置
cfa.AppSettings.Settings["RunMode"].Value = "-sa"; //修改一个配置
cfa.Save(ConfigurationSaveMode.Modified); //保存配置
ConfigurationManager.RefreshSection("appSettings"); //更新配置
技巧7: 程序目录下相对路径的子文件夹InfTxt内的文件version.txt显示到不可编辑的textBox内.
//step1: 判断是否存在文件夹,若不存在则创建
if (!System.IO.Directory.Exists(".\\InfoTxt"))
{
System.IO.Directory.CreateDirectory(".\\InfoTxt");
}
//step2: 判断文件是否存在,不存在则创建
if (!File.Exists(".\\InfoTxt\\version.txt"))
{
System.IO.File.CreateText(".\\InfoTxt\\version.txt");
}
//step3: 读取文件信息,并将其显示到TextBox
using (StreamReader sr = new StreamReader(".\\InfoTxt\\version.txt", System.Text.Encoding.Default))
{
string TextStr;
TextStr = sr.ReadToEnd().ToString();
sr.Close();
textBoxDescription.Text = TextStr;
}
顺便textBox的ReadOnly属性设为True时,控件不可编辑.
1) 拖放一个serialport控件到窗体,命名为serialPort.
2) 打开串口处理函数
private void TS_Button_open_Click(object sender, EventArgs e)
{
//检查是否存在合法的串口
if ((0 != TS_ComboBox_com.Items.Count) && (null != TS_ComboBox_com.Text))
{
try
{
//step1: 打开串口
serialPort.PortName = TS_ComboBox_com.Text;
serialPort.BaudRate = int.Parse(TS_ComboBox_bps.Text);
use_bps = serialPort.BaudRate; //标记使用波特率
serialPort.Open();
//step2: 初始化选项卡(我写的初始化代码可能需要几百毫秒执行,让进度条滚动显示一会)
ST_ProgressBar.Style = ProgressBarStyle.Marquee;
SUpdata_init();
SMstCfg_init();
ST_ProgressBar.Style = ProgressBarStyle.Continuous;
//step3: 启动线程程序 <-------------- 此处新增选项卡的处理
if (tabControl_tool.SelectedTab == tabPage1)
{
my_tab = tabPage1;
SUpdata_UartRx_init();
//th_uartRx = new Thread(SUpdata_UartRx_Service);
//th_uartRx.Start(); //启动线程
}
else if (tabControl_tool.SelectedTab == tabPage2)
{
my_tab = tabPage2;
SMstCfg_UartRx_init();
//th_uartRx = new Thread(SMstCfg_UartRx_Service);
//th_uartRx.Start(); //启动线程
}
else
{
my_tab = null;
MessageBox.Show("未定义控制程序!", "错误!", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
//step4: 标记控件
serial_is_open(true); //打开串口后,打开按键需要失效,关闭按键需要有效等操作由此函数完成
}
catch (Exception)
{
serial_is_open(false);
MessageBox.Show("串口可能已被占用!\n请先关闭占用串口的应用程序!", "错误:", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
else
{
MessageBox.Show("未检测到有效的串口!\n请连接串口设备后重试!", "提示:", MessageBoxButtons.OK, MessageBoxIcon.Information);
serial_list(); //枚举串口
}
}
3)关闭串口处理函数
private void TS_Button_close_Click(object sender, EventArgs e)
{
serial_is_open(false);
if (serialPort.IsOpen) //判断串口是否已打开
{
my_tab = null;
//if( (null != th_uartRx) && (th_uartRx.IsAlive) ) th_uartRx.Abort(); //终止线程
try
{
//step1: 打开串口
serialPort.Close();
//step2: 卸载选项卡内容
ST_ProgressBar.Style = ProgressBarStyle.Marquee;
SUpdata_uninit();
SMstCfg_uninit();
ST_ProgressBar.Style = ProgressBarStyle.Continuous;
//step3: 标记控件
serial_is_open(false);
}
catch (Exception)
{
serial_is_open(false);
MessageBox.Show("尝试关闭串口失败!\n请重新启动应用程序!", "错误:", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
else
{
serial_list(); //枚举串口
serial_is_open(false);
}
}
4) serialPort的接收处理函数
先介绍下串口的接收机制, .NET的串口接收已非常智能了, 完成以上代码后,无需其他更改,打开串口后当接收到一帧数据后, 才会触发serialPort_DataReceived接收事件,
注意: 比如设备连续向上位机发送:
第一次发送: 00 1 1 22 33 44 55 66 77 88 99
第二次发送: AA BB .....(共1024字节,小于接收缓冲)
第三次发送: DD EE FF
则serialPort_DataReceived事件会触发三次,每次使用serialPort.Read()方法读取到的数据和发送的数据相同, 无需特意去做帧尾识别了.
serial的处理代码如下:(实现功能: 将接受到的串口数据,根据激活的选项卡交由不同的服务函数执行)
private void serialPort_DataReceived(object sender, SerialDataReceivedEventArgs e)
{
//将数据读出放入数组
int n = serialPort.BytesToRead;
byte[] serial_rx_buffer = new byte[n]; //新建一个接受数组
serialPort.Read(serial_rx_buffer, 0, n); //将串口接收到的数据写入接收数组
//使用处理程序
if (tabPage1 == my_tab) SUpdata_UartRx_Service(serial_rx_buffer);
else if (tabPage2 == my_tab)SMstCfg_UartRx_Service(serial_rx_buffer);
else
{
MessageBox.Show("未定义处理程序,接收到数据:\n" + Serial_Fun.fun_Byte2HexStr(serial_rx_buffer));
}
}
5) serialPort数据发送
串口数据发送就非常简单:发送测试代码:
private void T2_button_next1_Click(object sender, EventArgs e)
{
byte[] serial_tx_buffer = {0,1,2,3,4,5,6,7,8,9};
serialPort.Write("\r\n");
Thread.Sleep(20); //ms级延时,注意sleep的精度只有十几毫秒,抖动较大,除非你的win系统啥软件也不装才能达到几毫秒的精度
serialPort.Write(serial_tx_buffer, 1, 3); //将会发送3个数据 01 02 03
}
string rxd_str = System.Text.Encoding.Default.GetString( buff);//数组装字符串
byte[] rxd_byte = System.Text.Encoding.Default.GetBytes(rxd_str);//字符转数组
注意:以上方法是等效装换,如Byte[] buff=0,0,32,48,49,50,51,32,00,52,53,32,0,0(共14个元素)转为字符串后,rxd_str的长度为14,但无法显示内容(显示效果等效于14个空格)
解决方法:
rxd_str = rxd_str.Replace("\0", " ");//将/0替换为空格
rxd_str = rxd_str.Trim('\0',' ');//分别从头部和尾部开始移除指定的字符直到遇到有效字符位置,使用rxd_str.Trim()不带参数,只会移除空格,带参数能移除指定字符
最终得到的字符串结果为(长度8)"0123 45"
实际情况中设备会返回多行ASCII字符数据, 上位机需要解析时必须分割
String.Split 方法有6个重载函数:
1) public string[] Split(params char[] separator)
2) public string[] Split(char[] separator, int count)
3) public string[] Split(char[] separator, StringSplitOptions options)
4) public string[] Split(string[] separator, StringSplitOptions options)
5) public string[] Split(char[] separator, int count, StringSplitOptions options)
6) public string[] Split(string[] separator, int count, StringSplitOptions options)
下边我们通过一些实例来说明下怎么使用(以下string words = "1,2.3,,4";):
1. public string[] Split(params char[] separator)
注:普通分割
string[] split = words.Split(new Char[] { ',' }); //返回:{"1","2.3","","4"}
string[] split = words.Split(new Char[] { ',', '.' });//返回:{"1","2","3","","4"}
2. public string[] Split(char[] separator, int count)
注:按输出个数分割
string[] split = words.Split(new Char[] { ',', '.' }, 2);//返回:{"1","2.3,,4"}
string[] split = words.Split(new Char[] { ',', '.' }, 6);//返回:{"1","2","3","","4"}
3. public string[] Split(char[] separator, StringSplitOptions options)
注:不保留空元素的分割
string[] split = words.Split(new Char[] { ',', '.' }, StringSplitOptions.RemoveEmptyEntries);
//返回:{"1","2","3","4"} 不保留空元素
string[] split = words.Split(new Char[] { ',', '.' }, StringSplitOptions.None);
//返回:{"1","2","3","","4"} 保留空元素
4. public string[] Split(string[] separator, StringSplitOptions options)
string[] split = words.Split(new string[] { ",", "." }, StringSplitOptions.RemoveEmptyEntries);
//返回:{"1","2","3","4"} 不保留空元素
string[] split = words.Split(new string[] { ",", "." }, StringSplitOptions.None);
//返回:{"1","2","3","","4"} 保留空元素
5. public string[] Split(char[] separator, int count, StringSplitOptions options)
string[] split = words.Split(new Char[] { ',', '.' }, 2, StringSplitOptions.RemoveEmptyEntries);
//返回:{"1","2.3,,4"} 不保留空元素
string[] split = words.Split(new Char[] { ',', '.' }, 6, StringSplitOptions.None);//返回:{"1","2","3","","4"} 保留空元素
6. public string[] Split(string[] separator, int count, StringSplitOptions options)
string[] split = words.Split(new string[] { ",", "." }, 2, StringSplitOptions.RemoveEmptyEntries);//返回:{"1","2.3,,4"} 不保留空元素
string[] split = words.Split(new string[] { ",", "." }, 6, StringSplitOptions.None);
//返回:{"1","2","3","","4"} 保留空元
举例:从以下tst_str字符串中提取出设备的MAC地址的字符串
//目标字符串(需要提取的结果"8C-61-5F-1F-34-34")
string tst_str = " Chip:NetCard 100Mbps Ethernet\r\n" +
"\tMAC: 8C-61-5F-1F-34-34\r\n" +
"\tIP: 192.168.1.63\r\n" +
"\tMask: 255.255.255.0\r\n" +
"\tWay: 192.168.1.1\r\n";
//step1: 设定起始和结束字符串
//step1: 检测起始字符"MAC:"
//step2: 检测结束字符从"MAC:"开始,遇到的第一个"\r\n"字符
//step3: 计算从"MAC:"字符开始的字节位置(idx_start)
//step4: 计算从"MAC:"开始到"\r\n"之前的字符数(idx_len)
//step5: 扣除"MAC:"输出子字符串" 8C-61-5F-1F-34-34"
//step6: 移除结果字符串前后可能存在的空格和制表符
string str_start="MAC:"; //起始字符串
string str_endof="\r\n"; //结束字符串
string out_str=""; //定义结果字符串
int idx_start = tst_str.IndexOf(str_start); //检测匹配到的"MAC:"的字节位置
int idx_len = tst_str.IndexOf(str_endof, idx_start) - idx_start;
if ((idx_start >= 0) && (idx_len >= str_start.Length))
{
out_str = tst_str.Substring(idx_start + str_start.Length, idx_len - str_start.Length);
out_str = out_str.Trim(' ', '\t'); //去掉头尾可能存在的空格和制表符
MessageBox.Show(out_str + "\n字符数:" + out_str.Length.ToString());
}
else
{
MessageBox.Show("未检测到目标字符串!");
}
在C语言中有sprintf函数非常好用,而C#语言的实现方式是:
int a = 123, b = 456;
MessageBox.Show(String.Format("a={0:d4},b={1:d4}",a,b)); //结果:"a=0123,b=0456"
MessageBox.Show(String.Format("a={0:d},b={1:d}", a, b)); //结果:"a=123,b=456"
再附上零占位符合空格占位符的实现代码
string.Format("{0:0000.00}", 12394.039) 结果为:12394.04
string.Format("{0:0000.00}", 194.039) 结果为:0194.04
string.Format("{0:###.##}", 12394.039) 结果为:12394.04
string.Format("{0:####.#}", 194.039) 结果为:194
1) 将"MAC: 8-61-5F-1F-34-34YYY"字符串中提取数值08 61 5F 1F 34 34这6个16进制元素到Byte[]数组:
注:每个16进制元素在字符串中占用1或2个数值字符
原理:从左倒要逐个读取数值字符(0-9,a-f,A-F)将其存入string tmp_str中,遇到下一个非数值字符或读取完毕后将tmp_str转为byte,动态添加到List
public static byte[] fun_HexStr2Bytes(string in_str)
{
string tmp_str = null;
List byte_buff = new List();
//解析字符中存在的数字字符
foreach (char c in in_str)
{
if (((c >= '0') && (c <= '9')) || ((c >= 'a') && (c <= 'f')) || ((c >= 'A') && (c <= 'F')))
{
tmp_str += c;
}
else if ( (1 == tmp_str.Length) || (2 == tmp_str.Length) )
{
byte tmp_byte = byte.Parse(tmp_str, System.Globalization.NumberStyles.HexNumber);
byte_buff.Add(tmp_byte);
tmp_str = null; //处理完后清string
}
else tmp_str = null;
}
//继续解析未处理的数字字符串
if ( (1 == tmp_str.Length) || (2 == tmp_str.Length) )
{
byte tmp_byte = byte.Parse(tmp_str, System.Globalization.NumberStyles.HexNumber);
byte_buff.Add(tmp_byte);
tmp_str = null; //处理完后清string
}
//返回数值
if (byte_buff.Count > 0)
{
byte[] out_bytes = new byte[byte_buff.Count];
byte_buff.CopyTo(out_bytes);
return out_bytes;
}
else
{
return null;
}
}
原理和上面的相同,放代码:
public static int[] fun_DecStr2Ints(string in_str)
{
string tmp_str = null;
List int_buff = new List();
//解析字符中存在的数字字符
foreach (char c in in_str)
{
if ( (c >= '0') && (c <= '9') )
{
tmp_str += c;
}
else if (tmp_str.Length > 0)
{
int tmp_int = int.Parse(tmp_str);
int_buff.Add(tmp_int);
tmp_str = null; //处理完后清string
}
else tmp_str = null;
}
//继续解析未处理的数字字符串
if (tmp_str.Length > 0)
{
int tmp_int = int.Parse(tmp_str);
int_buff.Add(tmp_int);
tmp_str = null; //处理完后清string
}
//返回数值
if (int_buff.Count > 0)
{
int[] out_ints= new int[int_buff.Count];
int_buff.CopyTo(out_ints);
return out_ints;
}
else
{
return null;
}
}