Serial Port 系列(8) 基本篇 -- 使用DataReceived 事件接收資料

終於進入接收資料的開始,就從DataReceived事件來進入這個階段。
純接收的模型一般來講用DataReceived事件是很方便的,雖然它其實有一些毛病,我們暫且先拋開這些毛病,從最簡單的程式碼看起。

       終於進入接收資料的開始,就從DataReceived事件來進入這個階段。

 

       純接收的模型一般來講用DataReceived事件是很方便的,雖然它其實有一些毛病,我們暫且先拋開這些毛病,從最簡單的程式碼看起。先來建立一個如下圖的Form:

 

2012-01-18_225346 (1) 左方的TextBbox ,設定為Multiline、Both ScrollBars,用來顯示收到的資料。

(2) 右方的Label2用來顯示收到的Byte數。

(3) Button的用途是用來清除TextBox中的文字,由於字串相加的特性,字越多它會越來越慢,但由於處理文字的效率不是要講的重點,所以偷懶地使用最簡單的方法。

 

 

      程式碼如下:

Imports System.IO.Ports
Public Class Form1
	Private comport As SerialPort
	Private totalLength As Int32 = 0
	Private Delegate Sub Display(ByVal buffer As Byte())

	Private Sub DisplayText(ByVal buffer As Byte())
		TextBox1.Text &= String.Format("{0}{1}", BitConverter.ToString(buffer), Environment.NewLine)
		totalLength = totalLength + buffer.Length
		Label2.Text = totalLength.ToString()
	End Sub

	Private Sub Form1_Load(sender As System.Object, e As System.EventArgs) Handles MyBase.Load
		comport = New SerialPort("COM5", 9600, Parity.None, 8, StopBits.One)
		AddHandler comport.DataReceived, AddressOf comport_DataReceived
		If comport.IsOpen = False Then
			comport.Open()
		End If
	End Sub

	Private Sub Button_ClearTextbox_Click(sender As System.Object, e As System.EventArgs) Handles Button_ClearTextbox.Click
		TextBox1.Clear()
	End Sub

	Private Sub comport_DataReceived(sender As Object, e As SerialDataReceivedEventArgs)
		Dim buffer(1023) As Byte
		Dim length As Int32 = DirectCast(sender, SerialPort).Read(buffer, 0, buffer.Length)
		Array.Resize(buffer, length)
		Dim d As New Display(AddressOf DisplayText)
		Me.Invoke(d, New Object() {buffer})
	End Sub
End Class

 

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.IO.Ports;
namespace SimpleReceiveEventCS
{
	public partial class Form1 : Form
	{
		public Form1()
		{
			InitializeComponent();
		}
		private SerialPort comport;
		private Int32 totalLength = 0;
		delegate void Display(Byte[] buffer);

		private void DisplayText(Byte[] buffer)
		{
			TextBox1.Text += String.Format("{0}{1}", BitConverter.ToString(buffer), Environment.NewLine);
			totalLength = totalLength + buffer.Length;
			Label2.Text = totalLength.ToString();
		}

		private void Form1_Load(object sender, EventArgs e)
		{
			comport = new SerialPort("COM5", 9600, Parity.None, 8, StopBits.One);
			comport.DataReceived +=new SerialDataReceivedEventHandler(comport_DataReceived);
			if (!comport.IsOpen)
			{
				comport.Open();
			}
		}

		private void comport_DataReceived(Object sender, SerialDataReceivedEventArgs e)
		{
			Byte[] buffer = new Byte[1024];
			Int32 length = (sender as SerialPort).Read(buffer, 0, buffer.Length);
			Array.Resize(ref buffer, length);
			Display d = new Display(DisplayText);
			this.Invoke(d,new Object[]{buffer});
		}

