目录
一、前言
人总是喜欢回忆过去,突然回忆起大学时光来,我的计算机老师是用自己做的点名工具,来抽人回答问题的。那么本次的主题就是做一个点名工具,咱们在点名的基础上加一个存储功能,在多次抽取的情况下进行存储。
二、主要技术点
Sqlite数据的增删改查、GDI/GDI+绘制、COM接口、文件数据读取、定时器。
三、准备工作
1.SQLite数据库操作工具
2.SqLiteCpp第三方库
GitHub - SRombauts/SQLiteCpp:SQLiteC++ (SQLiteCpp) 是一个智能且易于使用的 C++ SQLite3 包装器。
3.一个.txt格式的花名册
四、主界面
主界面包含三个控件:静态文本控件、按钮控件、编辑框控件。分别用于响应选择本地花名册文件、遍历花名册名单、显示花名册名单。
1.绘制背景图
映射WM_PAINT消息进行绘制,可以使用GDI或者GDI+方法。
MESSAGE_HANDLER(WM_PAINT, OnPaint)
GDI的方法:
LRESULT CRandomDlg::OnPaint(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
{
PAINTSTRUCT ps;
HDC hdc = BeginPaint(&ps);
HDC hdcMem = CreateCompatibleDC(hdc);
if (lstrlen(m_picPath) != 0)
{
HBITMAP hBitmap = (HBITMAP)LoadImage(NULL, m_picPath, IMAGE_BITMAP, 0, 0, LR_LOADFROMFILE | LR_DEFAULTSIZE);
HBITMAP oldBitmap = (HBITMAP)SelectObject(hdcMem, hBitmap);
BITMAP bitmap;
GetObject(hBitmap, sizeof(bitmap), &bitmap);
SetStretchBltMode(hdc, STRETCH_HALFTONE); //设置位图拉伸模式,解决模糊问题
StretchBlt(hdc, ps.rcPaint.left, ps.rcPaint.top, ps.rcPaint.right - ps.rcPaint.left,
ps.rcPaint.bottom - ps.rcPaint.top, hdcMem, 0, 0, bitmap.bmWidth, bitmap.bmHeight, SRCCOPY);
SelectObject(hdcMem, oldBitmap);
DeleteObject(hBitmap);
}
EndPaint(&ps);
DeleteDC(hdcMem);
return 0;
}
GDI+方法:
#include <gdiplus.h>
using namespace Gdiplus;
LRESULT CRandomDlg::OnPaint(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
{
PAINTSTRUCT ps;
HDC hdc = BeginPaint(&ps);
if (lstrlen(m_picPath) != 0)
{
Graphics gh(hdc);
RectF rect(ps.rcPaint.left, ps.rcPaint.top,
ps.rcPaint.right - ps.rcPaint.left,
ps.rcPaint.bottom - ps.rcPaint.top);
Image* image = Image::FromFile(m_picPath);
gh.DrawImage(image, rect);
}
EndPaint(&ps);
return 0;
}
如果你写代码所使用的框架没有初始化GDI+,那么需要我们手动初始化和释放:
//初始化GDI+
ULONG_PTR gdiplusToken;
GdiplusStartupInput gdiplusStartupInput;
GdiplusStartup(&gdiplusToken, &gdiplusStartupInput, NULL);
//释放GDI+
GdiplusShutdown(gdiplusToken);
2、实现读取花名册功能
映射WM_COMMAND消息。
MESSAGE_HANDLER(WM_COMMAND, OnCommand)
处理静态文本控件被点击时的通知消息STN_CLICKED,以此来弹出选择窗口选择花名册文件。我们使用IFileDialog接口来打开一个shell窗口进行文件选择,获取选择文件的路径之后通过CreateFile()函数打开文件,再通过ReadFile()函数读取其内容:
LRESULT CRandomDlg::OnCommand(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
{
int wmId = LOWORD(wParam);
int messageId = HIWORD(wParam);
switch (messageId)
{
case STN_CLICKED:
{
if (wmId == IDC_CHOOSESTATIC)
{
IFileDialog* pFileDialog = NULL;
//创建IFileDialog接口实例
HRESULT hr = CoCreateInstance(CLSID_FileOpenDialog, NULL, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pFileDialog));
if (SUCCEEDED(hr))
{
IShellItem* pItem = nullptr;
DWORD dwOptions;
hr = pFileDialog->GetOptions(&dwOptions);
//设置窗口选项
pFileDialog->SetOptions(dwOptions | FOS_FORCEFILESYSTEM | FOS_PATHMUSTEXIST | FOS_FILEMUSTEXIST);
//设置窗口标题
pFileDialog->SetTitle(L"请选择文件:");
//设置筛选器
COMDLG_FILTERSPEC fileSpec[] =
{
{ L"文本文件", L"*.txt"},
};
pFileDialog->SetFileTypes(1, fileSpec);
hr = pFileDialog->Show(GetWindow(IDD_RANDOMDIALOG));
//获取用户选择
if (SUCCEEDED(hr))
{
hr = pFileDialog->GetResult(&pItem);
if (SUCCEEDED(hr))
{
//获取选择文件的路径
hr = pItem->GetDisplayName(SIGDN_FILESYSPATH, &m_pszFilePath);
HANDLE hFile = CreateFile(m_pszFilePath, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if (hFile != INVALID_HANDLE_VALUE)
{
//打开文件读取内容
DWORD size = GetFileSize(hFile, NULL);
if (size != INVALID_FILE_SIZE)
{
nameVec.clear();
char* pBuffer = new char[size + 1];
memset(pBuffer, 0, size + 1);
DWORD dwBytesRead = 0;
WCHAR tmp[100] = { 0 };
if (ReadFile(hFile, pBuffer, size, &dwBytesRead, NULL))
{
splitString(pBuffer);
if (nameVec.empty())
MessageBox(L"所选文件没有内容!", L"Warning", MB_OK | MB_ICONWARNING);
else
{
stringToLPCWSTR(*nameVec.begin(), tmp);
SetDlgItemText(IDC_NAMESTATIC, tmp);
}
}
}
CloseHandle(hFile);
}
else
MessageBox(L"CreateFile Error!", L"Error", MB_OK | MB_ICONERROR);
}
else
{
MessageBox(L"GetResult Error!", L"Error", MB_OK | MB_ICONERROR);
}
}
// 释放资源
pFileDialog->Release();
CoTaskMemFree(m_pszFilePath);
m_pszFilePath = nullptr;
}
else
{
MessageBox(L"CoCreateInstance Error!", L"Error", MB_OK | MB_ICONERROR);
}
}
效果图:
3.实现遍历花名册功能
我们通过响应按钮被按下的通知消息BN_CLICKED,来开始遍历花名册。将选中的名字显示在主窗口上,再用定时器来延迟3秒之后清除显示,通过AnimateWindow()函数将显示和清除动作加上动画效果。
LRESULT CRandomDlg::OnCommand(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
{
int wmId = LOWORD(wParam);
int messageId = HIWORD(wParam);
switch (messageId)
{
case BN_CLICKED:
{
if (wmId == IDC_STOPBUTTON)
{
//响应开始/停止按钮
isStop = !isStop;
BOOL errorFlag = FALSE;
HANDLE handlePro = GetCurrentProcess();
SetUserObjectInformation(handlePro, UOI_TIMERPROC_EXCEPTION_SUPPRESSION, &errorFlag, sizeof(BOOL));
if (isStop)
{
SetDlgItemText(IDC_STOPBUTTON, L"开始");
KillTimer(ID_SHOWNAME);
WCHAR tmpStr[100] = { 0 };
stringToLPCWSTR(nameVec.at(index), tmpStr);
choosedVec.push_back(tmpStr);
HWND hwnd = GetDlgItem(IDC_POPSTATIC);
::SetWindowText(hwnd, tmpStr);
AnimateWindow(hwnd, 300, AW_ACTIVATE | AW_HOR_POSITIVE); //动态显示窗口
SetTimer(ID_POPNAME, 3000, PopNameProc);
}
else
{
if (nameVec.empty())
{
MessageBox(L"未选择抽奖名单或者名单为空", L"Warning", MB_OK | MB_ICONWARNING);
isStop = TRUE;
return FALSE;
}
::ShowWindow(GetDlgItem(IDC_POPSTATIC), SW_HIDE);
SetDlgItemText(IDC_STOPBUTTON, L"停止");
SetTimer(ID_SHOWNAME, 50, ShowNameProc);
InvalidateRect(0, 1);
UpdateWindow();
}
}
}
效果图:
4.实现储存功能
存储方式有很多种,我们本次是使用储存到数据库的方法。使用第三方库SqliteCPP操作sqlite数据库。
4.1创建数据库
我们需要在程序初始化时创建一个数据库文件,并且创建储存数据的表。
创建表指令:
CREATE TABLE 表名 (列名1 列1对应值的类型,……, 列名n 列n对应值的类型)
try
{
//创建数据库文件
if (!PathFileExists(L"C\\ToolBox"))
CreateDirectory(L"C:\\ToolBox", NULL);
SQLite::Database db("C:\\ToolBox\\TB.db", SQLite::OPEN_READWRITE | SQLite::OPEN_CREATE);
//创建表
string sql = R"(
CREATE TABLE IF NOT EXISTS RANDOM (
id INTEGER PRIMARY KEY NOT NULL,
name TEXT NOT NULL,
week INTEGER NOT NULL,
todayDate TEXT NOT NULL
)
)";
db.exec(sql);
}
catch (const exception& e)
{
MessageBoxA(NULL, e.what(), "Create table failed!", MB_OK | MB_ICONERROR);
}
4.2存储数据到数据库表
我们在第3点中响应按钮消息,将抽中的名单暂时存储到一个vector容器中,再统一保存到本地数据库。
插入数据指令 :
INSERT INTO 表名 (列名1,……,列名n)VALUES (列名1 对应的值,……,列名n对应的值)
try
{
SQLite::Database db("C:\\ToolBox\\TB.db", SQLite::OPEN_READWRITE | SQLite::OPEN_CREATE);
string sql;
for (int i = 0; i < choosedVec.size(); i++)
{
sql = "INSERT INTO RANDOM (name, week, todayDate) VALUES (?,?,?)";
SQLite::Statement query(db, sql);
query.bind(1, UnicodeToUtf8(choosedVec.at(i)));
query.bind(2, m_curWeek);
query.bind(3, UnicodeToUtf8(m_curDate));
query.exec();
}
nameVec.clear();
choosedVec.clear();
if (m_staticFont != NULL)
DeleteObject(m_staticFont);
if (m_titleFont != NULL)
DeleteObject(m_titleFont);
if (m_bkBrush != NULL)
DeleteObject(m_bkBrush);
}
catch (const exception& e)
{
MessageBoxA(NULL, e.what(), "Store to DB failed!", MB_OK | MB_ICONERROR);
return 0;
}
注意:sqlite数据库默认的编码模式是UTF-8,所以当我们上传的数据是字符串格式时且编译器设置的字符集非UTF8格式,那么需要先转换为UTF-8格式再上传,否则数据库存储的数据可能就是乱码了。
再储存完成之后我们可以使用事先准备的数据库工具来打开创建的数据库,以此查看数据:
4.3读取数据库表数据
我们可以在程序中读取数据库中储存的数据,读取数据指令:
SELECT 列名 FROM 表名 读取表中某一列的数据
SELECT * FROM 表名 读取整个表的数据
WCHAR date[50] = { 0 };
GetNowDateString(date);
wstring total;
try
{
SQLite::Database db(L"C:\\ToolBox\\TB.db", SQLite::OPEN_READWRITE | SQLite::OPEN_CREATE);
SQLite::Statement query2(db, "SELECT name FROM RANDOM WHERE todayDate = ?");
query2.bind(1, UnicodeToUtf8(date));
while (query2.executeStep())
{
wstring data;
UTF8ToUniocde(query2.getColumn(0).getString().c_str(), data);
total += data + L";";
}
if (!total.empty())
{
MessageBox(hWnd, total.c_str(), L"上期中奖名单", MB_OK);
}
else
{
MessageBox(hWnd, L"暂无数据", L"Tip", MB_OK | MB_ICONINFORMATION);
}
}
catch (const exception& e)
{
MessageBoxA(NULL, e.what(), "GetData failed!", MB_OK | MB_ICONERROR);
return FALSE;
}
效果图: