该博客涉及的完整工程托管在https://github.com/Wsine/UpperMonitor,以为好请给个Star (/▽\=)mysql
理论支持所有Win32运行环境git
在消息响应函数OnInitDialog()中完成整个框架的内容设置,包括插入Tab选项卡标签,关联对话框,调整大小和设置默认选项卡github
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()中完成选项卡切换,其实内容都一直存在,只是把非该选项卡的内容隐藏了,把该选项卡的内容显示出来,仅此而已web
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; } }
首先须要在工程中include相应的文件,就是已经封装好的ZM124U.lib和ZM124U.hsql
#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上面的说明。api
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接口,提供可用于关系型和非关系型数据源的接口[也就是能够操做电子表格、文本文件等]。
这部分的理论知识建议参考上面说起的参考内容
安装过程省略,首先须要建表,双击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语句完成
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; }
这里贴两张运行截图,运行结果都是正确的,只是展现一下界面和字体及颜色