		private void Button_ClearTextbox_Click(object sender, EventArgs e)
		{
			TextBox1.Clear();
		}
	}
}

 

 

       一些前面聊過的東西就不再重複,來看點新鮮的。

       (1) DataReceived 事件的重要特性

       在MSDN文件庫 [SerialPort.DataReceived 事件] 的備註中,提到一件非常重要的事情:

       SerialPort 物件收到資料時,會在次要執行緒上引發 DataReceived 事件。 由於這個事件是在次要執行緒上引發,而非主執行緒,所以嘗試在主執行緒中修改某些項目 (例如 UI 項目) 可能會引發執行緒例外狀況。 如果必須在主要 FormControl 中修改項目,請使用 Invoke 回傳變更要求,此方法將會針對適當的執行緒執行此作業。

 

       (2) 宣告委派

       因為需要使用Control.Invoke的緣故,所以先來宣告個委派 (Delegate) 用用;對初學者來講委派應該是個很難理解的東西,不過它在C++上有一個近親名叫『函式指標』,我自己是覺得這麼名字比委派來得好理解。為了能讓大家簡單理解委派在搞什麼鬼,我就用變數的概念來轉化一下。註: 以下我的說明不是正式的講法,只是為理解方便。

 

 

  Visual Basic C#
程式碼 Private Delegate Sub Display(ByVal buffer As Byte()) delegate void Display(Byte[] buffer);
解說 宣告一個繼承自Delegate 的型別叫 『無回傳值方法』Display(ByVal buffer As Byte()) 宣告一個繼承自Delegate 的型別叫 『無回傳值方法』Display(Byte[] buffer)

         

 

  Visual Basic C#
程式碼 Dim d As New Display(AddressOf DisplayText) Display d = new Display(DisplayText);
解說 宣告一個以 Display 委派為型別的變數d,其內容中的函式指標指向 DispalyText Method 所在的記憶體位址。 宣告一個以 Display 委派為型別的變數d,其內容中的函式指標指向 DispalyText Method 所在的記憶體位址。

 

  Visual Basic C#
程式碼 Me.Invoke(d, New Object() {buffer}) this.Invoke(d,new Object[]{buffer});
解說 在Me (也就是這個Form) 所在的執行緒上執行d所指向的Method,並將第二個參數傳入給d所指向的Method (也就是DiaplsyText) 當參數 在this (也就是這個Form) 所在的執行緒上執行d所指向的Method,並將第二個參數傳入給d所指向的Method (也就是DiaplsyText) 當參數

註:請參考MSDN文件庫 [Control.Invoke 方法 (Delegate, Object[])]

 

       (3) 加入SerialPort.DataReceived事件委派與撰寫委派函式

AddHandler comport.DataReceived, AddressOf comport_DataReceived

 

	Private Sub comport_DataReceived(sender As Object, e As SerialDataReceivedEventArgs)
		Dim buffer(1023) As Byte
		Dim length As Int32 = DirectCast(sender, SerialPort).Read(buffer, 0, buffer.Length)
		Array.Resize(buffer, length)
		Dim d As New Display(AddressOf DisplayText)
		Me.Invoke(d, New Object() {buffer})
	End Sub

 

comport.DataReceived +=new SerialDataReceivedEventHandler(comport_DataReceived);

 

		private void comport_DataReceived(Object sender, SerialDataReceivedEventArgs e)
		{
			Byte[] buffer = new Byte[1024];
			Int32 length = (sender as SerialPort).Read(buffer, 0, buffer.Length);
			Array.Resize(ref buffer, length);
			Display d = new Display(DisplayText);
			this.Invoke(d,new Object[]{buffer});
		}

 

       所以當DataReceived 事件被引發後,就會執行comport_DataReceived方法中的內容使用 SerialPort.Read 方法 (Byte[], Int32, Int32) 讀取緩衝區資料,然後呼叫DispalyText 顯示某些資料在畫面上。不過這只是DataReceived 事件最基礎的用法,主要是為了讓不熟這事件的人能夠初步的瞭解如何使用它,這樣的寫法其實存在著一些問題,下一篇會談到這一部份的問題及如何避免。