MFC 项目:简易销售系统实践
预览
源链接:https://blog.iyatt.com/?p=19158
测试环境
- https://blog.iyatt.com/?p=18843#%E7%8E%AF%E5%A2%83
项目
参考黑马程序员的案例教程进行实践的记录,部分内容自行修改过,比如原案例直接读写文件保存账号、密码、数据,这里改为使用 SQLite3 数据库。
注意 VS 默认头文件扩展名用的 .h,我是喜欢在 C++ 中使用扩展名 .hpp,只要不是模板创建的代码部分,是我自己添加的都使用 .hpp 扩展名。
源码:下载地址见文首源链接
新建项目
新建一个 MFC 应用,项目名 saleSystem,应用程序类型选单个文档,项目样式选MFC standard
用户界面功能中经典菜单选项选无,再点完成进行创建
不使用安全函数
在项目名称上右键属性
展开配置属性->C/C++->代码生成,在安全检查中选禁用安全检查
添加窗口图标
将 user.ico 放到项目目录下的res目录里
切换到资源视图选项卡,展开上面的资源分支,在 Icon 上右键添加资源
点导入
选择图标文件
将ID改为IDI_ICON_WIN
在类视图下,CMainFrame类中的OnCreate方法里添加代码
// 加载图标
HICON winIcon = AfxGetApp()->LoadIconW(IDI_ICON_WIN);
// 设置小图标
SetIcon(winIcon, FALSE);
按{F5}
调试运行,点是加载图标文件
运行效果
设置窗口大小和居中显示
在类视图下,CMainFrame类中的OnCreate方法里添加代码
// 设置位置(0,0)和窗口大小(800×600)
MoveWindow(0, 0, 800, 600);
// 居中显示
CenterWindow();
设置窗口标题
在资源视图下,展开资源树,双击打开 String Table,在底部添加一个 ID 为ID_STRING_PROJECTNAME,值为销售系统
在类视图下CsaleSystemDoc类中OnNewDocument方法下添加代码
CString projectName;
projectName.LoadStringW(ID_STRING_PROJECTNAME); // 导入字符串资源
SetTitle(projectName); // 设置窗口标题
运行效果
设计 SQLite3 数据库读写实现
这里实现账号、密码、商品信息的读写,数据库采用 SQLite3,配置 SQLite3 环境参考:https://blog.iyatt.com/?p=19187
手动创建一个数据库文件
在项目目录下打开终端,执行命令打开数据库文件 saleSystem.sb(不存在会自动创建),打开后会处于命令交互模式
sqlite3 saleSystem.sqlite3
新建一张表用于存储账号、密码,并写入初始账号、密码(账号:admin,密码:123456)
create table users
(
id integer primary key autoincrement ,
username text not null unique ,
password text not null
);
insert into users (username, password) values ('admin', '123456');
在创建一张表用于存储商品数据,并插入几条商品数据
create table products (
id integer primary key autoincrement ,
name text not null unique ,
price real not null ,
stock integer not null
);
insert into products (name, price, stock) values
('桌子', 199.9, 5),
('椅子', 49.8, 10);
退出交互模式
.exit
读写实现
在类视图下,项目上右键添加->类
创建一个类,类名StoreManager
StoreManager.hpp
#pragma once
extern "C"
{
#include "sqlite3.h"
}
#include <vector>
typedef struct
{
int id;
CString name;
double price;
int stock;
}productStruct;
typedef struct
{
CString username;
CString password;
}loginInfoStruct;
typedef std::vector<productStruct> productsVector;
class StoreManager
{
private:
static sqlite3* db;
static productStruct product;
static loginInfoStruct loginInfo;
private:
static int readLoginInfoCallback(void* data, int argc, char** argv, char** colName);
/**
* @brief UTF-8 编码窄字符串转 GBK 编码 CString
* @param utf8Str
* @return
*/
static CString utf8ToGbk(const char* utf8Str);
/**
* @brief GBK 编码 CString 转 UTF-8 窄字符串
* @param gbkStr
* @return
*/
static char* gbkToUtf8(const CString& gbkStr);
public:
/**
* @brief 连接数据库
* @param databasePath 数据库文件路径
*/
static void connect(CString& databasePath);
/**
* @brief 读取登录信息
* @param username 用户名
* @param password 密码
*/
static void readLogin(CString& username, CString& password);
/**
* @brief 修改密码
* @param username 要修改密码的用户
* @param password 新密码
*/
static bool writePassword(CString& username, CString& password);
/**
* @brief 关闭数据库
*/
static void close();
/**
* @brief 读取商品信息
* @param products 商品信息数组
*/
static void readProducts(productsVector& products);
/**
* @brief 写入商品信息
* @param products 商品信息数据
*/
static void writeProducts(productsVector& products);
/**
* @brief 修改商品信息
* @param products
*/
static void modifyProducts(productsVector& products);
};
StoreManager.cpp
#include "pch.h"
#include "StoreManager.hpp"
#include <string>
#include <stdexcept>
sqlite3* StoreManager::db = nullptr;
loginInfoStruct StoreManager::loginInfo;
productStruct StoreManager::product;
void StoreManager::connect(CString& databasePath)
{
if (StoreManager::db != nullptr)
{
return;
}
CW2A databasePathA(databasePath.GetString()); // 宽字符串转普通字符串
if (sqlite3_open(databasePathA, &StoreManager::db) != SQLITE_OK)
{
std::string error = "打开数据库失败:" + std::string(sqlite3_errmsg(StoreManager::db));
throw std::runtime_error(error);
}
}
void StoreManager::close()
{
if (StoreManager::db != nullptr)
{
sqlite3_free(StoreManager::db);
StoreManager::db = nullptr;
}
}
int StoreManager::readLoginInfoCallback(void* data, int argc, char** argv, char** colName)
{
(void)data;
(void)argc;
(void)colName;
StoreManager::loginInfo.username = argv[1];
StoreManager::loginInfo.password = argv[2];
return 0;
}
void StoreManager::readLogin(CString& username, CString& password)
{
if (StoreManager::db == nullptr)
{
std::string error = "请连接数据库后再读取登录信息";
throw std::runtime_error(error);
}
const char* sqlA = "select * from users";
char* errorA = nullptr;
if (sqlite3_exec(StoreManager::db, sqlA, StoreManager::readLoginInfoCallback, nullptr, &errorA) != SQLITE_OK)
{
std::string error = "读取登录信息失败:" + std::string(errorA);
sqlite3_free(errorA);
throw std::runtime_error(error);
}
username = StoreManager::loginInfo.username;
password = StoreManager::loginInfo.password;
}
bool StoreManager::writePassword(CString& username, CString& password)
{
if (StoreManager::db == nullptr)
{
std::string error = "请连接数据库后再读取登录信息";
throw std::runtime_error(error);
}
CString sql;
sql.Format(L"update users set password = '%s' where username = '%s'", password.GetString(), username.GetString());
CW2A sqlA(sql);
char* errorA = nullptr;
if (sqlite3_exec(StoreManager::db, sqlA, nullptr, nullptr, &errorA) != SQLITE_OK)
{
CA2W errorW(errorA);
AfxMessageBox(errorW);
sqlite3_free(errorA);
return false;
}
return true;
}
CString StoreManager::utf8ToGbk(const char* utf8Str)
{
int wideCharLen = MultiByteToWideChar(CP_UTF8, 0, utf8Str, -1, nullptr, 0);
if (wideCharLen <= 0)
{
return CString();
}
wchar_t* wideCharStr = new wchar_t[wideCharLen];
MultiByteToWideChar(CP_UTF8, 0, utf8Str, -1, wideCharStr, wideCharLen);
int gbkLen = WideCharToMultiByte(CP_ACP, 0, wideCharStr, -1, nullptr, 0, nullptr, nullptr);
if (gbkLen <= 0)
{
delete[] wideCharStr;
return CString();
}
char* gbkStr = new char[gbkLen];
WideCharToMultiByte(CP_ACP, 0, wideCharStr, -1, gbkStr, gbkLen, nullptr, nullptr);
CString result(gbkStr);
delete[] wideCharStr;
delete[] gbkStr;
return result;
}
char* StoreManager::gbkToUtf8(const CString& gbkStr)
{
// 获取宽字符字符串的长度
int wideCharLen = gbkStr.GetLength();
if (wideCharLen <= 0)
{
return nullptr; // 如果字符串为空,直接返回 nullptr
}
// 将 CString 转换为宽字符数组
const WCHAR* gbkW = gbkStr.GetString();
// 获取需要的 UTF-8 编码字符串的长度(包括结尾的 '\0')
int utf8Len = WideCharToMultiByte(CP_UTF8, 0, gbkW, -1, nullptr, 0, nullptr, nullptr);
if (utf8Len <= 0)
{
return nullptr; // 如果转换失败,返回 nullptr
}
// 分配内存用于存储 UTF-8 编码的字符串
char* utf8Str = new char[utf8Len];
// 执行转换
if (WideCharToMultiByte(CP_UTF8, 0, gbkW, -1, utf8Str, utf8Len, nullptr, nullptr) == 0)
{
delete[] utf8Str; // 如果转换失败,释放已分配的内存
return nullptr;
}
return utf8Str; // 返回转换后的 UTF-8 字符串
}
void StoreManager::readProducts(productsVector& products)
{
if (StoreManager::db == nullptr)
{
std::string error = "请连接数据库后再读取商品信息";
throw std::runtime_error(error);
}
products.clear();
const char* sqlA = "select * from products";
char** result = nullptr;
char* errorA = nullptr;
int rows, cols;
if (sqlite3_get_table(StoreManager::db, sqlA, &result, &rows, &cols, &errorA) != SQLITE_OK)
{
CA2W errorW(errorA);
AfxMessageBox(errorW);
sqlite3_free(errorA);
return;
}
productStruct product;
for (int row = 1; row <= rows; ++row)
{
product.id = std::stoi(result[row * cols + 0]);
product.name = StoreManager::utf8ToGbk(result[row * cols + 1]);
product.price = std::stod(result[row * cols + 2]);
product.stock = std::stoi(result[row * cols + 3]);
products.push_back(product);
}
}
void StoreManager::writeProducts(productsVector& products)
{
if (StoreManager::db == nullptr)
{
std::string error = "请连接数据库后再写入商品信息";
throw std::runtime_error(error);
}
CString sql;
char* errorA = nullptr;
for (productStruct product : products)
{
sql.Format(L"insert into products (name, price, stock) values ('%s', %f, %d)", product.name.GetString(), product.price, product.stock);
char* sqlA = StoreManager::gbkToUtf8(sql);
if (sqlite3_exec(StoreManager::db, sqlA, nullptr, nullptr, &errorA) != SQLITE_OK)
{
CA2W errorW(errorA);
AfxMessageBox(errorW);
sqlite3_free(errorA);
delete[] sqlA;
break;
}
else
{
delete[] sqlA;
}
}
}
void StoreManager::modifyProducts(productsVector& products)
{
if (StoreManager::db == nullptr)
{
std::string error = "请连接数据库后再修改商品信息";
throw std::runtime_error(error);
}
CString sql;
char* errorA = nullptr;
for (productStruct product : products)
{
sql.Format(L"update products set price = %f, stock = %d where name = '%s'", product.price, product.stock, product.name.GetString());
char* sqlA = StoreManager::gbkToUtf8(sql);
if (sqlite3_exec(StoreManager::db, sqlA, nullptr, nullptr, &errorA) != SQLITE_OK)
{
CA2W errorW(errorA);
AfxMessageBox(errorW);
sqlite3_free(errorA);
delete[] sqlA;
break;
}
else
{
delete[] sqlA;
}
}
}
保证程序退出时关闭数据库
在类视图下,CsaleSystemApp类中,找一个位置写入。注意要引用 StoreManager.hpp 头文件
struct Release
{
~Release()
{
StoreManager::close();
}
} release;
可以把 C++ 中的 struct 看作是默认 public 的 class,这里析构函数需要 public,使用 struct 就不需要额外写一个public:,然后实例化一个对象,这样程序结束的时候就会自动调用这个析构函数,完成资源的释放。
登录对话框
界面设计
在资源视图下,Dialog上右键插入
将对话框 ID 改为 DIALOG_LOGIN
添加控件,并设置好描述文字,设置窗口标题
(使用 Static Text、Edit Control 和 Button)
在对话框空白处右键添加类
创建一个类 LoginDialog
为用户名编辑框创建变量usernameEditControl,访问选private
同样为密码编辑框创建变量passwordEditControl
功能实现
让登录对话框在文档之前创建
在类视图下,CsaleSystemApp 类中的 InitInstance 方法中
找到**CWinApp::InitInstance();**的位置,在它之前以模态的方式运行登录对话框。注意要引用对话框的头文件 LoginDialog.hpp。
然后判断返回值,后续实现中如果登录成功才会调用 OnOK,这边得到的返回值就是 IDOK,如果不是那就说明点击了取消登录或右上角的关闭按钮,以及其它操作,这时候就要直接返回,就不会创建后续文档页面。
LoginDialog loginDialog;
if (loginDialog.DoModal() != IDOK)
{
return TRUE;
}
这时候调试运行就会先显示登录对话框,但是关闭登录对话框后会显示文档页面,后续还要处理这个逻辑,只有登录验证才应该显示后续的文档。
数据库连接
在资源视图下,StringTable 添加一个字段 DATABASE_PATH,设置 SQLite3 数据库文件的路径,这里就在项目目录下,使用相对路径
类视图下,在登录对话框类LoginDialog上右键属性
上面点击图标切换到重写页面,添加(重写) OnInitDialog
然后回到 OnInitDialog 函数中,添加连接数据库的代码。注意前面要引用数据库读写实现的头文件 StoreManager.hpp。
CString databasePath;
databasePath.LoadStringW(DATABASE_PATH);
StoreManager::connect(databasePath);
登录按钮回调实现
双击登录按钮会直接创建点击事件的回调
写入代码
// 读取用户输入的用户名和密码
CString inputUsername, inputPassword;
this->usernameEditControl.GetWindowTextW(inputUsername);
this->passwordEditControl.GetWindowTextW(inputPassword);
// 读取数据库中的用户名和密码
CString username, password;
StoreManager::readLogin(username, password);
// 判断用户名和密码
if (inputUsername == username)
{
if (inputPassword != password)
{
MessageBox(L"密码错误");
this->usernameEditControl.SetWindowTextW(L"");
this->passwordEditControl.SetWindowTextW(L"");
}
else
{
MessageBox(L"登录成功");
CDialogEx::OnOK();
}
}
else
{
MessageBox(L"用户名不存在");
this->usernameEditControl.SetWindowTextW(L"");
this->passwordEditControl.SetWindowTextW(L"");
}
调试运行
取消按钮回调实现
双击取消按钮,创建点击事件回调
点击取消调用取消方法
CDialogEx::OnCancel();
修改回车键为登录
对话框默认状态下按回车会调用 CDialogEx::OnOK(),这就会让登录验证成为摆设,相当于点击了登录对话框的 OK,那么接下来就会直接进入文档页面。
另外按照一般使用习惯回车键就是确认输入,这里也就是确认登录。
在类视图下,LoginDialog类上右键属性,重写对话框的 OnOK
注释掉CDialogEx::OnOK();,然后调用登录按钮点击事件的回调方法,这就按回车就等于是点击登录按钮。
this->OnBnClickedButton1();
静态拆分窗口
自定义视图类
在类视图中项目名上右键类向导
下拉MFC 类
类名 SelectView,基类CTreeView
同样再添加一个类DisplayView,基类CFormView
创建完上面两个类,在类向导页面点确定,编译在下面可能看到一堆 SelectView 头文件和源文件的报错
在头文件增加引用 #include “afxcview.h”,
再编译就好了
拆分窗口
在类视图中,点开 CMainFrame 类,声明一个对象
private:
CSplitterWnd splitter;
在CMainFrame上右键属性
重写 OnCreateClient
把原来的返回注释了,重新写
注意需要引用 SelectView.hpp 和 DisplayView.hpp 头文件
// 拆分为 1 行 2 列
this->splitter.CreateStatic(this, 1, 2);
// 创建左侧视图
this->splitter.CreateView(0, 0, RUNTIME_CLASS(SelectView), CSize(200, 500), pContext);
// 创建右侧视图
this->splitter.CreateView(0, 1, RUNTIME_CLASS(DisplayView), CSize(600, 500), pContext);
return TRUE;
调试运行,登录后可以看到下图的布局
左侧树视图
添加功能节点
资源视图下,Icon右键添加资源
导入
选择 re.ico 文件
ID 重设为 IDI_ICON_RE
类视图下双击SelectView类,在类头文件中增加
private:
CTreeCtrl* treeCtrl;
CImageList imageList;
SelectView 类上右键属性
重写 OnInitialUpdate
写入
// 加载图标
HICON icon = AfxGetApp()->LoadIconW(IDI_ICON_RE);
// 创建图片列表
this->imageList.Create(30, 30, ILC_COLOR32, 1, 1);
// 添加图标
this->imageList.Add(icon);
// 获取树控件
this->treeCtrl = &GetTreeCtrl();
// 树控件设置图片列表
this->treeCtrl->SetImageList(&this->imageList, TVSIL_NORMAL);
// 树控件设置节点
this->treeCtrl->InsertItem(L"个人信息", 0, 0, TVI_ROOT, TVI_LAST);
this->treeCtrl->InsertItem(L"销售管理", 0, 0, TVI_ROOT, TVI_LAST);
this->treeCtrl->InsertItem(L"库存信息", 0, 0, TVI_ROOT, TVI_LAST);
this->treeCtrl->InsertItem(L"库存添加", 0, 0, TVI_ROOT, TVI_LAST);
this->treeCtrl->InsertItem(L"库存删除", 0, 0, TVI_ROOT, TVI_LAST);
运行登陆后
功能节点消息处理
查看 SelectView 类属性,添加消息TVN_SELCHANGED 的回调
写入
// 获取选中的项目
HTREEITEM item = this->treeCtrl->GetSelectedItem();
// 获取选中项文本内容
CString selectedText = this->treeCtrl->GetItemText(item);
if (selectedText == L"个人信息")
{
}
else if (selectedText == L"销售管理")
{
}
else if (selectedText == L"库存信息")
{
}
else if (selectedText == L"库存添加")
{
}
else if (selectedText == L"库存删除")
{
}
个人信息页面
界面设计
资源视图下,Dialog上右键插入
ID改为DIALOG_USER,边框改为None,样式改为Child
绘制页面,原来的确定和取消按钮保留
(Group Box、Static Text、Edit Control)
在对话框空白处右键添加类
类名 UserDialog,基类CFormView
下面分别为 4 个编辑框创建变量
身份
变量名positionEditControl
用户名编辑框变量usernameEditControl
新密码编辑框变量newPasswordEditControl
确定密码编辑框变量confirmPasswordEditControl
功能实现
初始化界面
类视图中 UserDialog 类上右键属性
重写OnInitialUpdate
this->positionEditControl.SetWindowTextW(L"销售员");
确定修改密码
双击确定按钮,编辑确定按钮的单机事件回调,注意要引用 StoreManager.hpp 头文件
if (this->usernameEditControl.GetWindowTextLengthW() == 0)
{
MessageBox(L"输入用户名不能为空");
return;
}
if (this->newPasswordEditControl.GetWindowTextLengthW() == 0 || this->confirmPasswordEditControl.GetWindowTextLengthW() == 0)
{
MessageBox(L"输入密码不能为空");
return;
}
CString newPassword, confirmPassword;
this->newPasswordEditControl.GetWindowTextW(newPassword);
this->confirmPasswordEditControl.GetWindowTextW(confirmPassword);
if (newPassword != confirmPassword)
{
MessageBox(L"输入密码和确定密码不同");
return;
}
CString oldPassword, username, inputUsername;
StoreManager::readLogin(username, oldPassword);
this->usernameEditControl.GetWindowTextW(inputUsername);
if (inputUsername != username)
{
MessageBox(L"用户名错误");
return;
}
if (newPassword == oldPassword)
{
MessageBox(L"新密码和原密码相同");
return;
}
if (StoreManager::writePassword(inputUsername, newPassword))
{
MessageBox(L"修改密码成功");
}
else
{
MessageBox(L"修改密码失败");
}
取消修改密码
双击取消按钮
this->usernameEditControl.SetWindowTextW(L"");
this->newPasswordEditControl.SetWindowTextW(L"");
this->confirmPasswordEditControl.SetWindowTextW(L"");
界面挂载
自定义消息发送
在类视图中双击CMainFrame类进行编辑,写入自定义消息
constexpr UINT NM_USER = WM_USER + 100;
constexpr UINT NM_SELL = WM_USER + 101;
constexpr UINT NM_INFO = WM_USER + 102;
constexpr UINT NM_ADD = WM_USER + 103;
constexpr UINT NM_DEL = WM_USER + 104;
添加自定义消息处理函数,头文件中添加声明
protected:
afx_msg LRESULT onMyChange(WPARAM wParam, LPARAM lParam);
源文件中添加定义
afx_msg LRESULT CMainFrame::onMyChange(WPARAM wParam, LPARAM lParam)
{
}
然后看到 BEGIN_MESSAGE_MAP,在它和 END_MESSAGE_MAP() 之间添加代码
// 响应自定义消息
ON_MESSAGE(NM_USER, onMyChange)
ON_MESSAGE(NM_SELL, onMyChange)
ON_MESSAGE(NM_INFO, onMyChange)
ON_MESSAGE(NM_ADD, onMyChange)
ON_MESSAGE(NM_DEL, onMyChange)
编辑 SelectView 类中的 OnTvnSelchanged 方法,注意需要引用 MainFrm.h 头文件
if (selectedText == L"个人信息")
{
// 将消息放入消息队列
::PostMessage(
AfxGetMainWnd()->GetSafeHwnd(), // 框架窗口对象指针
NM_USER, // 发送自定义消息
NM_USER, // 消息的附加参数
0 // 消息的附加参数,这里不使用
);
}
else if (selectedText == L"销售管理")
{
::PostMessage(AfxGetMainWnd()->GetSafeHwnd(), NM_SELL, NM_SELL, 0);
}
else if (selectedText == L"库存信息")
{
::PostMessage(AfxGetMainWnd()->GetSafeHwnd(), NM_INFO, NM_INFO, 0);
}
else if (selectedText == L"库存添加")
{
::PostMessage(AfxGetMainWnd()->GetSafeHwnd(), NM_ADD, NM_ADD, 0);
}
else if (selectedText == L"库存删除")
{
::PostMessage(AfxGetMainWnd()->GetSafeHwnd(), NM_DEL, NM_DEL, 0);
}
自定义消息处理
编辑 CMainFrame 类中的 OnMyChange 方法,注意引用 UserDialog.hpp 头文件
CCreateContext context;
this->splitter.DeleteView(0, 1);
switch (wParam)
{
case NM_USER:
{
context.m_pNewViewClass = RUNTIME_CLASS(UserDialog);
this->splitter.CreateView(0, 1, RUNTIME_CLASS(UserDialog), CSize(600, 500), &context);
((UserDialog*)this->splitter.GetPane(0, 1))->OnInitialUpdate();
break;
}
case NM_SELL:
{
break;
}
case NM_INFO:
{
break;
}
case NM_ADD:
{
break;
}
case NM_DEL:
{
break;
}
}
this->splitter.RecalcLayout();
this->splitter.SetActivePane(0, 1);
context.m_pLastView = (CFormView*)this->splitter.GetPane(0, 1);
context.m_pCurrentFrame = this;
return 0;
调试运行效果
销售管理页面
界面设计
资源视图下插入新的对话框
ID改为DIALOG_SELL,边框改为None,样式改为Child
绘制界面,不删除原先的确定和取消按钮,确定的描述文字改成购买,商品名那里的 Combo Box 属性里的类型改为下拉列表,订单信息那里的大编辑框属性里多行、垂直滚动依次设置为True,数量编辑框属性里的数字改为True
(Group Box、Static Text、Edit Control、Combo Box)
在对话框空白处右键添加类
类名为SellDialog,基类为CFormView
为商品名组合框创建变量productNameComboBoxControl
为单价编辑框创建变量priceEditValue,注意类别选值,变量类型填 double
为数量编辑框创建变量numEditValue,类别选值,变量类型填 int
为订单信息编辑框创建变量sellEditControl
界面挂载
编辑CMainFrame类中的OnMyChange方法,在 case NM_SELL 下写。注意引用 SellDialog.hpp 头文件
context.m_pNewViewClass = RUNTIME_CLASS(SellDialog);
this->splitter.CreateView(0, 1, RUNTIME_CLASS(SellDialog), CSize(600, 500), &context);
((SellDialog*)this->splitter.GetPane(0, 1))->OnInitialUpdate();
调试运行
功能实现
初始化界面
CellDialog类上右键属性,重写OnInitialUpdate方法
这里从数据库读取商品数据,把商品名设置给组合框,注意引用 StoreManager.hpp 头文件
productsVector products;
StoreManager::readProducts(products);
for (productStruct product : products)
{
this->productNameComboBoxControl.AddString(product.name);
}
this->productNameComboBoxControl.SetCurSel(0);
调试运行
组合框切换刷新信息
在商品名组合框上右键属性
创建 CBN_SELCHANGE 事件的回调方法
写入
// 获取当前选中项的索引
int curIdx = this->productNameComboBoxControl.GetCurSel();
// 获取当前选中项的文本
CString curText;
this->productNameComboBoxControl.GetLBText(curIdx, curText);
productsVector products;
StoreManager::readProducts(products);
for (productStruct product : products)
{
if (curText == product.name)
{
this->priceEditValue = product.price;
this->numEditValue = 0;
UpdateData(FALSE);
break;
}
}
另外在前面重写的 OnInitialUpdate 方法末尾调用一下,实现初始化
this->OnCbnSelchangeCombo1();
调试运行
购买实现
双击购买按钮,创建点击事件回调,写入
if (this->numEditValue == 0)
{
MessageBox(L"购买数量不能为 0");
return;
}
int curIdx = this->productNameComboBoxControl.GetCurSel();
CString curText;
this->productNameComboBoxControl.GetLBText(curIdx, curText);
productsVector products;
StoreManager::readProducts(products);
for (productsVector::iterator product = products.begin(); product != products.end(); ++product)
{
if (curText == product->name)
{
if (this->numEditValue > product->stock)
{
CString msg;
msg.Format(L"购买数量超出库存,当前库存数量:%d,请减小购买数量后再试", product->stock);
MessageBox(msg.GetString());
return;
}
product->stock = product->stock - this->numEditValue;
productsVector modifyProduct = { *product };
StoreManager::modifyProducts(modifyProduct);
CString sellMsg;
sellMsg.Format(L"商品:%s\r\n单价:%f\r\n数量:%d\r\n总价:%f\r\n剩余库存:%d", product->name.GetString(), product->price, this->numEditValue, product->price * this->numEditValue, product->stock);
this->sellEditControl.SetWindowTextW(sellMsg.GetString());
break;
}
}
调试运行效果
取消
双击取消按钮,创建事件回调
this->sellEditControl.SetWindowTextW(L"");
this->numEditValue = 0;
UpdateData(FALSE);
库存信息页面
界面设计
添加一个对话框
属性里,ID 设置为DIALOG_INFO,边框选None,样式选Child
绘制界面,删除确定和取消按钮,List Control 的视图改为Report
(Static Text、List Control)
对话框空白处右键添加类
类名InfoDialog,基类CFormView
在列表控件上右键添加变量infoListControl
界面挂载
编辑CMainFrame类中的onMyChange方法,在case NM_INFO下写入,注意引用头文件 InfoDialog.hpp
context.m_pNewViewClass = RUNTIME_CLASS(InfoDialog);
this->splitter.CreateView(0, 1, RUNTIME_CLASS(InfoDialog), CSize(600, 500), &context);
((InfoDialog*)this->splitter.GetPane(0, 1))->OnInitialUpdate();
调试运行
功能实现
在 InfoDialog 类上右键 属性
重写 OnInitialUpdate 方法
注意引用 StoreManager.hpp 头文件
// 显示表头
this->infoListControl.SetExtendedStyle(this->infoListControl.GetExtendedStyle() | LVS_EX_FULLROWSELECT | LVS_EX_GRIDLINES);
CString field[] = { L"商品ID", L"商品名称", L"商品价格", L"库存数量" };
for (int i = 0; i < sizeof(field) / sizeof(field[0]); ++i)
{
this->infoListControl.InsertColumn(i, field[i], LVCFMT_CENTER, 90);
}
// 读取商品信息
productsVector products;
StoreManager::readProducts(products);
// 显示数据
int idx = 0;
CString tmpStr;
for (productsVector::iterator product = products.begin(); product != products.end(); ++product)
{
tmpStr.Format(L"%d", product->id);
this->infoListControl.InsertItem(idx, tmpStr);
this->infoListControl.SetItemText(idx, 1, product->name);
tmpStr.Format(L"%f", product->price);
this->infoListControl.SetItemText(idx, 2, tmpStr);
tmpStr.Format(L"%d", product->stock);
this->infoListControl.SetItemText(idx, 3, tmpStr);
++idx;
}
调试运行
库存添加页面
界面设计
添加一个对话框
ID 改为 DIALOG_ADD,边框改为None,样式改为Child
绘制界面,删除原来的确定和取消按钮,个数编辑框属性的数字设置为True,Combo Box 属性类型选下拉列表,库存单价编辑库属性只读设置为True
(Static Text、Group Box、Edit Control、Button、Combo Box)
在对话框空白处右键添加类AddDialog,基类CFormView
为组合框和编辑框添加变量
库存商品编辑框,变量名stockProductComboBoxControl,访问private
库存价格编辑框,变量名stockPriceEditValue,类别值,访问private,变量类型double
库存个数编辑框,变量名stockNumEditValue,类别值,访问private,变量类型int
新商品编辑框,变量名newProductEditValue,类别值,访问private
新商品单价编辑框,变量名newPriceEditValue,类别值,访问private,变量类型double
新商品库存编辑框,变量名newNumEditValue,类别值,访问private,变量类型int
界面挂载
编辑 CMainFrame 类中 onMyChange 方法,在 case NM_ADD 中添加,注意引用头文件 AddDialog.hpp
context.m_pNewViewClass = RUNTIME_CLASS(AddDialog);
this->splitter.CreateView(0, 1, RUNTIME_CLASS(AddDialog), CSize(600, 500), &context);
((AddDialog*)this->splitter.GetPane(0, 1))->OnInitialUpdate();
调试运行
功能实现
初始化库存组合框
重写 OnInitialUpdate 方法
注意引用 StoreManager.hpp 头文件
productsVector products;
StoreManager::readProducts(products);
for (productsVector::iterator product = products.begin(); product != products.end(); ++product)
{
this->stockProductComboBoxControl.AddString(product->name);
}
this->stockProductComboBoxControl.SetCurSel(0);
调试运行
库存组合框切换事件回调
int curIdx = this->stockProductComboBoxControl.GetCurSel();
CString curText;
this->stockProductComboBoxControl.GetLBText(curIdx, curText);
productsVector products;
StoreManager::readProducts(products);
for (productsVector::iterator product = products.begin(); product != products.end(); ++product)
{
if (curText == product->name)
{
this->stockPriceEditValue = product->price;
this->stockNumEditValue = 0;
UpdateData(FALSE);
break;
}
}
在 OnInitialUpdate 里调用这个方法,保证初始化的时候正确设置
this->OnCbnSelchangeCombo2();
调试运行
添加库存
双击添加库存按钮
UpdateData(TRUE);
if (this->stockNumEditValue <= 0 || this->stockPriceEditValue <= 0)
{
MessageBox(L"数量必须大于 0,或价格不能低于 0");
return;
}
int curIdx = this->stockProductComboBoxControl.GetCurSel();
CString curText;
this->stockProductComboBoxControl.GetLBText(curIdx, curText);
productsVector products;
StoreManager::readProducts(products);
for (productsVector::iterator product = products.begin(); product != products.end(); ++product)
{
if (curText == product->name)
{
product->stock += this->stockNumEditValue;
CString msg;
productsVector tmpProduct = { *product };
StoreManager::modifyProducts(tmpProduct);
msg.Format(L"增加库存:%d,库存总量:%d", this->stockNumEditValue, product->stock);
MessageBox(msg.GetString());
break;
}
}
this->stockNumEditValue = 0;
UpdateData(FALSE);
调试运行
取消库存设置
双击取消库存设置按钮
this->stockNumEditValue = 0;
UpdateData(FALSE);
添加新商品
双击添加新商品按钮
UpdateData(TRUE);
if (this->newNumEditValue <= 0 || this->newPriceEditValue <= 0 || this->newProductEditValue.IsEmpty())
{
MessageBox(L"输入信息有误");
return;
}
productStruct product;
product.name = this->newProductEditValue;
product.price = this->newPriceEditValue;
product.stock = this->newNumEditValue;
productsVector tmpProduct = { product };
StoreManager::writeProducts(tmpProduct);
this->newProductEditValue.Empty();
this->newPriceEditValue = 0;
this->newNumEditValue = 0;
UpdateData(FALSE);
this->OnInitialUpdate();
CString msg;
msg.Format(L"添加商品:%s,单价:%f,数量:%d", product.name.GetString(), product.price, product.stock);
MessageBox(msg.GetString());
调试运行
取消商品设置
双击取消商品设置按钮
this->newProductEditValue.Empty();
this->newPriceEditValue = 0;
this->newNumEditValue = 0;
UpdateData(FALSE);
库存删除页面
界面设计
添加一个对话款
ID 改为DIALOG_DEL,边框改为None,样式改为Child
绘制界面,保留确定和取消按钮,Combo Box 类型改为下拉列表,数量编辑框属性数字设置为True,设置单价编辑框只读
(Group Box、Static Text、Combo Box、Edit Control)
为对话框添加类DelDialog,基类CFormView
为组合框和编辑框添加变量
商品名 productComboBoxControl
单价 priceEditValue
数量 numEditValue
界面挂载
编辑 CMainFrame 类中 onMyChange 方法,在 case NM_DEL 下写入,注意引用头文件 DelDialog.hpp
context.m_pNewViewClass = RUNTIME_CLASS(DelDialog);
this->splitter.CreateView(0, 1, RUNTIME_CLASS(DelDialog), CSize(600, 500), &context);
((DelDialog*)this->splitter.GetPane(0, 1))->OnInitialUpdate();
调试运行
功能实现
初始化界面
重写 OnInitialUpdate 方法
注意引用 StoreManager.hpp 头文件
productsVector products;
StoreManager::readProducts(products);
for (productsVector::iterator product = products.begin(); product != products.end(); ++product)
{
this->productComboBoxControl.AddString(product->name);
}
this->productComboBoxControl.SetCurSel(0);
调试运行
组合框切换
int curIdx = this->productComboBoxControl.GetCurSel();
CString curText;
this->productComboBoxControl.GetLBText(curIdx, curText);
productsVector products;
StoreManager::readProducts(products);
for (productsVector::iterator product = products.begin(); product != products.end(); ++product)
{
if (curText == product->name)
{
this->priceEditValue = product->price;
this->numEditValue = 0;
UpdateData(FALSE);
break;
}
}
this->OnCbnSelchangeCombo1();
调试运行
确定按钮
双击确定按钮
UpdateData(TRUE);
if (this->numEditValue <= 0)
{
MessageBox(L"数量必须大于 0");
return;
}
int curIdx = this->productComboBoxControl.GetCurSel();
CString curText;
this->productComboBoxControl.GetLBText(curIdx, curText);
productsVector products;
StoreManager::readProducts(products);
for (productsVector::iterator product = products.begin(); product != products.end(); ++product)
{
if (curText == product->name)
{
product->stock -= this->numEditValue;
productsVector tmpProduct = { *product };
StoreManager::modifyProducts(tmpProduct);
CString msg;
msg.Format(L"删除商品:%s, 单价:%f,数量:%d", product->name.GetString(), product->price, this->numEditValue);
MessageBox(msg.GetString());
break;
}
}
this->numEditValue = 0;
UpdateData(FALSE);
调试运行
取消按钮
双击取消按钮
this->numEditValue = 0;
UpdateData(FALSE);
菜单栏
资源视图,Menu
删除帮助以外的所有菜单栏
手动添加菜单
添加事件处理程序
个人信息
::PostMessage(AfxGetMainWnd()->GetSafeHwnd(), NM_USER, NM_USER, 0);
销售管理
::PostMessage(AfxGetMainWnd()->GetSafeHwnd(), NM_SELL, NM_SELL, 0);
库存信息
::PostMessage(AfxGetMainWnd()->GetSafeHwnd(), NM_INFO, NM_INFO, 0);
库存添加
::PostMessage(AfxGetMainWnd()->GetSafeHwnd(), NM_ADD, NM_ADD, 0);
库存删除
::PostMessage(AfxGetMainWnd()->GetSafeHwnd(), NM_DEL, NM_DEL, 0);
这样就可以通过菜单进行切换了