深入淺出序列槽程式設計(5)
――基于第三方類的序列槽程式設計
宋寶華 [email][email protected][/email]
1.序列槽類
從本系列文章連載三、四可以看出,與通過WIN32 API進行序列槽通路相比,通過MScomm這個Activex控件進行序列槽通路要來的友善許多,它基本上可以向使用者屏蔽多線程的細節,以事件(發出OnComm消息)方式實作序列槽的異步通路。
盡管如此,MScomm控件的使用仍有諸多不便,譬如其發送和接收資料都要進行VARIANT類型對象與字元串的轉化等。是以,國内外許多優秀的程式員自己編寫了一些序列槽類,使用這些類,我們将可以更友善的操作序列槽。在筆者的《深入淺出Win32多線程程式設計之綜合執行個體》(網址:[url]http://dev.yesky.com[/url])一文中,曾向讀者展示了由Remon Spekreijse編寫的CSerialPort序列槽類,而本文将向您展示由程式員llbird(部落格位址為:[url]http://blog.csdn.net/wujian53/[/url])編寫的cnComm(中國序列槽?)序列槽類。
llbird是一位優秀的程式員,他的代碼風格簡潔而緊湊,類的聲明和實作都被定義在一個頭檔案中,使用這個類的朋友隻需要在工程中包含這一頭檔案即可:
/*
Comm Base Library(WIN98/NT/2000) ver 1.1
Compile by: BC++ 5; C++ BUILDER 4, 5, 6, X; VC++ 5, 6; VC.NET; GCC;
copyright(c) 2004.5 - 2005.8 llbird [email][email protected][/email]
*/
#ifndef _CN_COMM_H_
#define _CN_COMM_H_
#pragma warning(disable: 4530)
#pragma warning(disable: 4786)
#pragma warning(disable: 4800)
#include <assert.h>
#include <stdio.h>
#include <windows.h>
//送到視窗的消息 WPARAM 端口号
#define ON_COM_RECEIVE WM_USER + 618
#define ON_COM_CTS WM_USER + 619 //LPARAM 1 valid
#define ON_COM_DSR WM_USER + 621 //LPARAM 1 valid
#define ON_COM_RING WM_USER + 623
#define ON_COM_RLSD WM_USER + 624
#define ON_COM_BREAK WM_USER + 625
#define ON_COM_TXEMPTY WM_USER + 626
#define ON_COM_ERROR WM_USER + 627 //LPARAM save Error ID
#define DEFAULT_COM_MASK_EVENT EV_RXCHAR | EV_ERR | EV_CTS | EV_DSR | EV_BREAK | EV_TXEMPTY | EV_RING | EV_RLSD
class cnComm
{
public:
//------------------------------Construction-----------------------------------
//第1個參數為是否在打開序列槽時啟動監視線程, 第2個參數為IO方式 阻塞方式(0)/ 異步重疊方式(預設)
cnComm(bool fAutoBeginThread = true, DWORD dwIOMode =
FILE_FLAG_OVERLAPPED): _dwIOMode(dwIOMode), _fAutoBeginThread
(fAutoBeginThread)
{
Init();
}
virtual ~cnComm()
Close();
UnInit();
//----------------------------------Attributes----------------------------------
//判斷序列槽是否打開
inline bool IsOpen()
return _hCommHandle != INVALID_HANDLE_VALUE;
operator bool()
//獲得序列槽句炳
inline HANDLE GetHandle()
return _hCommHandle;
operator HANDLE()
//獲得序列槽參數 DCB
DCB *GetState()
return IsOpen() && ::GetCommState(_hCommHandle, &_DCB) == TRUE ?
&_DCB: NULL;
//設定序列槽參數 DCB
bool SetState(DCB *pdcb = NULL)
return IsOpen() ? ::SetCommState(_hCommHandle, pdcb == NULL ? &_DCB:
pdcb) == TRUE: false;
//設定序列槽參數:波特率,停止位,等 支援設定字元串 "9600, 8, n, 1"
bool SetState(char *szSetStr)
if (IsOpen())
{
if (::GetCommState(_hCommHandle, &_DCB) != TRUE)
return false;
if (::BuildCommDCB(szSetStr, &_DCB) != TRUE)
return ::SetCommState(_hCommHandle, &_DCB) == TRUE;
}
return false;
//設定序列槽參數:波特率,停止位,等
bool SetState(DWORD dwBaudRate, DWORD dwByteSize = 8, DWORD dwParity =
NOPARITY, DWORD dwStopBits = ONESTOPBIT)
_DCB.BaudRate = dwBaudRate;
_DCB.ByteSize = (unsigned char)dwByteSize;
_DCB.Parity = (unsigned char)dwParity;
_DCB.StopBits = (unsigned char)dwStopBits;
//獲得逾時結構
LPCOMMTIMEOUTS GetTimeouts(void)
return IsOpen() && ::GetCommTimeouts(_hCommHandle, &_CO) == TRUE ?
&_CO: NULL;
//設定逾時
bool SetTimeouts(LPCOMMTIMEOUTS lpCO)
return IsOpen() ? ::SetCommTimeouts(_hCommHandle, lpCO) == TRUE:
false;
//設定序列槽的I/O緩沖區大小
bool SetBufferSize(DWORD dwInputSize, DWORD dwOutputSize)
return IsOpen() ? ::SetupComm(_hCommHandle, dwInputSize, dwOutputSize)
== TRUE: false;
//關聯消息的視窗句柄
inline void SetWnd(HWND hWnd)
assert(::IsWindow(hWnd));
_hNotifyWnd = hWnd;
//設定發送通知, 接受字元最小值
inline void SetNotifyNum(DWORD dwNum)
_dwNotifyNum = dwNum;
//線程是否運作
inline bool IsThreadRunning()
return _hThreadHandle != NULL;
//獲得線程句柄
inline HANDLE GetThread()
return _hThreadHandle;
//設定要監視的事件, 打開前設定有效
void SetMaskEvent(DWORD dwEvent = DEFAULT_COM_MASK_EVENT)
_dwMaskEvent = dwEvent;
//獲得讀緩沖區的字元數
int GetInputSize()
COMSTAT Stat;
DWORD dwError;
return ::ClearCommError(_hCommHandle, &dwError, &Stat) == TRUE ?
Stat.cbInQue : (DWORD) - 1L;
//----------------------------------Operations----------------------------------
//打開序列槽 預設 9600, 8, n, 1
bool Open(DWORD dwPort)
return Open(dwPort, 9600);
//打開序列槽 預設 baud_rate, 8, n, 1
bool Open(DWORD dwPort, DWORD dwBaudRate)
if (dwPort < 1 || dwPort > 1024)
return false;
BindCommPort(dwPort);
if (!OpenCommPort())
if (!SetupPort())
return SetState(dwBaudRate);
//打開序列槽, 使用類似"9600, 8, n, 1"的設定字元串設定序列槽
bool Open(DWORD dwPort, char *szSetStr)
return SetState(szSetStr);
//讀取序列槽 dwBufferLength個字元到 Buffer 傳回實際讀到的字元數 可讀任意資料
DWORD Read(LPVOID Buffer, DWORD dwBufferLength, DWORD dwWaitTime = 10)
if (!IsOpen())
return 0;
if (::ClearCommError(_hCommHandle, &dwError, &Stat) && dwError > 0)
::PurgeComm(_hCommHandle,
PURGE_RXABORT | PURGE_RXCLEAR);
return 0;
if (!Stat.cbInQue)
// 緩沖區無資料
unsigned long uReadLength = 0;
dwBufferLength = dwBufferLength > Stat.cbInQue ? Stat.cbInQue :
dwBufferLength;
if (!::ReadFile(_hCommHandle, Buffer, dwBufferLength, &uReadLength,
&_ReadOverlapped))
if (::GetLastError() == ERROR_IO_PENDING)
{
WaitForSingleObject(_ReadOverlapped.hEvent, dwWaitTime);
// 結束異步I/O
if (!::GetOverlappedResult(_hCommHandle, &_ReadOverlapped,
&uReadLength, false))
{
if (::GetLastError() != ERROR_IO_INCOMPLETE)
uReadLength = 0;
}
}
else
uReadLength = 0;
return uReadLength;
//讀取序列槽 dwBufferLength - 1 個字元到 szBuffer 傳回ANSI C 模式字元串指針 适合一般字元通訊
char *ReadString(char *szBuffer, DWORD dwBufferLength, DWORD dwWaitTime =
20)
unsigned long uReadLength = Read(szBuffer, dwBufferLength - 1,
dwWaitTime);
szBuffer[uReadLength] = '\0';
return szBuffer;
//寫序列槽 可寫任意資料 "abcd" or "\x0\x1\x2"
DWORD Write(LPVOID Buffer, DWORD dwBufferLength)
if (::ClearCommError(_hCommHandle, &dwError, NULL) && dwError > 0)
::PurgeComm(_hCommHandle, PURGE_TXABORT | PURGE_TXCLEAR);
unsigned long uWriteLength = 0;
if (!::WriteFile(_hCommHandle, Buffer, dwBufferLength, &uWriteLength,
&_WriteOverlapped))
if (::GetLastError() != ERROR_IO_PENDING)
uWriteLength = 0;
return uWriteLength;
//寫序列槽 寫ANSI C 模式字元串指針
DWORD Write(const char *szBuffer)
assert(szBuffer);
return Write((void*)szBuffer, strlen(szBuffer));
//讀序列槽 同步應用
DWORD ReadSync(LPVOID Buffer, DWORD dwBufferLength)
DWORD uReadLength = 0;
::ReadFile(_hCommHandle, Buffer, dwBufferLength, &uReadLength, NULL);
//寫序列槽 同步應用
DWORD WriteSync(LPVOID Buffer, DWORD dwBufferLength)
::WriteFile(_hCommHandle, Buffer, dwBufferLength, &uWriteLength, NULL)
;
//寫序列槽 szBuffer 可以輸出格式字元串 包含緩沖區長度
DWORD Write(char *szBuffer, DWORD dwBufferLength, char *szFormat, ...)
va_list va;
va_start(va, szFormat);
_vsnprintf(szBuffer, dwBufferLength, szFormat, va);
va_end(va);
return Write(szBuffer);
//寫序列槽 szBuffer 可以輸出格式字元串 不檢查緩沖區長度 小心溢出
DWORD Write(char *szBuffer, char *szFormat, ...)
vsprintf(szBuffer, szFormat, va);
//關閉序列槽 同時也關閉關聯線程
virtual void Close()
PurgeComm(_hCommHandle, PURGE_TXABORT | PURGE_TXCLEAR);
EndThread();
::CloseHandle(_hCommHandle);
_hCommHandle = INVALID_HANDLE_VALUE;
//DTR 電平控制
bool SetDTR(bool OnOrOff)
return IsOpen() ? EscapeCommFunction(_hCommHandle, OnOrOff ? SETDTR :
CLRDTR): false;
//RTS 電平控制
bool SetRTS(bool OnOrOff)
return IsOpen() ? EscapeCommFunction(_hCommHandle, OnOrOff ? SETRTS :
CLRRTS): false;
//
bool SetBreak(bool OnOrOff)
return IsOpen() ? EscapeCommFunction(_hCommHandle, OnOrOff ? SETBREAK
: CLRBREAK): false;
//輔助線程控制 建監視線程
bool BeginThread()
if (!IsThreadRunning())
_fRunFlag = true;
_hThreadHandle = NULL;
DWORD id;
_hThreadHandle = ::CreateThread(NULL, 0, CommThreadProc, this, 0,
&id);
return (_hThreadHandle != NULL);
//暫停監視線程
inline bool SuspendThread()
return IsThreadRunning() ? ::SuspendThread(_hThreadHandle) !=
0xFFFFFFFF: false;
//恢複監視線程
inline bool ResumeThread()
return IsThreadRunning() ? ::ResumeThread(_hThreadHandle) !=
//終止線程
bool EndThread(DWORD dwWaitTime = 100)
if (IsThreadRunning())
_fRunFlag = false;
::SetCommMask(_hCommHandle, 0);
::SetEvent(_WaitOverlapped.hEvent);
if (::WaitForSingleObject(_hThreadHandle, dwWaitTime) !=
WAIT_OBJECT_0)
if (!::TerminateThread(_hThreadHandle, 0))
return false;
::CloseHandle(_hThreadHandle);
::ResetEvent(_WaitOverlapped.hEvent);
return true;
protected:
volatile DWORD _dwPort; //序列槽号
volatile HANDLE _hCommHandle; //序列槽句柄
char _szCommStr[20]; //儲存COM1類似的字元串
DCB _DCB; //波特率,停止位,等
COMMTIMEOUTS _CO; //逾時結構
DWORD _dwIOMode; // 0 同步 預設 FILE_FLAG_OVERLAPPED重疊I/O異步
OVERLAPPED _ReadOverlapped, _WriteOverlapped; // 重疊I/O
volatile HANDLE _hThreadHandle; //輔助線程
volatile HWND _hNotifyWnd; // 通知視窗
volatile DWORD _dwNotifyNum; //接受多少位元組(>=_dwNotifyNum)發送通知消息
volatile DWORD _dwMaskEvent; //監視的事件
volatile bool _fRunFlag; //線程運作循環标志
bool _fAutoBeginThread; //Open() 自動 BeginThread();
OVERLAPPED _WaitOverlapped; //WaitCommEvent use
//初始化
void Init()
memset(_szCommStr, 0, 20);
memset(&_DCB, 0, sizeof(_DCB));
_DCB.DCBlength = sizeof(_DCB);
_hCommHandle = INVALID_HANDLE_VALUE;
memset(&_ReadOverlapped, 0, sizeof(_ReadOverlapped));
memset(&_WriteOverlapped, 0, sizeof(_WriteOverlapped));
_ReadOverlapped.hEvent = ::CreateEvent(NULL, true, false, NULL);
assert(_ReadOverlapped.hEvent != INVALID_HANDLE_VALUE);
_WriteOverlapped.hEvent = ::CreateEvent(NULL, true, false, NULL);
assert(_WriteOverlapped.hEvent != INVALID_HANDLE_VALUE);
_hNotifyWnd = NULL;
_dwNotifyNum = 0;
_dwMaskEvent = DEFAULT_COM_MASK_EVENT;
_hThreadHandle = NULL;
memset(&_WaitOverlapped, 0, sizeof(_WaitOverlapped));
_WaitOverlapped.hEvent = ::CreateEvent(NULL, true, false, NULL);
assert(_WaitOverlapped.hEvent != INVALID_HANDLE_VALUE);
//析構
void UnInit()
if (_ReadOverlapped.hEvent != INVALID_HANDLE_VALUE)
CloseHandle(_ReadOverlapped.hEvent);
if (_WriteOverlapped.hEvent != INVALID_HANDLE_VALUE)
CloseHandle(_WriteOverlapped.hEvent);
if (_WaitOverlapped.hEvent != INVALID_HANDLE_VALUE)
CloseHandle(_WaitOverlapped.hEvent);
//綁定序列槽
void BindCommPort(DWORD dwPort)
assert(dwPort >= 1 && dwPort <= 1024);
char p[5];
_dwPort = dwPort;
strcpy(_szCommStr, "\\\\.\\COM");
ltoa(_dwPort, p, 10);
strcat(_szCommStr, p);
//打開序列槽
virtual bool OpenCommPort()
Close();
_hCommHandle = ::CreateFile(_szCommStr,
GENERIC_READ | GENERIC_WRITE, 0, NULL,
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL | _dwIOMode,
NULL);
if (_fAutoBeginThread)
if (IsOpen() && BeginThread())
return true;
Close(); //建立線程失敗
return IsOpen();
//設定序列槽
virtual bool SetupPort()
if (!::SetupComm(_hCommHandle, 4096, 4096))
if (!::GetCommTimeouts(_hCommHandle, &_CO))
_CO.ReadIntervalTimeout = 0;
_CO.ReadTotalTimeoutMultiplier = 1;
_CO.ReadTotalTimeoutConstant = 1000;
_CO.WriteTotalTimeoutMultiplier = 1;
_CO.WriteTotalTimeoutConstant = 1000;
if (!::SetCommTimeouts(_hCommHandle, &_CO))
if (!::PurgeComm(_hCommHandle, PURGE_TXABORT | PURGE_RXABORT |
PURGE_TXCLEAR | PURGE_RXCLEAR))
return true;
//---------------------------------------threads callback-----------------------------------
//線程收到消息自動調用, 如視窗句柄有效, 送出消息, 包含序列槽編号, 均為虛函數可以在基層類中擴充
virtual void OnReceive() //EV_RXCHAR
if (::IsWindow(_hNotifyWnd))
::PostMessage(_hNotifyWnd, ON_COM_RECEIVE,
WPARAM(_dwPort), LPARAM (0));
virtual void OnDSR()
DWORD Status;
if (GetCommModemStatus(_hCommHandle, &Status))
::PostMessage(_hNotifyWnd, ON_COM_DSR, WPARAM(_dwPort),
LPARAM((Status &MS_DSR_ON) ? 1 : 0));
virtual void OnCTS()
::PostMessage(_hNotifyWnd, ON_COM_CTS, WPARAM(_dwPort),
LPARAM( (Status &MS_CTS_ON) ? 1 : 0));
virtual void OnBreak()
::PostMessage(_hNotifyWnd, ON_COM_BREAK, WPARAM(_dwPort),
LPARAM(0));
virtual void OnTXEmpty()
::PostMessage(_hNotifyWnd, ON_COM_TXEMPTY, WPARAM(_dwPort),
LPARAM (0));
virtual void OnError()
::ClearCommError(_hCommHandle, &dwError, NULL);
::PostMessage(_hNotifyWnd, ON_COM_ERROR, WPARAM(_dwPort),
LPARAM (dwError));
virtual void OnRing()
::PostMessage(_hNotifyWnd, ON_COM_RING, WPARAM(_dwPort),
LPARAM(0));
virtual void OnRLSD()
::PostMessage(_hNotifyWnd, ON_COM_RLSD, WPARAM(_dwPort),
virtual DWORD ThreadFunc()
if (!::SetCommMask(_hCommHandle, _dwMaskEvent))
char szBuffer[256];
_snprintf(szBuffer, 255,
"%s(%d) : COM%d Call WINAPI SetCommMask(%x, %x) Fail, thread work invalid! GetLastError() = %d;", __FILE__, __LINE__, _dwPort, _hCommHandle, _dwMaskEvent, GetLastError());
MessageBox(NULL, szBuffer, "Class cnComm", MB_OK);
return 1;
for (DWORD dwLength, dwMask = 0; _fRunFlag && IsOpen(); dwMask = 0)
if (!::WaitCommEvent(_hCommHandle, &dwMask, &_WaitOverlapped))
if (::GetLastError() == ERROR_IO_PENDING)
// asynchronous
::GetOverlappedResult(_hCommHandle, &_WaitOverlapped,
&dwLength, TRUE);
else
continue;
if (dwMask == 0)
continue;
switch (dwMask)
case EV_RXCHAR:
::ClearCommError(_hCommHandle, &dwError, &Stat);
if (Stat.cbInQue >= _dwNotifyNum)
OnReceive();
break;
case EV_TXEMPTY:
OnTXEmpty();
case EV_CTS:
OnCTS();
case EV_DSR:
OnDSR();
case EV_RING:
OnRing();
case EV_RLSD:
OnRLSD();
case EV_BREAK:
OnBreak();
case EV_ERR:
OnError();
} //case
} //for
return 0;
private:
//the function protected
cnComm(const cnComm &);
cnComm &operator = (const cnComm &);
//base function for thread
static DWORD WINAPI CommThreadProc(LPVOID lpPara)
return ((cnComm*)lpPara)->ThreadFunc();
};
#endif //_CN_COMM_H_
2.執行個體
程式的功能和界面(如下圖)都與本文連載三、四中《基于WIN32 API的序列槽程式設計》和《基于控件的序列槽程式設計》相同,不同的隻是本節的序列槽通信要以llbird定義的cnComm類來實作。
我們需要為序列槽的接收事件定義一個使用者消息ON_COM_RECEIVE,是以對話框的消息映射為:
BEGIN_MESSAGE_MAP(CSerialPortClassDlg, CDialog)
//{{AFX_MSG_MAP(CSerialPortClassDlg)
ON_WM_SYSCOMMAND()
ON_WM_PAINT()
ON_WM_QUERYDRAGICON()
ON_BN_CLICKED(IDC_CLEAR_BUTTON, OnClearButton)
ON_BN_CLICKED(IDC_SEND_BUTTON, OnSendButton)
ON_MESSAGE(ON_COM_RECEIVE,OnCommRecv)
//}}AFX_MSG_MAP
END_MESSAGE_MAP()
同時,我們需要在對話框類的頭檔案中定義cnComm類的成員變量com和接收資料消息處理函數OnCommRecv:
cnComm com;
afx_msg void OnCommRecv(WPARAM wParam, LPARAM lParam);
在對話框初始化時調用打開序列槽:
BOOL CSerialPortClassDlg::OnInitDialog()
{
CDialog::OnInitDialog();
// Add "About..." menu item to system menu.
// IDM_ABOUTBOX must be in the system command range.
ASSERT((IDM_ABOUTBOX &0xFFF0) == IDM_ABOUTBOX);
ASSERT(IDM_ABOUTBOX < 0xF000);
CMenu *pSysMenu = GetSystemMenu(FALSE);
if (pSysMenu != NULL)
CString strAboutMenu;
strAboutMenu.LoadString(IDS_ABOUTBOX);
if (!strAboutMenu.IsEmpty())
{
pSysMenu->AppendMenu(MF_SEPARATOR);
pSysMenu->AppendMenu(MF_STRING, IDM_ABOUTBOX, strAboutMenu);
}
}
// Set the icon for this dialog. The framework does this automatically
// when the application's main window is not a dialog
SetIcon(m_hIcon, TRUE); // Set big icon
SetIcon(m_hIcon, FALSE); // Set small icon
// TODO: Add extra initialization here
com.Open(1); //打開序列槽1并使用預設設定
com.SetWnd(AfxGetMainWnd()->m_hWnd); //設定消息處理視窗
return TRUE; // return TRUE unless you set the focus to a control
}
發送字元串的過程很簡單,隻需要調用cnComm類的Write函數:
//"發送"按鈕函數(完成資料的發送功能)
void CSerialPortClassDlg::OnSendButton()
// TODO: Add your control notification handler code here
UpdateData(true);
com.Write(m_send); //發送字元串
接收字元串的過程也很簡單,隻需要調用cnComm類的ReadString函數:
void CSerialPortClassDlg::OnCommRecv(WPARAM wParam, LPARAM lParam)
//讀取序列槽上的字元
char str[100];
com.ReadString(str, 100);
m_recv += str;
UpdateData(false);
讀者朋友們這時一定會發出感慨:使用cnComm類後,進行序列槽資料收發的程式是多麼簡單啊!的确,序列槽的初始化、讀寫幾乎都是用1~2條語句搞定的!
這就是我們要特别用一次連載來講述使用第三方類來進行序列槽通信的原因。實際上,筆者在進行網絡通信程式程式設計時,也不認為MS提供的CSocket類是最友善的選擇,照樣習慣使用第三方的網絡通信類。它們的确有非常簡潔明快的接口,這一點也是值得MS哥哥們學習的。
本文轉自 21cnbao 51CTO部落格,原文連結:http://blog.51cto.com/21cnbao/120810,如需轉載請自行聯系原作者