当前位置: 首页 > article >正文

MFC 项目:简易销售系统实践

预览

源链接:https://blog.iyatt.com/?p=19158

file
file
file
file
file
file
file

测试环境

  • https://blog.iyatt.com/?p=18843#%E7%8E%AF%E5%A2%83

项目

参考黑马程序员的案例教程进行实践的记录,部分内容自行修改过,比如原案例直接读写文件保存账号、密码、数据,这里改为使用 SQLite3 数据库。
注意 VS 默认头文件扩展名用的 .h,我是喜欢在 C++ 中使用扩展名 .hpp,只要不是模板创建的代码部分,是我自己添加的都使用 .hpp 扩展名。
源码:下载地址见文首源链接

新建项目

新建一个 MFC 应用,项目名 saleSystem应用程序类型单个文档项目样式MFC standard
file

用户界面功能经典菜单选项,再点完成进行创建
file

不使用安全函数

在项目名称上右键属性
file

展开配置属性->C/C++->代码生成,在安全检查中禁用安全检查
file

添加窗口图标

user.ico 放到项目目录下的res目录里
file

切换到资源视图选项卡,展开上面的资源分支,在 Icon 上右键添加资源
file

导入
file

选择图标文件
file

ID改为IDI_ICON_WIN
file

类视图下,CMainFrame类中的OnCreate方法里添加代码
file

	// 加载图标
	HICON winIcon = AfxGetApp()->LoadIconW(IDI_ICON_WIN);
	// 设置小图标
	SetIcon(winIcon, FALSE);

{F5}调试运行,点加载图标文件
file

运行效果
file

设置窗口大小和居中显示

类视图下,CMainFrame类中的OnCreate方法里添加代码

	// 设置位置(0,0)和窗口大小(800×600)
	MoveWindow(0, 0, 800, 600);
	// 居中显示
	CenterWindow();

file

设置窗口标题

资源视图下,展开资源树,双击打开 String Table,在底部添加一个 ID 为ID_STRING_PROJECTNAME,值为销售系统
file

类视图CsaleSystemDoc类中OnNewDocument方法下添加代码

	CString projectName;
	projectName.LoadStringW(ID_STRING_PROJECTNAME); // 导入字符串资源
	SetTitle(projectName); // 设置窗口标题

file

运行效果
file

设计 SQLite3 数据库读写实现

这里实现账号、密码、商品信息的读写,数据库采用 SQLite3,配置 SQLite3 环境参考:https://blog.iyatt.com/?p=19187

手动创建一个数据库文件

在项目目录下打开终端,执行命令打开数据库文件 saleSystem.sb(不存在会自动创建),打开后会处于命令交互模式

sqlite3 saleSystem.sqlite3

file

新建一张表用于存储账号、密码,并写入初始账号、密码(账号: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');

file

在创建一张表用于存储商品数据,并插入几条商品数据

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);

file

退出交互模式

.exit

file

读写实现

类视图下,项目上右键添加->
file

创建一个类,类名StoreManager
file

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;

file

可以把 C++ 中的 struct 看作是默认 public 的 class,这里析构函数需要 public,使用 struct 就不需要额外写一个public:,然后实例化一个对象,这样程序结束的时候就会自动调用这个析构函数,完成资源的释放。

登录对话框

界面设计

资源视图下,Dialog上右键插入
file

将对话框 ID 改为 DIALOG_LOGIN
file

添加控件,并设置好描述文字,设置窗口标题
(使用 Static Text、Edit Control 和 Button)
file

在对话框空白处右键添加类
file

创建一个类 LoginDialog
file

为用户名编辑框创建变量usernameEditControl,访问选private
file

file

同样为密码编辑框创建变量passwordEditControl
file

功能实现

让登录对话框在文档之前创建

类视图下,CsaleSystemApp 类中的 InitInstance 方法中
file

找到**CWinApp::InitInstance();**的位置,在它之前以模态的方式运行登录对话框。注意要引用对话框的头文件 LoginDialog.hpp
然后判断返回值,后续实现中如果登录成功才会调用 OnOK,这边得到的返回值就是 IDOK,如果不是那就说明点击了取消登录或右上角的关闭按钮,以及其它操作,这时候就要直接返回,就不会创建后续文档页面。

	LoginDialog loginDialog;
	if (loginDialog.DoModal() != IDOK)
	{
		return TRUE;
	}

