C++ MFC實現基於RFID讀寫器的上位機軟件
該博客涉及的完整工程托管在https://github.com/Wsine/UpperMonitor,覺得好請給個Star (/▽\=)
運行和測試環境
- Windows 10
- Visual Studio 2013
- msado15.dll(工程自帶)
- ZM124U.dll(工程自帶)
- RFID讀寫器ZM124U
理論支持全部Win32運行環境
參考內容
- https://github.com/Wsine/UpperMonitor/blob/master/references/ZM12xUE系列接口函數說明_20130712.pdf
- http://durant35.github.io/categories/物聯網技術導論實驗課/
- MSDN Microsoft Developer Network MFC&ATL VS2013
代碼實現
軟件框架
在消息響應函數OnInitDialog()中完成整個框架的內容設置,包括插入Tab選項卡標簽,關聯對話框,調整大小和設置默認選項卡
BOOL CUpperMonitorDlg::OnInitDialog() {
CDialogEx::OnInitDialog();
// TODO: 在此添加額外的初始化代碼
// 1. 插入Tab選項卡標簽
TCITEM tcItemDebugger;
tcItemDebugger.mask = TCIF_TEXT;
tcItemDebugger.pszText = _T("調試助手");
m_MainMenu.InsertItem(0, &tcItemDebugger);
TCITEM tcItemAppdev;
tcItemAppdev.mask = TCIF_TEXT;
tcItemAppdev.pszText = _T("應用開發");
m_MainMenu.InsertItem(1, &tcItemAppdev);
// 2. 關聯對話框,將TAB控件設為選項卡對應對話框的父窗口
m_MenuDebugger.Create(IDD_DEBUGGER, GetDlgItem(IDC_TAB));
m_MenuAppdev.Create(IDD_APPDEV, GetDlgItem(IDC_TAB));
// 3. 獲取TAB控件客戶區大小,用於調整選項卡對話框在父窗口中的位置
CRect rect;
m_MainMenu.GetClientRect(&rect);
rect.top += 22;
rect.right -= 3;
rect.bottom -= 2;
rect.left += 1;
// 4. 設置子對話框尺寸並移動到指定位置
m_MenuDebugger.MoveWindow(&rect);
m_MenuAppdev.MoveWindow(&rect);
// 5. 設置默認選項卡,對選項卡對話框進行隱藏和顯示
m_MenuDebugger.ShowWindow(SW_SHOWNORMAL);
m_MenuAppdev.ShowWindow(SW_HIDE);
m_MainMenu.SetCurSel(0);
return TRUE; // 除非將焦點設置到控件,否則返回 TRUE
}
在消息響應函數OnSelchangeTab()中完成選項卡切換,其實內容都一直存在,只是把非該選項卡的內容隱藏了,把該選項卡的內容顯示出來,僅此而已
void CUpperMonitorDlg::OnSelchangeTab(NMHDR *pNMHDR, LRESULT *pResult) {
*pResult = 0;
// 獲取當前點擊選項卡標簽下標
int cursel = m_MainMenu.GetCurSel();
// 根據下標將相應的對話框顯示,其余隱藏
switch(cursel) {
case 0:
m_MenuDebugger.ShowWindow(SW_SHOWNORMAL);
m_MenuAppdev.ShowWindow(SW_HIDE);
break;
case 1:
m_MenuDebugger.ShowWindow(SW_HIDE);
m_MenuAppdev.ShowWindow(SW_SHOWNORMAL);
break;
default:
break;
}
}
調用ZM12xUE API
首先需要在工程中include相應的文件,就是已經封裝好的ZM124U.lib和ZM124U.h
#pragma comment(lib, "./libs/ZM124U.lib")
#include "./libs/ZM124U.h"
然后就可以當作已經實現的函數一樣,直接調用即可。庫函數的傳入參數和返回值全部都可以在上面的參考文件中找到,返回值一般使用IFD_OK足夠了。下面以打開設備為例,展示一下如何使用
void CDebugger::OnBnClickedBtnopendevice() {
if(IDD_PowerOn() == IFD_OK) {
// 更新狀態欄,成功
isDeviceOpen = true;
((CEdit*)GetDlgItem(IDC_EDITSTATUS))->SetWindowTextW(_T("開啟設備成功"));
}
else {
// 更新狀態欄,失敗
isDeviceOpen = false;
((CEdit*)GetDlgItem(IDC_EDITSTATUS))->SetWindowTextW(_T("開啟設備失敗"));
}
}
類型轉換
unsigned char轉CString
這部分和printf函數相似,就是使用CString.Format()函數把它轉換為相應的內容,然后拼接到最終的CString對象中。大多數類型轉CString都可以使用這種方法,說明CString封裝得好。CString.Format()函數的第一個參數的寫法類似於printf的輸出時的寫法。詳看MSDN上面的說明。
CString uid, temp;
unsigned char buff[1024];
uid.Empty();
for(int i = 0; i < buff_len; i++) {
// 將獲得的UID數據(1 byte)轉為16進制
temp.Format(_T("%02x"), buff[i]);
uid += temp;
}
CString轉int / long
前提CString的內容是數字。使用函數_ttoi(CString) | _ttol(CString)
即可,如需轉unsigned char
類型,再使用強制轉換類型即可
CString mecNum;
int mecNumInt = _ttoi(mecNum);
long mecNumLong = _ttol(mecLong);
unsigned char point = (unsigned char)_ttoi(mecNum) + 1;
*CString轉char **
說實話沒有找到好的方法,但是只要char*
空間足夠,可以使用=
直接賦值,內置轉換,我覺得一定有這個API的,但是我沒找到而已
void CUtils::CString2CharStar(const CString& s, char* ch, int len) {
int i;
for (i = 0; i < len; i++) {
ch[i] = s[i];
}
ch[i] = '\0';
return;
}
*十六進制CString轉UnsignedChar **
這里要求傳入參數必須是偶數個字符,同時操作兩個字符的轉換方法
void CUtils::HexCString2UnsignedCharStar(const CString& hexStr, unsigned char* asc, int* asc_len) {
*asc_len = 0;
int len = hexStr.GetLength();
char temp[200];
char tmp[3] = { 0 };
char* Hex;
unsigned char* p;
CString2CharStar(hexStr, temp, len);
Hex = temp;
p = asc;
while (*Hex != '\0') {
tmp[0] = *Hex;
Hex++;
tmp[1] = *Hex;
Hex++;
tmp[2] = '\0';
*p = (unsigned char)strtol(tmp, NULL, 16);
p++;
(*asc_len)++;
}
*p = '\0';
return;
}
界面美化
設置控件顏色
在每個控件開始繪制內容在窗口的時候,都會向父窗口發送WM_CTLCOLOR消息
,因此我們只需要重載相應的父窗口消息響應函數OnCtlColor()即可,程序會運行完響應函數的代碼再開始繪制內容。因此在響應函數中判斷並改變刷子顏色即可。
#define RED RGB(255, 0, 0)
#define BLUE RGB(0, 0, 255)
#define BLACK RGB(0, 0, 0)
HBRUSH CDebugger::OnCtlColor(CDC* pDC, CWnd* pWnd, UINT nCtlColor) {
HBRUSH hbr = CDialogEx::OnCtlColor(pDC, pWnd, nCtlColor);
// TODO: 在此更改 DC 的任何特性
switch(pWnd->GetDlgCtrlID()) {
case IDC_EDITSTATUS:
if(this->isDeviceOpen)
pDC->SetTextColor(BLUE);
else
pDC->SetTextColor(RED);
break;
case IDC_EDITIOSTATUS:
if(this->canIO)
pDC->SetTextColor(BLUE);
else
pDC->SetTextColor(RED);
break;
case IDC_EDITVERSIONINFO:
case IDC_EDITCARDUID:
pDC->SelectObject(&m_font);
break;
default:
pDC->SetTextColor(BLACK);
break;
}
return hbr;
}
設置控件字體
在窗口的類中聲明CFont
類型的字體,在窗口的構造函數中為字體其賦值,在數據交換的消息響應函數中把字體綁定到響應的控件中去。注意,Create出來的GDI對象都是需要手動刪除的,否則會造成GDI泄漏。【聽說把Windows系統的2000多個GDI泄漏完了,系統就繪制不了任何東西了,看來就像電腦卡了哈哈o(^▽^)o不知道新版VS有沒有修復,我沒有試過,有人試了記得告訴我...
/* Debugger.h */
class CDebugger : CDialogEx {
private:
CFont m_font;
};
/* Debugger.cpp */
CDebugger::CDebugger(CWnd* pParent) : CDialogEx(CDebugger::IDD, pParent) {
// 創建字體
m_font.CreatePointFont(93, _T("楷體"));
}
CDebugger::~CDebugger() {
DeleteObject(m_font);
}
void CDebugger::DoDataExchange(CDataExchange* pDX) {
CDialogEx::DoDataExchange(pDX);
GetDlgItem(IDC_OpenDevice)->SetFont(&m_font);
GetDlgItem(IDC_GETCARDINFO)->SetFont(&m_font);
}
數據庫操作
這里使用的是ODBC(Open Database Connection)技術和OLE DB(對象鏈接與嵌入數據庫)技術。ODBC API可以與任何具有ODBC驅動程序的關系數據庫進行通信;OLE DB擴展了ODBC,提供數據庫編程的COM接口,提供可用於關系型和非關系型數據源的接口[也就是可以操作電子表格、文本文件等]。
這部分的理論知識建議參考上面提及的參考內容
配置數據庫
- mysql
- mysql-connector-odbc
- ZD124UE_DEMO.sql(工程文件包含)
安裝過程省略,首先需要建表,雙擊ZD124UE_DEMO.sql
即可新建一個數據庫及其相應的表格。然后配置數據源,參照下圖,點擊Test查看連通情況
連接數據庫
注意:這里Open函數的字符串需要和上面配置的數據源一致
BOOL CAdoMySQLHelper::MySQL_Connect(){
// 初始化OLE/COM庫環境
CoInitialize(NULL);
try{
// 通過名字創建Connection對象
HRESULT hr = this->m_pConnection.CreateInstance("ADODB.Connection");
if (FAILED(hr)){
AfxMessageBox(_T("創建_ConnectionPtr智能指針失敗"));
return false;
}
// 設置連接超時時間
this->m_pConnection->ConnectionTimeout = 600;
// 設置執行命令超時時間
this->m_pConnection->CommandTimeout = 120;
// 連接數據庫
this->m_pConnection->Open("DSN=MySQL5.5;Server=localhost;Database=ZD124UE_DEMO",
"root",
"root",
adModeUnknown);
}
catch (_com_error &e){
// 若連接打開,需要在異常處理中關閉和釋放連接
if ((NULL != this->m_pConnection) && this->m_pConnection->State){
this->m_pConnection->Close();
this->m_pConnection.Release();
this->m_pConnection = NULL;
}
// 非CView和CDialog需要使用全局函數AfxMessageBox
AfxMessageBox(e.Description());
}
return true;
}
斷開數據庫
void CAdoMySQLHelper::MySQL_Close(){
if ((NULL != this->m_pConnection) && (this->m_pConnection->State)){
this->m_pConnection->Close(); // 關閉連接
this->m_pConnection.Release();// 釋放連接
this->m_pConnection = NULL;
}
// 訪問完COM庫后,卸載COM庫
CoUninitialize();
}
執行SQL語句
這部分可以完成數據庫四大操作中的增、刪、改三大操作,也就是用一行SQL語句完成
BOOL CAdoMySQLHelper::MySQL_ExecuteSQL(CString sql){
_CommandPtr m_pCommand;
try{
m_pCommand.CreateInstance("ADODB.Command");
_variant_t vNULL;
vNULL.vt = VT_ERROR;
// 定義為無參數
vNULL.scode = DISP_E_PARAMNOTFOUND;
// 將建立的連接賦值給它
m_pCommand->ActiveConnection = this->m_pConnection;
// SQL語句
m_pCommand->CommandText = (_bstr_t)sql;
// 執行SQL語句
m_pCommand->Execute(&vNULL, &vNULL, adCmdText);
}
catch (_com_error &e){
// 需要在異常處理中釋放命令對象
if ((NULL != m_pCommand) && (m_pCommand->State)){
m_pCommand.Release();
m_pCommand = NULL;
}
// 非CView和CDialog需要使用全局函數AfxMessageBox
AfxMessageBox(e.Description());
return false;
}
return true;
}
查詢獲取返回值
查詢內容的方式實現如下面的代碼實現。同時為了可以查詢不同的表格,使用了void*
作為返回值,具體使用方法看下面
void* CAdoMySQLHelper::MySQL_Query(CString cond, CString table){
// 打開數據集SQL語句
_variant_t sql = "SELECT * FROM " + (_bstr_t)table + " WHERE " + (_bstr_t)cond;
OnRecord* pOnRecord = NULL;
RemainTime* pRemainTime = NULL;
try{
// 定義_RecordsetPtr智能指針
_RecordsetPtr m_pRecordset;
HRESULT hr = m_pRecordset.CreateInstance(__uuidof(Recordset));
if (FAILED(hr)){
AfxMessageBox(_T("創建_RecordsetPtr智能指針失敗"));
return (void*)false;
}
// 打開連接,獲取數據集
m_pRecordset->Open(sql,
_variant_t((IDispatch*)(this->m_pConnection), true),
adOpenForwardOnly,
adLockReadOnly,
adCmdText);
// 確定表不為空
if (!m_pRecordset->ADOEOF){
// 移動游標到最前
m_pRecordset->MoveFirst();
// 循環遍歷數據集
while (!m_pRecordset->ADOEOF){
if (table == ONTABLE) {
/********* Get UID ********/
_variant_t varUID = m_pRecordset->Fields->GetItem(_T("UID"))->GetValue();
varUID.ChangeType(VT_BSTR);
CString strUID = varUID.bstrVal;
/********* Get RemainSeconds ********/
_variant_t varRemainTime = m_pRecordset->Fields->GetItem(_T("RemainTime"))->GetValue();
varRemainTime.ChangeType(VT_INT);
int intRemainTime = varRemainTime.intVal;
/********** Get StartTime ******************/
_variant_t varStartTime = m_pRecordset->GetCollect(_T("StartTime"));
COleDateTime varDateTime = (COleDateTime)varStartTime;
CString strStartTime = varDateTime.Format(TIMEFORMAT);
/*********** Get isOverTime ***********/
_variant_t varIsOverTime = m_pRecordset->Fields->GetItem(_T("isOverTime"))->GetValue();
varIsOverTime.ChangeType(VT_BOOL);
bool boolIsOverTime = varIsOverTime.boolVal;
/************ Generate OnRecord ****************/
pOnRecord = new OnRecord(strUID, intRemainTime, strStartTime, boolIsOverTime);
}
else if(table == REMAINTIMETABLE) {
/********* Get UID ********/
_variant_t varUID = m_pRecordset->Fields->GetItem(_T("UID"))->GetValue();
varUID.ChangeType(VT_BSTR);
CString strUID = varUID.bstrVal;
/********* Get RemainSeconds ********/
_variant_t varRemainTime = m_pRecordset->Fields->GetItem(_T("RemainTime"))->GetValue();
varRemainTime.ChangeType(VT_INT);
int intRemainTime = varRemainTime.intVal;
/************ Generate RemainTime ****************/
pRemainTime = new RemainTime(strUID, intRemainTime);
}
break; // Only Return one struct
m_pRecordset->MoveNext();
}
}
}
catch (_com_error &e){
AfxMessageBox(e.Description());
}
if (table == ONTABLE)
return (void*)pOnRecord;
else
return (void*)pRemainTime;
}
調用方法如下:
void CAppdev::OnBnClickedBtnstartweb() {
OnRecord* pRecord = (OnRecord*)adoMySQLHelper.MySQL_Query(cond, ONTABLE);
// do something you want
delete(pRecord); // Important!
}
void CAppdev::OnBnClickedBtnexitweb() {
RemainTime* pRemainTime = (RemainTime*)adoMySQLHelper.MySQL_Query(cond, REMAINTIMETABLE);
// do something you want
delete(pRemainTime); // important!
}
定時器實現
首先需要創建一個定時器,調用SetTimer()
函數設置一個定時器並用一個UINT_PTR
記錄下該定時器,這里設置的定時器計時單位是毫秒;然后在消息響應函數OnTimer()
中對其進行相關的操作;切記定時器也是需要銷毀的,但需要在消息響應函數DestroyWindow()
中完成,不能在析構函數中完成
/* Appdev.h */
class CAppdev : public CDialogEx {
private:
UINT_PTR m_ActiveTimer;
};
/* Appdev.cpp */
void CAppdev::DoDataExchange(CDataExchange* pDX) {
CDialogEx::DoDataExchange(pDX);
// 啟動定時器
m_ActiveTimer = SetTimer(SCANTIMER_ID, SCANTIMER * 1000, NULL);
}
void CAppdev::OnTimer(UINT_PTR nIDEvent) {
// 在此添加消息處理程序代碼和/或調用默認值
switch (nIDEvent){
case SCANTIMER_ID:
// do something here
break;
default:
break;
}
CDialogEx::OnTimer(nIDEvent);
}
BOOL CAppdev::DestroyWindow() {
// 銷毀定時器
KillTimer(m_ActiveTimer);
return CDialogEx::DestroyWindow();
}
臨界區問題
軟件是一個讀寫器的上位機軟件,那么系統會定時更新數據庫即用戶的余時,並將已經超時的用戶移除和標記。若此時也有用戶訪問數據庫,由於ADO DB使用的是讀寫模式打開的,因此多並發訪問會出問題。因此,這個問題就變成了一個經典的臨界區問題,所以可以用經典的臨界區解法=。=在定時器操作數據庫的時候獲得鎖,定時器結束釋放鎖,用戶只有當定時器沒獲得鎖的時候才能訪問數據庫,其余情況阻塞
void CAppdev::OnBnClickedBtnretimedefinit() {
while (this->isWritingRemainTimeTable) { Sleep(100); } // 休眠0.1s等待定時器操作完成
adoMySQLHelper.MySQL_UpdateRemainTime(uid, DEFAULTREMAINTIME, REMAINTIMETABLE);
}
void CAppdev::OnTimer(UINT_PTR nIDEvent){
// 在此添加消息處理程序代碼和/或調用默認值
switch (nIDEvent){
case SCANTIMER_ID:
this->isWritingRemainTimeTable = true;
adoMySQLHelper.MySQL_ScanOnTable(SCANTIMER);
this->isWritingRemainTimeTable = false;
break;
default:
break;
}
CDialogEx::OnTimer(nIDEvent);
}
文件操作
在讀寫文件的時候,打開文件時添加參數CFile::modeNoTruncate
,該參數會打開指定路徑的文件,若沒有該文件,則新建文件后打開,因此不用考慮文件是否存在的情況
文件編碼
原本的文件只能支持英文,為了能支持中文甚至全部語言,選擇使用Unicode編碼格式,自定義文件的時候往文件頭添加Unicode編碼頭0xFEFF
即可讓程序知道該文件的編碼方式
BOOL FileUnicodeEncode(CFile &mFile) {
WORD unicode = 0xFEFF;
mFile.SeekToBegin();
mFile.Write(&unicode, 2); // Unicode
return true;
}
寫入文件
默認寫到文件末尾,不用煩惱各種插入問題,畢竟不是鏈表,插入比較麻煩
void CRecordHelper::SaveRecharges(CString uid, CString accounts, long remainings, CString result){
// 打開文件
CFile mFile(this->mSaveFile, CFile::modeCreate | CFile::modeNoTruncate | CFile::modeReadWrite);
// 獲取當前時間
CTime curTime = CTime::GetCurrentTime();
// 格式化輸出
CString contents;
contents.Format(_T("卡號:%s\r\n時間:%s\r\n結果:%s\r\n內容:用戶充值\r\n金額:%s\r\n余額:%d\r\n\r\n"),
uid, curTime.Format(TIMEFORMAT), result, accounts, remainings);
// 指向文件末尾並寫入
mFile.SeekToEnd();
mFile.Write(contents, wcslen(contents)*sizeof(wchar_t));
// 關閉文件
mFile.Close();
}
讀取文件
讀取文件由於要求用戶最新的內容輸出在最開頭,因此選擇讀取的時候倒序分段讀取,拼接字符串即可。寫入文件時使用CFile
類即可,但是要實現按行讀取使用CStdioFile
類比較方便,該類如其名,CStdioFIle::ReadString()
操作可以讀取一行,剩下的就是判斷一段即可
CString CRecordHelper::LoadRecords(){
// 打開文件
CStdioFile mFile(this->mSaveFile, CFile::modeCreate | CFile::modeNoTruncate | CFile::modeRead | CFile::typeUnicode);
// 指向開頭並循環讀入
mFile.SeekToBegin();
CString contents, line, multiLine;
contents.Empty();
multiLine.Empty();
// 倒序分段讀取
while (mFile.ReadString(line)) {
line.Trim();
if (line.IsEmpty()) {
contents = multiLine + _T("\r\n") + contents;
multiLine.Empty();
}
else {
multiLine += (line + _T("\r\n"));
}
}
contents = multiLine + _T("\r\n") + contents;
// 關閉文件並返回結果
mFile.Close();
return contents;
}
清空文件
這里調用構造CFile對象的時候去掉CFile::modeNoTruncate
參數,就默認新建一個文件了
BOOL CRecordHelper::EmptyRecords(){
// 清空文件
CFile mFile(this->mSaveFile, CFile::modeCreate | CFile::modeReadWrite);
FileUnicodeEncode(mFile);
mFile.Close();
return true;
}
運行截圖
這里貼兩張運行截圖,運行結果都是正確的,只是展示一下界面和字體及顏色