在使用C#开发winform应用程序时,经常会碰到对控件跨线程访问造成的异常。在winform中UI线程和工作线程是分开的,但在实际使用中经常会需要在工作线程更新UI线程中创建的控件。
方法1:禁用跨线程访问控件检测
.NET默认开启了禁止跨线程控件访问,在程序中将其置为false取消跨线程访问检测即可实现跨线程访问。
代码中添加如下代码:
Control.CheckForIllegalCrossThreadCalls = false;
备注:该方法虽然可以实现跨线程访问,但同时也取消了线程之间冲突访问的检查,因此可能会存在多个线程对同一控件进行同时访问,此时该控件的值难以预料。因此,在实际使用非线程安全,不推荐使用!
方法2:使用delegate和Invoke/BeginInvoke
Invoke是同步的,它会等待工作线程完成
BeginInvoke是异步的,它会创建另外一个线程去完成工作线程
在使用委托时分为3步(委托有C语言中函数指针的意味):
定义声明委托--->实例化委托--->调用委托
1)定义声明委托
修饰符 delegate 返回值类型 委托名 ( 参数列表 );
2)实例化委托
委托名 委托对象名 = new 委托名 ( 方法名 );
委托中的方法民对应的方法的返回值和参数列表的类型和数目必须一致。
3)调用委托
委托对象名 ( 参数列表 );
#define USE_DELEGATE
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 SerialPort_CRC8_CRC16
{
public partial class Form1 : Form
{
#if USE_DELEGATE
/* 自定义委托,用于实现跨线程对控件的内容更新 */
public delegate void MyControlInvoke(Control sender, string data);
/// <summary>
/// 更新控件内容
/// </summary>
/// <param name="control"></param>
/// <param name="data"></param>
public void UpdataControl(Control control, string data)
{
//判断调用UpdataControl方法的线程和控件线程是否相同,不同则需使用Invoke方法,相同则直接操作
if (control.InvokeRequired == true)
{
MyControlInvoke myControlInvoke = new MyControlInvoke(UpdataControl);
//方法1:异步执行
this.BeginInvoke(myControlInvoke, new object[] { control, data });
//方法2:同步执行
//this.Invoke(myControlInvoke, new object[] { control, data });
}
else
{
control.Text = data;
}
}
#endif
/// <summary>
/// Form1类的构造函数
/// </summary>
public Form1()
{
InitializeComponent();
serialPort1.DataReceived += new System.IO.Ports.SerialDataReceivedEventHandler(serialPort1_DataReceived);
cbxPortNum.DataSource = System.IO.Ports.SerialPort.GetPortNames();
btnOprtPort.BackColor = Color.OrangeRed;
#if !USE_DELEGATE
Control.CheckForIllegalCrossThreadCalls = false; //不使用委托时关闭跨线程调用检测(非线程安全)
#endif
rbtnCRC16.Checked = true;
}
byte[] myDataByte = new byte[] { }; //发送数据缓冲区
private void Calculate_CRC16()
{
CRC16 crc16 = new CRC16();
string[] myDataStr = tBox1.Text.Split(' ');
List<byte> Arc = new List<byte>();
foreach (var item in myDataStr)
{
try
{
Arc.Add(Convert.ToByte(item, 16));
}
catch (Exception)
{
MessageBox.Show("请输入以空格为间隔的16进制字符串!");
return;
}
}
myDataByte = Arc.ToArray();
tBox2.Clear();
/* 发送该指令至TDM表头读取类别量程信息
* 发送:xx 03 00 01 00 01 crcL crcH
* 接收:xx 03 02 class range crcL crcH
*/
UInt16 crc16Res = crc16.Crc16_Modbus(myDataByte, (uint)myDataByte.Length); //计算CRC校验和,crc_Data的计算结果为低字节在高位,高字节在低位
Arc.Add((byte)(crc16Res >> 8));
Arc.Add((byte)(crc16Res));
myDataByte = Arc.ToArray();
sendDataCount = Arc.Count;
foreach (var item in myDataByte)
{
tBox2.Text += (item.ToString("X02") + ' ');
}
}
private void Calculate_CRC8()
{
myCRC mycrc = new myCRC();
string[] myDataStr = tBox1.Text.Split(' ');
List<byte> Arc = new List<byte>();
foreach (var item in myDataStr)
{
try
{
Arc.Add(Convert.ToByte(item, 16));
}
catch (Exception)
{
MessageBox.Show("请输入以空格为间隔的16进制字符串!");
return;
}
}
myDataByte = Arc.ToArray();
tBox2.Clear();
byte res = mycrc.Crc8_init(0x00, myDataByte, (UInt16)(myDataStr.Length));
Arc.Add(res);
myDataByte = Arc.ToArray();
sendDataCount = Arc.Count;
foreach (var item in myDataByte)
{
tBox2.Text += (item.ToString("X02") + ' ');
}
}
private void button1_Click(object sender, EventArgs e)
{
if (rbtnCRC8.Checked == true)
{
Calculate_CRC8();
}
else if (rbtnCRC16.Checked == true)
{
Calculate_CRC16();
}
}
private void btnClear_Click(object sender, EventArgs e)
{
#if USE_DELEGATE
this.UpdataControl(tBox1, string.Empty);
this.UpdataControl(tBox2, string.Empty);
this.UpdataControl(tboxSend, string.Empty);
this.UpdataControl(tboxRecv, string.Empty);
#else
tBox1.Text = string.Empty;
tBox2.Text = string.Empty;
tboxSend.Text = string.Empty;
tboxRecv.Text = string.Empty;
#endif
}
private void tBox1_KeyDown(object sender, KeyEventArgs e)
{
if (e.KeyCode == Keys.Enter)
{
if (rbtnCRC8.Checked == true)
{
Calculate_CRC8();
}
else if (rbtnCRC16.Checked == true)
{
Calculate_CRC16();
}
}
}
public byte[] usartRecvBuffer = new byte[4096]; //开辟4096Byte的接收缓冲区
private void serialPort1_DataReceived(object sender, System.IO.Ports.SerialDataReceivedEventArgs e)
{
//if (e.EventType == System.IO.Ports.SerialData.Eof)
if (serialPort1.BytesToRead == 0)
{
return;
}
string str = string.Empty;
stopWatch.Stop();
TimeSpan timespan = stopWatch.Elapsed;
str = timespan.TotalMilliseconds.ToString() + "mS";
#if USE_DELEGATE
this.UpdataControl(label6, str);
#else
label6.Text = str;
#endif
//串口接收并不是接收的每个字节都会进入该事件,因此需在该事件中接收完数据
Int32 readByteNum = 0;
/* 等待数据接收完成,即3mS内串口接收到的数据长度不再变化则认为数据已经接收完成 */
do
{
readByteNum = serialPort1.BytesToRead;
System.Threading.Thread.Sleep(10);
} while (readByteNum < serialPort1.BytesToRead && serialPort1.BytesToRead < 4096);
serialPort1.Read(usartRecvBuffer, 0, readByteNum); //将串口缓冲区的数据保存至接收缓冲区
serialPort1.DiscardInBuffer(); //清空串口缓冲区的内容
str = string.Empty;
for (int i = 0; i < readByteNum; i++)
{
str += (usartRecvBuffer[i].ToString("X02") + ' ');
}
#if USE_DELEGATE
this.UpdataControl(tboxRecv, str);
#else
tboxRecv.Text = str;
#endif
}
public System.Diagnostics.Stopwatch stopWatch = new System.Diagnostics.Stopwatch();
public int sendDataCount = 0; //要发送的字节数
/// <summary>
/// “发送”按钮按下
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void btnSend_Click(object sender, EventArgs e)
{
if (serialPort1.IsOpen == false)
{
MessageBox.Show("发送数据前请先打开串口!", "提示");
return;
}
if (sendDataCount == 0)
{
MessageBox.Show("发送区为空!", "提示");
return;
}
#if USE_DELEGATE
this.UpdataControl(label6, "0mS");
this.UpdataControl(tboxRecv, string.Empty);
this.UpdataControl(tboxSend, tBox2.Text);
#else
label6.Text = "0";
tboxRecv.Text = string.Empty;
tboxSend.Text = tBox2.Text;
#endif
stopWatch.Restart();
serialPort1.Write(myDataByte, 0, sendDataCount);
}
private void btnOprtPort_Click(object sender, EventArgs e)
{
if (btnOprtPort.Text == "单击打开串口")
{
try
{
//初始化并打开串口
serialPort1.PortName = cbxPortNum.Text;
serialPort1.BaudRate = 9600;
serialPort1.DataBits = 8;
serialPort1.Parity = System.IO.Ports.Parity.None;
serialPort1.StopBits = System.IO.Ports.StopBits.One;
serialPort1.ReceivedBytesThreshold = 1;
serialPort1.Open();
#if USE_DELEGATE
this.UpdataControl(label6, "0mS");
this.UpdataControl(tboxRecv, string.Empty);
this.UpdataControl(tboxSend, tBox2.Text);
this.UpdataControl(btnOprtPort, "单击关闭串口");
#else
label6.Text = "0mS";
tboxRecv.Text = string.Empty;
tboxSend.Text = tBox2.Text;
btnOprtPort.Text = "单击关闭串口";
#endif
btnOprtPort.BackColor = Color.GreenYellow;
}
catch (Exception)
{
MessageBox.Show("串口打开失败!", "警告");
}
}
else
{
try
{
serialPort1.Close();
#if USE_DELEGATE
this.UpdataControl(btnOprtPort, "单击打开串口");
#else
btnOprtPort.Text = "单击打开串口";
#endif
btnOprtPort.BackColor = Color.OrangeRed;
}
catch (Exception)
{
MessageBox.Show("串口关闭失败!", "警告");
}
}
}
private void Form1_FormClosing(object sender, FormClosingEventArgs e)
{
if (serialPort1.IsOpen) //程序退出时需关闭已打开串口
{
try
{
serialPort1.Close();
}
catch (Exception)
{
throw;
}
}
}
}
}