file

这时候调试运行就会先显示登录对话框,但是关闭登录对话框后会显示文档页面,后续还要处理这个逻辑,只有登录验证才应该显示后续的文档。
file

数据库连接

资源视图下,StringTable 添加一个字段 DATABASE_PATH,设置 SQLite3 数据库文件的路径,这里就在项目目录下,使用相对路径
file

类视图下,在登录对话框类LoginDialog上右键属性
file

上面点击图标切换到重写页面,添加(重写) OnInitDialog
file

然后回到 OnInitDialog 函数中,添加连接数据库的代码。注意前面要引用数据库读写实现的头文件 StoreManager.hpp

	CString databasePath;
	databasePath.LoadStringW(DATABASE_PATH);
	StoreManager::connect(databasePath);

file

登录按钮回调实现

双击登录按钮会直接创建点击事件的回调
file

写入代码

	// 读取用户输入的用户名和密码
	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"");
	}

file

调试运行
file

file

取消按钮回调实现

双击取消按钮,创建点击事件回调
file

点击取消调用取消方法

	CDialogEx::OnCancel();

file

修改回车键为登录

对话框默认状态下按回车会调用 CDialogEx::OnOK(),这就会让登录验证成为摆设,相当于点击了登录对话框的 OK,那么接下来就会直接进入文档页面。
另外按照一般使用习惯回车键就是确认输入,这里也就是确认登录。
类视图下,LoginDialog类上右键属性,重写对话框的 OnOK
file

注释掉CDialogEx::OnOK();,然后调用登录按钮点击事件的回调方法,这就按回车就等于是点击登录按钮。

	this->OnBnClickedButton1();

file

静态拆分窗口

自定义视图类

类视图中项目名上右键类向导
file

下拉MFC 类
file

类名 SelectView,基类CTreeView
file

同样再添加一个类DisplayView,基类CFormView
file

创建完上面两个类,在类向导页面点确定,编译在下面可能看到一堆 SelectView 头文件和源文件的报错
file

在头文件增加引用 #include “afxcview.h”
file

再编译就好了
file

拆分窗口

类视图中,点开 CMainFrame 类,声明一个对象

private:
	CSplitterWnd splitter;

file

CMainFrame上右键属性
file

重写 OnCreateClient
file

把原来的返回注释了,重新写
注意需要引用 SelectView.hppDisplayView.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;

file

调试运行,登录后可以看到下图的布局
file

左侧树视图

添加功能节点

资源视图下,Icon右键添加资源
file

导入
file

选择 re.ico 文件
file

ID 重设为 IDI_ICON_RE
file

类视图下双击SelectView类,在类头文件中增加

private:
	CTreeCtrl* treeCtrl;
	CImageList imageList;

file

SelectView 类上右键属性
file

重写 OnInitialUpdate
file

写入

	// 加载图标
	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);

file

运行登陆后
file

功能节点消息处理

查看 SelectView 类属性,添加消息TVN_SELCHANGED 的回调
file

写入

	// 获取选中的项目
	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"库存删除")
	{

	}

file

个人信息页面

界面设计

资源视图下,Dialog上右键插入
file

ID改为DIALOG_USER边框改为None样式改为Child
file

file

绘制页面,原来的确定和取消按钮保留
(Group Box、Static Text、Edit Control)
file

在对话框空白处右键添加类
file

类名 UserDialog,基类CFormView
file

下面分别为 4 个编辑框创建变量
身份
file

变量名positionEditControl
file

用户名编辑框变量usernameEditControl
file

新密码编辑框变量newPasswordEditControl
file

确定密码编辑框变量confirmPasswordEditControl
file

功能实现

初始化界面

类视图UserDialog 类上右键属性
file

重写OnInitialUpdate
file

	this->positionEditControl.SetWindowTextW(L"销售员");

file

确定修改密码

双击确定按钮,编辑确定按钮的单机事件回调,注意要引用 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"修改密码失败");
	}

file

取消修改密码

