經過了上一篇的說明,應該都瞭解基本發送的做法,記得我在 [Serial Port 系列(2) 在開始寫程式之前]提到了多執行緒,現在就用多執行緒來實做發送。
為什麼通訊的部份應該要放在多執行緒中呢?有幾種狀況我們會遇到,例如像單次通訊的時間很長、亦或使用大量甚至無限迴圈進行通訊,在使用者操作軟體的時候你不會希望聽到他說:「我按了通訊後你的程式就當掉了,畫面都不會動。」諸如此類的評論;所以我一向很習慣把通訊寫在另一個執行緒來執行。這個會複雜點,希望你們有耐心看得下去。
經過了上一篇的說明,應該都瞭解基本發送的做法,記得我在 [Serial Port 系列(2) 在開始寫程式之前]提到了多執行緒,現在就用多執行緒來實做發送。
為什麼通訊的部份應該要放在多執行緒中呢?有幾種狀況我們會遇到,例如像單次通訊的時間很長、亦或使用大量甚至無限迴圈進行通訊,在使用者操作軟體的時候你不會希望聽到他說:「我按了通訊後你的程式就當掉了,畫面都不會動。」諸如此類的評論;所以我一向很習慣把通訊寫在另一個執行緒來執行。這個會複雜點,希望你們有耐心看得下去。
(1) 匯入命名空間
為了避免打很多字,請將以下兩個命名空間匯入:System.IO.Ports、System.Threading
(2) 定義兩個類別中的私有變數
Private comport As SerialPort
Private sending As Boolean
private SerialPort comport;
private Boolean sending;
其中 sending 這個變數是為了確認是否資料正在傳送中,是為了避免在傳送的當兒又呼叫SerialPort.Write造成例外。
(3) Method:產生 SerialPort 執行個體
Private Function CreateComport(ByVal port As SerialPort) As SerialPort
If port Is Nothing Then
port = New SerialPort("COM4", 9600, Parity.None, 8, StopBits.One)
End If
Return port
End Function
private SerialPort CreateComport(SerialPort port)
{
if (port == null)
{
port = new SerialPort("COM4", 9600, Parity.None, 8, StopBits.One);
}
return port;
}
檢查當comport變數沒有指向任何一個SerialPort執行個體時則產生一個執行個體。
(4) Method:開啟序列埠
Private Function OpenComport(ByVal port As SerialPort) As Boolean
Try
If (Not port Is Nothing) AndAlso (port.IsOpen = False) Then
port.Open()
End If
Return True
Catch ex As Exception
'這邊你可以自訂發生例外的處理程序
MessageBox.Show(String.Format("出問題啦:{0}", ex.ToString()))
Return False
End Try
End Function
private Boolean OpenComport(SerialPort port)
{
try
{
if ((port != null) && (!port.IsOpen))
{
port.Open();
}
return true;
}
catch (Exception ex)
{
MessageBox.Show(String.Format("出問題啦:{0}", ex.ToString()));
return false;
}
}
檢查當變數指向一個執行個體 (也就是不為 Nothing/Null) 且此執行個體的 IsOpen屬性為False (也就是序列埠還沒被開啟)時,則開啟序列埠。
(5) Method:關閉序列埠
Private Sub CloseComport(ByVal port As SerialPort)
Try
If (Not port Is Nothing) AndAlso (port.IsOpen) AndAlso (sending = False) Then
port.Close()
MessageBox.Show("序列埠已關閉")
End If
Catch ex As Exception
'這邊你可以自訂發生例外的處理程序
MessageBox.Show(String.Format("出問題啦:{0}", ex.ToString()))
End Try
End Sub
private void CloseComport(SerialPort port)
{
try
{
if ((port != null) && (port.IsOpen) && (!sending))
{
port.Close();
MessageBox.Show("序列埠已關閉");
}
}
catch (Exception ex)
{
//這邊你可以自訂發生例外的處理程序
MessageBox.Show(String.Format("出問題啦:{0}", ex.ToString()));
}
}
有開就有關,會執行關閉的條件為執行個體存在且序列埠已開啟且非傳送狀態。
(6) Method:傳送資料
Private Sub SendData(ByVal port As Object)
Dim buffer(1023) As Byte
For i As Int32 = 0 To buffer.Length - 1
buffer(i) = i Mod 256
Next
sending = True
Try
DirectCast(port, SerialPort).Write(buffer, 0, buffer.Length)
Catch ex As Exception
'這邊你可以自訂發生例外的處理程序
CloseComport(DirectCast(port, SerialPort))
MessageBox.Show(String.Format("出問題啦:{0}", ex.ToString()))
Finally
sending = False
End Try
End Sub
private void SendData(Object port) { Byte[] buffer = new Byte[1024]; for (int i = 0; i < 1024; i++) { buffer[i] = (Byte)(i % 256); } sending = true; try { (port as SerialPort).Write(buffer, 0, buffer.Length); } catch (Exception ex) { //這邊你可以自訂發生例外的處理程序 CloseComport((port as SerialPort)); MessageBox.Show(String.Format("出問題啦:{0}", ex.ToString())); } finally { sending = false; } }
這個方法其實只要你看懂前一篇就知道它不過就是先建立一個Byte陣列,然後呼叫Write方法傳送出去,然後在傳送前將sending變數值設為True,在方法結束前將sending變數值設為Fasle。比較特別的是這個SendData方法的傳入參數型別是Object,因為這個方法將會使用執行緒的方式呼叫,而Thread.Start 方法 (Object)的參數型別是Object,在MSDN 文件庫[ParameterizedThreadStart 委派]中有一段話:
當建立 Managed 執行緒時,在此執行緒上執行的方法會由傳遞給 Thread 建構函式的 ThreadStart 委派或 ParameterizedThreadStart 委派所表示。 此執行緒要等到呼叫 Thread.Start 方法之後,才會開始執行。 ThreadStart 或 ParameterizedThreadStart 委派是在執行緒上叫用,而執行動作會在委派所代表的方法第一行開始。 若為 ParameterizedThreadStart 委派,則傳遞至 Start(Object) 方法的物件會傳遞至委派。
Visual Basic 和 C# 的使用者在建立執行緒時,可以省略 ThreadStart 或 ParameterizedThreadStart 委派建構函式。 在 Visual Basic 中,當傳遞方法至 Thread 建構函式時,請使用 AddressOf 運算子,例如 Dim t As New Thread(AddressOf ThreadProc)。 在 C# 中,只需指定執行緒程序的名稱。 編譯器會選擇正確的委派建構函式。
上面程式碼用的就是由編譯器決定的方式,其實我們在用的就是ParameterizedThreadStart 委派。若是Visual Baisc的使用者可能會發現就算參數改成SerialPort型別還是會動,這是因為Visual Basic隱含轉換的關係,但我不建議這樣做,因為你一旦對於隱含轉換太過習慣,當你換個強型別更嚴謹的程式語言(ex: C#)就會被整的很慘。
各位還有另外一個疑問可能是為什麼我不直接拿類別內的全域私有變數comport來處理就好,為何要把它當參數傳來傳去,其實沒有特別的原因,純粹是自己失心瘋就這樣寫了,所以其實上面的每一個Method都可以改成無參數,然後直接用私有變數comport處理就好。
現在該有的方法都有了,接下來就是要寫一些事件委派處理常式,按下Button1會傳送、按下Button2就關閉序列埠。
Private Sub Form1_Load(sender As System.Object, e As System.EventArgs) Handles MyBase.Load
sending = False
End Sub
Private Sub Button1_Click(sender As System.Object, e As System.EventArgs) Handles Button1.Click
comport = CreateComport(comport)
If sending = False AndAlso OpenComport(comport) = True Then
Dim t As Threading.Thread = New Threading.Thread(AddressOf SendData)
t.IsBackground = True
t.Start(comport)
End If
End Sub
Private Sub Button2_Click(sender As System.Object, e As System.EventArgs) Handles Button2.Click
CloseComport(comport)
End Sub
private void Form1_Load(object sender, EventArgs e)
{
sending = false;
}
private void button1_Click(object sender, EventArgs e)
{
comport = CreateComport(comport);
if (!sending && OpenComport(comport))
{
Thread t = new Thread(SendData);
t.IsBackground = true;
t.Start(comport as Object);
}
}
private void button2_Click(object sender, EventArgs e)
{
CloseComport(comport);
}
在Button1的Click事件委派函式中做了一個檢查sending值的動作,這個動作在確保不會在SerialPort正在傳送資料的時候,又同時再度呼叫Write,其實還可以更機車的在呼叫SerilPort.Write之前先讓Button1的Enabled屬性變成False,在Finally區塊再恢復成True,不過這牽涉到跨執行緒UI委派的問題,我打算以後再來寫這一部份。然候產生一個執行緒並使用IsBackground屬性將其設為背景執行緒,在這個例子中是否是背景執行緒倒是沒這麼重要,但如果今天執行緒中跑的是無窮迴圈的時候,IsBackground這件事就相當重要了,因為背景執行緒會因為呼叫端程式的關閉而結束,但如果你有一個無窮迴圈的前景執行緒,當你的主程式關了之後它還是繼續一直在跑。
這一篇還是滿簡單的,最後來回顧一下幾個重點:
(1) 撰寫自訂方法 (Method) 讓程式的結構看起來更清楚。
(2) 產生執行緒來執行通訊的程序,避免通訊過程中鎖住使用者操作介面。
(3) 設定檢查機制,避免例外的產生。
範例程式下載:SimpleSend.zip