目录
一、前言
之前的文章一直用的是Windows的API以及ATL的框架来绘制控件的,那本次使用MFC框架来做一下小工具。
二、主要功能和技术点
1.主要功能
1)支持对数据的存储、修改以及查询。
2)可以显示本周的数据详情。
3)支持导出本周的数据以及所有的数据为EXCEL文件。
4)支持自定义窗口背景图片。
2.主要技术点
Sqlite数据库的增删改查、GDI/GDI+绘制、EXCEL接口、控件自绘、正则表达式。
三、准备工作
1.SQLite数据库操作工具
详情见《基于Windows系统用C++做一个点名工具》文章。
2.SqLiteCpp第三方库
详情见《基于Windows系统用C++做一个点名工具》文章。
3.安装office导入Excel需要的接口
3.1具体步骤
1)右击【项目】--》 点击【添加】--》点击【新建项】
2)在弹出来的窗口中选择【MFC】-->选择【TypeLib中的MFC类】,点击添加。
3)在弹出来的窗口中的【实现接口的位置】中选择【文件】,再点击【...】在你的EXCEL.EXE所在的路径点击打开。
4)按【>】添加以下七个基础接口:
_Application、_Workbook、_Worksheet、Worksheets、Workbooks、Range、Font
点击确定,可以在头文件中看见已经已经添加完成:
5)分别打开这个七个头文件,将导入的代码注释掉。
同时把CFont类重新改一个名,因为std命名空间中也有相同的类,会冲突。
头文件名也改一下
6)在我们的程序中添加这七个头文件,编译。
7)如果编译报错,那么我们根据报错提示改一下头文件中的代码
①CRange中的VARIANT DialogBox() 改为 VARIANT _DialogBox()
②CWorksheets 和 CWorkbooks中的InvokeHelper方法如果缺少参数则用NULL填充
③出现未定义的标识符XmlMap,使用void代替。
注意:本文用到了单元格填充颜色,所以还需要按以上的方法添加Interior接口,头文件是<Cnterior.h>。
四、主界面
界面由两个窗口组成:输入窗口和数据展示窗口。
1)输入窗口由静态文本、编辑框、菜单栏以及按钮组成
2)数据展示窗口由静态文本、列表视图以及按钮组成
无论是Windows底层api还是基于mfc的框架,都是通过处理窗口消息来实现窗口以及控件的绘制。mfc框架只是在Windows底层api的基础上对其进行了封装而已,所以通过mfc封装的宏来映射窗口消息即可。因为mfc封装了大量的消息映射函数,所以大部分都不需要自己手动映射,我们只需要在控件的属性中点击【黄色小闪电的标志】,然后在下方选择要映射函数即可。
1.自定义窗口背景
1.1消息映射
映射窗口的WM_PAINT消息,进行背景绘制。
BEGIN_MESSAGE_MAP(CAccountDlg, CDialogEx)
ON_WM_PAINT()
END_MESSAGE_MAP()
1.2选择背景图片
//设置筛选器
TCHAR szFilter[] = _T("BMP图片(*.bmp)|*.bmp*|JPG图片(*.jpg)|*.jpg*|PNG图片(*.png)|*.png*|所有文件(*.)|*.*|");
CFileDialog fileDlg(TRUE, _T("doc"), _T(""), OFN_OVERWRITEPROMPT, szFilter, this);
// 构造打开文件对话框
if (IDOK == fileDlg.DoModal())
{
//获取选择文件的路径
m_bkPicPth = fileDlg.GetPathName();
SaveToPathDB(DB_FULLPATH, m_bkPicPth);
this->InvalidateRect(0, 1);
this->UpdateWindow();
}
1.3绘制背景
使用GDI+用图片来绘制背景,初始状态下默认浅蓝色背景。
PAINTSTRUCT ps;
CDC* pDC = BeginPaint(&ps);
Gdiplus::Graphics gh(pDC->GetSafeHdc());
if (m_bkPicPth.GetLength() > 0)
{
Gdiplus::Image* image = Gdiplus::Image::FromFile(m_bkPicPth.GetString());
gh.DrawImage(image, Gdiplus::RectF(ps.rcPaint.left, ps.rcPaint.top,
ps.rcPaint.right - ps.rcPaint.left, ps.rcPaint.bottom - ps.rcPaint.top));
}
else
{
Gdiplus::SolidBrush brush(Gdiplus::Color(120, 200, 250));
gh.FillRectangle(&brush, Gdiplus::RectF(ps.rcPaint.left, ps.rcPaint.top,
ps.rcPaint.right - ps.rcPaint.left, ps.rcPaint.bottom - ps.rcPaint.top));
}
1.4静态控件透明化
我们如果自绘了背景,那么静态文本的留白会让界面不那么美观,所以我们需要对静态文本控件的背景透明化。那么就需要映射处理WM_CTLCOLORSTATIC消息。但是在mfc中把所有控件的绘制背景的消息都封装成了ON_WM_CTLCOLOR宏,所以我们映射该宏,在消息处理函数中对其进行判断处理即可。
BEGIN_MESSAGE_MAP(CAccountDlg, CDialogEx)
ON_WM_CTLCOLOR()
END_MESSAGE_MAP()
HBRUSH CAccountDlg::OnCtlColor(CDC* pDC, CWnd* pWnd, UINT nCtlColor)
{
if (nCtlColor == CTLCOLOR_STATIC)
{
UINT id = ::GetDlgCtrlID(pWnd->GetSafeHwnd());
if(id == IDC_TODAYEDIT)
return (HBRUSH)GetStockObject(WHITE_BRUSH);
else
{
SetTextColor(pDC->GetSafeHdc(), RGB(255, 255, 255));
SetBkColor(pDC->GetSafeHdc(), RGB(255, 255, 255));
SetBkMode(pDC->GetSafeHdc(), TRANSPARENT);
return (HBRUSH)GetStockObject(NULL_BRUSH);
}
}
return (HBRUSH)GetStockObject(WHITE_BRUSH);
}
2.实现统计功能
2.1消息映射
响应编辑框的内容改变,映射EN_CHANGE或者EN_UPDATE消息。
BEGIN_MESSAGE_MAP(CAccountDlg, CDialogEx)
ON_EN_UPDATE(IDC_BREAKFASTEDIT, &CAccountDlg::OnEnUpdateBreakfastedit)
END_MESSAGE_MAP()
2.2数据统计
//判断字符串是否为合法数字字符串
static bool IsNumCharacterStr(CString str)
{
// 定义一个正则表达式,仅匹配正整数或正浮点数
std::regex pattern("^\\d+(\\.)?\\d{0,2}$");
std::string numStr = WCharToString(str.GetString());
return std::regex_match(numStr, pattern);
}
TCHAR text[100] = {};
SendDlgItemMessage(itemID, WM_GETTEXT, 100, (LPARAM)text);
if (IsNumCharacterStr(text))
{
num = _wtof_l(text, NULL);
}
else
{
SendDlgItemMessage(itemID, WM_SETTEXT, NULL, (LPARAM)L"0");
num = 0;
}
m_totalNum = m_breakfastNum + m_lunchNum + m_dinnerNum +
m_fruitNum + m_elecProductNum + m_dailyProductNum +
m_relaxationNum + m_medicalNum + m_petNum + m_educationNum +
m_trafficNum + m_rentNum + m_otherNum;
CString totalStr;
totalStr.Format(L"%.2lf", m_totalNum);
SendDlgItemMessage(IDC_TODAYEDIT, WM_SETTEXT, NULL, (LPARAM)totalStr.GetString());
if (m_totalNum >= 0.01)
{
CWnd* cwnd = GetDlgItem(IDC_SAVEBUTTON);
cwnd->EnableWindow();
}
3.实现保存、详情功能
3.1消息映射
响应按钮按下,映射BN_CLICKED消息。
BEGIN_MESSAGE_MAP(CAccountDlg, CDialogEx)
ON_BN_CLICKED(IDC_SAVEBUTTON, &CAccountDlg::OnBnClickedSavebutton)
ON_BN_CLICKED(IDC_DETAILBUTTON, &CAccountDlg::OnBnClickedDetailbutton)
END_MESSAGE_MAP()
3.2保存数据到Sqlite数据库
//保存数据到本地数据库
static BOOL SaveToDB(CString path, ACCOUNTDATA data)
{
try
{
std::string fullPath = UnicodeToUtf8(path.GetString());
Database db(fullPath, OPEN_READWRITE);
std::string sql = R"(INSERT OR REPLACE INTO ACCOUNT
(week, breakfast,lunch,dinner,fruit,elecProduct,dailyProduct,
relaxation,medical,pet,education,traffic,rent,other,total,date)
VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
)";
Statement query2(db, sql);
int i = 1;
//绑定数据
query2.bind(i++, data.week);
query2.bind(i++, data.breakfast);
query2.bind(i++, data.lunch);
query2.bind(i++, data.dinner);
query2.bind(i++, data.fruit);
query2.bind(i++, data.elecProduct);
query2.bind(i++, data.dailyProduct);
query2.bind(i++, data.relaxation);
query2.bind(i++, data.medical);
query2.bind(i++, data.pet);
query2.bind(i++, data.education);
query2.bind(i++, data.traffic);
query2.bind(i++, data.rent);
query2.bind(i++, data.other);
query2.bind(i++, data.total);
query2.bind(i, UnicodeToUtf8(data.date.GetString()));
//执行
query2.exec();
}
catch (const std::exception& e)
{
MessageBoxA(NULL, e.what(), "Save to db failed", MB_OK | MB_ICONERROR);
return FALSE;
}
return TRUE;
}
if (SaveToDB(DB_FULLPATH, data))
{
SelectInDB(DB_FULLPATH, m_startDate, m_endDate, m_accountVec);
int size = m_accountVec.size() - 1;
GetTypeTotal(m_accountVec.at(size), m_accountVec);
this->MessageBox(_T("保存成功"), _T("Success"), MB_OK | MB_ICONINFORMATION);
CWnd* cwnd = GetDlgItem(IDC_SAVEBUTTON);
cwnd->EnableWindow(0);
}
3.3显示详情窗口
this->ShowWindow(SW_HIDE);
CDetailDlg detailDlg;
float dpi = GetDpiForOwnWindow(this->GetSafeHwnd());
detailDlg.Init(m_accountVec, m_curDate, m_bkPicPth, dpi);
detailDlg.DoModal();
this->ShowWindow(SW_SHOW);
4.实现导出Excel功能
4.1消息映射
同样是响应按钮按下的消息,映射BN_CLICKED消息。
BEGIN_MESSAGE_MAP(CDetailDlg, CDialogEx)
ON_BN_CLICKED(IDC_EXPORTALLBUTTON, &CDetailDlg::OnBnClickedExportallbutton)
END_MESSAGE_MAP()
4.2导出数据到excel中
CApplication app;
CWorkbooks books;
CWorkbook book;
CWorksheets sheets;
CWorksheet sheet;
CRange range;
COleVariant covOptional((long)DISP_E_PARAMNOTFOUND, VT_ERROR);
books = app.get_Workbooks();
book = books.Add(covOptional);
sheets = book.get_Worksheets();
sheet = sheets.get_Item(COleVariant((short)1));
range.AttachDispatch(sheet.get_Cells());//得到全部单元格
int rowNum = m_mylist.GetItemCount(); //所有数据行(不包括列头)
CHeaderCtrl* pHead = m_mylist.GetHeaderCtrl();
int colNum = pHead->GetItemCount(); //所有列头
//写入列头
TCHAR colName[MAX_PATH] = { 0 };
for (int col = 0; col < colNum; col++)
{
range.put_Item(_variant_t((long)(1)), variant_t((long)(col + 1)),
_variant_t(columnhead[col]));
}
//写入数据
for (int i = 0; i < rowNum; i++)
{
CString text;
for (int j = 0; j < colNum; j++)
{
//从excel的第二行开始写入列表数据。
range.put_Item(_variant_t((long)(i + 2)), variant_t((long)(j + 1)),
_variant_t(text.GetString()));
}
}
设置指定范围的单元格背景颜色
range.AttachDispatch(sheet.get_Range(_variant_t(rowStr), _variant_t(colStr)));
Cnterior it;
it.AttachDispatch(range.get_Interior());
it.put_Color(_variant_t(RGB(140, 255, 170)));
注意:无论是从列表上获取数据还是从容器中获取数据保存到excel中,excel的表格行列索引都是从1开始的。
五、源码
见压缩包。