双击取消按钮

	this->usernameEditControl.SetWindowTextW(L"");
	this->newPasswordEditControl.SetWindowTextW(L"");
	this->confirmPasswordEditControl.SetWindowTextW(L"");

file

界面挂载

自定义消息发送

类视图中双击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;

file

添加自定义消息处理函数,头文件中添加声明

protected:
	afx_msg LRESULT onMyChange(WPARAM wParam, LPARAM lParam);

file

源文件中添加定义

afx_msg LRESULT CMainFrame::onMyChange(WPARAM wParam, LPARAM lParam)
{

}

file

然后看到 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)

file

编辑 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);
	}

file

自定义消息处理

编辑 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;

file

调试运行效果
file
file
file
file
file
file

销售管理页面

界面设计

资源视图下插入新的对话框
file

ID改为DIALOG_SELL边框改为None样式改为Child
file

绘制界面,不删除原先的确定和取消按钮,确定的描述文字改成购买,商品名那里的 Combo Box 属性里的类型改为下拉列表,订单信息那里的大编辑框属性里多行垂直滚动依次设置为True,数量编辑框属性里的数字改为True
(Group Box、Static Text、Edit Control、Combo Box)
file

在对话框空白处右键添加类
file

类名为SellDialog,基类为CFormView
file

为商品名组合框创建变量productNameComboBoxControl
file
file

为单价编辑框创建变量priceEditValue,注意类别选值,变量类型填 double
file

为数量编辑框创建变量numEditValue,类别选值,变量类型填 int
file

为订单信息编辑框创建变量sellEditControl
file

界面挂载

编辑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();

file

调试运行
file

功能实现

初始化界面

CellDialog类上右键属性,重写OnInitialUpdate方法
file

这里从数据库读取商品数据,把商品名设置给组合框,注意引用 StoreManager.hpp 头文件

	productsVector products;
	StoreManager::readProducts(products);

	for (productStruct product : products)
	{
		this->productNameComboBoxControl.AddString(product.name);
	}
	this->productNameComboBoxControl.SetCurSel(0);

file

调试运行
file

组合框切换刷新信息

在商品名组合框上右键属性
file

创建 CBN_SELCHANGE 事件的回调方法
file

写入

	// 获取当前选中项的索引
	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;
		}
	}

file

另外在前面重写的 OnInitialUpdate 方法末尾调用一下,实现初始化

	this->OnCbnSelchangeCombo1();

file

调试运行
file
file

购买实现

双击购买按钮,创建点击事件回调,写入

	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;
		}
	}

file

调试运行效果
file
file
file
file

取消

双击取消按钮,创建事件回调

	this->sellEditControl.SetWindowTextW(L"");
	this->numEditValue = 0;
	UpdateData(FALSE);

file

库存信息页面

界面设计

添加一个对话框
file

属性里,ID 设置为DIALOG_INFO,边框选None,样式选Child
file
file

绘制界面,删除确定和取消按钮,List Control 的视图改为Report
(Static Text、List Control)
file

对话框空白处右键添加类
file

类名InfoDialog,基类CFormView
file

在列表控件上右键添加变量infoListControl
file

界面挂载

编辑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();

file

调试运行
file

功能实现

InfoDialog 类上右键 属性
file

重写 OnInitialUpdate 方法
file

注意引用 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;
	}

file

调试运行
file

库存添加页面

界面设计

添加一个对话框
file

ID 改为 DIALOG_ADD,边框改为None,样式改为Child
file
file

绘制界面,删除原来的确定和取消按钮,个数编辑框属性的数字设置为True,Combo Box 属性类型选下拉列表,库存单价编辑库属性只读设置为True
(Static Text、Group Box、Edit Control、Button、Combo Box)
file

在对话框空白处右键添加类AddDialog,基类CFormView
file

为组合框和编辑框添加变量
file

库存商品编辑框,变量名stockProductComboBoxControl,访问private
file

库存价格编辑框,变量名stockPriceEditValue,类别,访问private,变量类型double
file

库存个数编辑框,变量名stockNumEditValue,类别,访问private,变量类型int
file

新商品编辑框,变量名newProductEditValue,类别,访问private
file

新商品单价编辑框,变量名newPriceEditValue,类别,访问private,变量类型double
file

