MFC-项目简易销售系统实践
MFC 项目:简易销售系统实践
预览
源链接:https://blog.iyatt.com/?p=19158
测试环境
项目
参考黑马程序员的案例教程进行实践的记录,部分内容自行修改过,比如原案例直接读写文件保存账号、密码、数据,这里改为使用 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);
这样就可以通过菜单进行切换了