新商品库存编辑框,变量名newNumEditValue,类别,访问private,变量类型int
file

界面挂载

编辑 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();

file

调试运行
file

功能实现

初始化库存组合框

重写 OnInitialUpdate 方法
file
file

注意引用 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);

file

调试运行
file

库存组合框切换事件回调

file
file

	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;
		}
	}

file

OnInitialUpdate 里调用这个方法,保证初始化的时候正确设置

	this->OnCbnSelchangeCombo2();

file

调试运行
file

添加库存

双击添加库存按钮

	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);

file

调试运行
file

取消库存设置

双击取消库存设置按钮

	this->stockNumEditValue = 0;
	UpdateData(FALSE);

file

添加新商品

双击添加新商品按钮

	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());

file

调试运行
file

取消商品设置

双击取消商品设置按钮

	this->newProductEditValue.Empty();
	this->newPriceEditValue = 0;
	this->newNumEditValue = 0;
	UpdateData(FALSE);

file

库存删除页面

界面设计

添加一个对话款
file

ID 改为DIALOG_DEL,边框改为None,样式改为Child
file

绘制界面,保留确定和取消按钮,Combo Box 类型改为下拉列表,数量编辑框属性数字设置为True,设置单价编辑框只读
(Group Box、Static Text、Combo Box、Edit Control)
file

为对话框添加类DelDialog,基类CFormView
file
file

为组合框和编辑框添加变量
商品名 productComboBoxControl
file
file

单价 priceEditValue
file

数量 numEditValue
file

界面挂载

编辑 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();

file

调试运行
file

功能实现

初始化界面

重写 OnInitialUpdate 方法
file
file

注意引用 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);

file

调试运行
file

组合框切换

file
file

	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;
		}
	}

file

	this->OnCbnSelchangeCombo1();

file

调试运行
file

确定按钮

双击确定按钮

	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);

file

调试运行
file

取消按钮

双击取消按钮

	this->numEditValue = 0;
	UpdateData(FALSE);

file

菜单栏

资源视图,Menu
file

删除帮助以外的所有菜单栏
file

手动添加菜单
file

添加事件处理程序
个人信息
file
file

	::PostMessage(AfxGetMainWnd()->GetSafeHwnd(), NM_USER, NM_USER, 0);

file

销售管理
file

	::PostMessage(AfxGetMainWnd()->GetSafeHwnd(), NM_SELL, NM_SELL, 0);

file

库存信息
file

	::PostMessage(AfxGetMainWnd()->GetSafeHwnd(), NM_INFO, NM_INFO, 0);

file

库存添加
file

	::PostMessage(AfxGetMainWnd()->GetSafeHwnd(), NM_ADD, NM_ADD, 0);

file

库存删除
file

	::PostMessage(AfxGetMainWnd()->GetSafeHwnd(), NM_DEL, NM_DEL, 0);

file

这样就可以通过菜单进行切换了
file


http://www.kler.cn/a/580677.html

相关文章:

  • 【HeadFirst系列之HeadFirst设计模式】第18天之深入解析解释器模式:从原理到实战
  • LLaMA-Factory训练DeepSeek大模型+本地部署
  • PPT内视频播放无法播放的原因及解决办法
  • 【Godot】@export_multiline
  • 【性能测试】Jmeter下载安装、环境配置-小白使用手册(1)
  • 《基于深度学习的图像识别技术在医学影像分析中的应用研究》
  • 智能焊机监测系统:打造工业安全的数字化盾牌
  • C#工业上位机实时信号边沿检测实现
  • Liunx——动静态库
  • Android Broadcast广播封装
  • 基于深度学习的中文文本情感分析系统
  • 我与DeepSeek读《大型网站技术架构》(8)- 网站应用攻击与防御
  • 智能化时代,美容院如何借助科技实现高效运营?
  • 行为级建模
  • React框架连续解构赋值详细解读
  • Nest.js全栈开发终极实践:TypeORM+微服务+Docker构建高可用企业级应用
  • LeetCode --- 439周赛
  • HarmonyOS Next 中的状态管理
  • 推理LLMs发展综述:从GPT到DeepSeek
  • 手机号实名认证接口:数字时代的安全与便捷保障