MFC——对话框的创建及使用


MFC对话框的创建实例

对话框程序的创建过程:

  • 选择“文件 | 新建 | 项目”菜单;

  • 在“新建项目”对话框中,选择“ MFC 应用程序 ”,输入工程名称,选择“确定”。
    在这里插入图片描述

  • 选择“ 基于对话框”,即创建基于对话框的应用程序,选择“完成”。
    在这里插入图片描述

对话框应用程序框架介绍

资源视图
用 AppWizard 创建基于对话框的应用程序框架(假定工程名为 Dialog )后,项目工作区上增加了一个“资源视图”选项卡。
在这里插入图片描述
或者,通过视图找到“资源视图”选项卡:
在这里插入图片描述

在 MFC中,与用户进行交互的对话框界面被认为是一种资源。展开“Dialog”,可以看到有一个ID为IDD_ DIALOG _DIALOG(中间部分(DIALOG)与项目名称相同)的资源,对应中间的对话框设计界面。不管在何时,只要双击对话框资源的ID,对话框设计界面就会显示在中间。

类视图
在类视图中,可以看到生成了3 个类:CAboutDlg、CDialogApp和CDialogDlg。
在这里插入图片描述

  • CAboutDlg:对应生成的版本信息对话框。
  • CDialogApp:应用程序类,从 CWinApp 继承过来,封装了初始化、运行、终止该程序的代码。
  • CDialogDlg:对话框类,从CdialogEx继承过来的,在程序运行时看到的对话框就是它的一个具体对象。
  • DoDataExchange函数:该函数主要完成对话框数据的交换和校验。
  • OnInitDialog函数:相当于对对话框进行初始化处理。

设计界面和工具箱

在这里插入图片描述
模态对话框
当模态对话框显示时,程序会暂停执行,直到关闭这个模态对话框之后,才能执行程序中的其他任务。

  • 1)通过工具箱在界面上放一个Button,双击此按钮即可跳转到按钮处理函数:

在这里插入图片描述

//按钮处理函数
void CDialogDlg::OnBnClickedButton1()
{
	// TODO:  在此添加控件通知处理程序代码
}
  • 2)资源视图 -> Dialog -> 右击 -> 插入 Dialog:

在这里插入图片描述在这里插入图片描述

  • 3)修改对话框ID:
    在这里插入图片描述

  • 4)点击对话框模板 -> 右击 -> 添加类:
    在这里插入图片描述在这里插入图片描述

  • 5)类视图中多了一个自定义类:
    在这里插入图片描述

6)按钮处理函数创建对话框,以模态方式运行。
实现模态对话框的创建需要调用CDialog类的成员函数CDialog::DoModel,该函数的功能就是创建并显示一个对话框:

//启动模态对话框按钮
void CDialogDlg::OnBnClickedButton1()
{
	//需要包含头文件:#include "DlgExec.h"
	CDlgExec dlg;
	dlg.DoModal(); //以模态方式运行
}

非模态对话框
当非模态对话框显示时,运行转而执行程序中的其他任务,而不用关闭这个对话框。

图形界面操作过程和模态对话框一样,只是,非模态对话框实现方式不一样,先创建(CDialog::Create)一次,然后再显示(CWnd::ShowWindow)。

  • 1)主对话框.h类中声明对话框对象:
    在这里插入图片描述

  • 2)创建对话框放在主对话框类的构造函数或OnCreate()函数,目的只创建一次对话框:

//主对话框构造函数
CDialogDlg::CDialogDlg(CWnd* pParent /*=NULL*/)
	: CDialogEx(CDialogDlg::IDD, pParent)
{
	m_hIcon = AfxGetApp()->LoadIcon(IDR_MAINFRAME);

	m_dlg.Create(IDD_DIALOG_SHOW); //IDD_DIALOG_SHOW为对话框ID
}
  • 3)按钮处理函数显示对话框:
//启动非模态对话框按钮
void CDialogDlg::OnBnClickedButton2()
{
	// TODO:  在此添加控件通知处理程序代码

	m_dlg.ShowWindow(SW_SHOWNORMAL); //显示非模态对话框
}

MFC常用控件

静态文本框CStatic

在这里插入图片描述静态文本框是最简单的控件,它主要用来显示文本信息,不能接受用户输入,一般不需要连接变量,也不需要处理消息。

静态文本框的重要属性有:

  • ID:所有静态文本框的缺省ID都是IDC_STATIC,静态ID,不响应任何消息(事件)
  • Caption:修改显示的内容
接口 功能
CWnd::SetWindowText 设置控件内容
CWnd::GetWindowText 获取控件内容
CStatic::SetBitmap 设置位图(后缀为bmp的图片)

关联控件变量
由于XXX_STATIC静态ID是不能关联变量,故需把ID修改后,再关联变量:
在这里插入图片描述在这里插入图片描述在这里插入图片描述在主对话框类OnInitDialog()中,完成相应接口测试:

在这里插入图片描述

//设置静态控件内容为Tom
	m_label.SetWindowText(TEXT("Tom"));

	//获取静态控件的内容
	CString str;
	m_label.GetWindowText(str);
	MessageBox(str);

	//设置静态控件窗口风格为位图居中显示
	m_label.ModifyStyle(0xf, SS_BITMAP | SS_CENTERIMAGE);

	//通过路径获取bitmap句柄
#define HBMP(filepath,width,height) (HBITMAP)LoadImage(AfxGetInstanceHandle(),filepath,IMAGE_BITMAP,width,height,LR_LOADFROMFILE|LR_CREATEDIBSECTION)

	//静态控件设置bitmap
	m_label.SetBitmap(HBMP(TEXT("./1.bmp"), 300, 250));

普通按钮 CButton

在这里插入图片描述按钮是最常见的、应用最广泛的一种控件。在程序执行期间,当单击某个按钮后就会执行相应的消息处理函数。

按钮的主要属性是Caption,来设置在按钮上显示的文本。

命令按钮处理的最多的消息是:BN_CLICKED,双击按钮即可跳转到处理函数。或者,通过按钮属性 -> 控制事件 -> 选择所需事件,添加处理函数:
在这里插入图片描述

//按钮BN_CLICKED事件处理函数
void CMFCApplication2Dlg::OnBnClickedButton1()
{
	// TODO:  在此添加控件通知处理程序代码
}

常用接口:

接口 功能
CWnd::SetWindowText 设置控件内容
CWnd::GetWindowText 获取控件内容
CWnd::EnableWindow 设置控件是否变灰

关联控件变量
在这里插入图片描述在主对话框类OnInitDialog()中,完成相应接口测试:
在这里插入图片描述

//获取按钮的内容
	CString str;
	m_button.GetWindowText(str);
	MessageBox(str);

	//设置按钮内容
	m_button.SetWindowText(TEXT("^_^"));

	//设置按钮状态为灰色
	m_button.EnableWindow(FALSE);
	m_button.EnableWindow(TRUE);

编辑框 CEdit

在这里插入图片描述
常用属性设置:

属性 含义
Number True只能输入数字
Password True密码模式
Want return True接收回车键,自动换行,只有在多行模式下,才能换行
Multiline True多行模式
Auto VScroll True 当垂直方向字符太多,自动出现滚动条,同时设置Vertical Scroll才有效
Vertical Scroll True当垂直方向字符太多,自动出现滚动条,和Auto VScroll配合使用
Horizontal Scroll True当垂直方向字符太多,自动出现滚动条,和Auto HScroll配合使用
Read Only True 只读

常用接口:

接口 功能
CWnd::SetWindowText 设置控件内容
CWnd::GetWindowText 获取控件内容

关联控件变量
在这里插入图片描述在主对话框类OnInitDialog()中,完成相应接口测试:

//设置按钮内容
	m_edit.SetWindowText(TEXT("this is a test"));

	//获取按钮的内容
	CString str;
	m_edit.GetWindowText(str);
	MessageBox(str);

管理基本类型变量
在这里插入图片描述若一个编辑框连接了一个Value类别的变量,则该变量就表示这个编辑框,编辑框中显示的内容就是变量的值。

但是,改变了编辑框的内容并不会自动更新对应的变量的值,同样,改变了变量的值也不会自动刷新编辑框的内容。若要保持一致,需要使用UpdateData()函数更新:

  • 若编辑框的内容改变了,则应使用语句UpdateData(TRUE) 获取对话框数据
  • 若变量的值改变了,则应使用语句UpdateData(FALSE) 初始化对话框控件
    在这里插入图片描述在主对话框类OnInitDialog()中,完成相应代码测试:
	m_e1 = 10.11;
	UpdateData(FALSE); //FALSE说明把m_e1的值更新到对应的控件中

	UpdateData(TRUE); //TRUE说明把控件的值更新到m_e1变量中

组合框(下拉框)CComboBox

在这里插入图片描述常用属性设置:

属性 含义
data 设置内容,不同内容间用英文的分号“;”分隔
type 显示风格
Sort True 内容自动排序

常用接口:

接口 功能
CComboBox::AddString 组合框添加一个字符串
CComboBox::SetCurSel 设置当前选择项(当前显示第几项),下标从0开始
CComboBox::GetCurSel 获取组合框中当前选中项的下标
CComboBox::GetLBText 获取指定位置的内容
CComboBox::DeleteString 删除指定位置的字符串
CComboBox::InsertString 在指定位置插入字符串

关联控件变量后,测试接口:
在这里插入图片描述

//添加字符串内容
	m_combo.AddString(TEXT("可乐")); 
	m_combo.AddString(TEXT("雪碧"));

	m_combo.SetCurSel(1);//显示显示第1项

	//获取组合框中当前选中项的下标
	int index = m_combo.GetCurSel(); 
	CString str;
	m_combo.GetLBText(index, str); //获取指定下标的内容
	MessageBox(str);
	m_combo.DeleteString(0); //删除第0项字符串
	
	m_combo.InsertString(0, _T("hello")); //在第0位置插入“hello”

组合框常用的事件为:CBN_SELCHANGE,当选择组合框某一项时,自动触发此事件。
在这里插入图片描述

void CMFCApplication2Dlg::OnCbnSelchangeCombo1()
{
	// TODO:  在此添加控件通知处理程序代码
}

列表控件 CListCtrl

在这里插入图片描述常用属性设置:view -> Report(报表方式)
在这里插入图片描述

常用接口:

接口 功能
CListCtrl::SetExtendedStyle 设置列表风格
CListCtrl::GetExtendedStyle 获取列表风格
CListCtrl::InsertColumn 插入某列内容,主要用于设置标题
CListCtrl::InsertItem 在某行插入新项内容
CListCtrl::SetItemText 设置某行某列的子项内容
CListCtrl::GetItemText 获取某行某列的内容

关联控件变量后,测试接口:
在这里插入图片描述

//设置风格样式
	//LVS_EX_GRIDLINES 网格
	//LVS_EX_FULLROWSELECT 选中整行
	m_list.SetExtendedStyle(m_list.GetExtendedStyle()
		| LVS_EX_GRIDLINES | LVS_EX_FULLROWSELECT);

	//插入标题
	CString head[] = { TEXT("姓名"), TEXT("年龄"), TEXT("性别") };

	//插入列
	m_list.InsertColumn(0, head[0], LVCFMT_LEFT, 100);
	m_list.InsertColumn(1, head[1], LVCFMT_LEFT, 100);
	m_list.InsertColumn(2, head[2], LVCFMT_LEFT, 100);

	//插入正文内容,先确定行,再确定列
	for (int i = 0; i < 10; i++)
	{
		CString str;
		str.Format(TEXT("张三_%d"), i );

		//确定行
		m_list.InsertItem(i, str);

		//设置列
		int j = 0;
		m_list.SetItemText(i, ++j, TEXT("男"));
		m_list.SetItemText(i, ++j, TEXT("23"));
	}

程序效果图:
在这里插入图片描述

树控件 CTreeCtrl

在这里插入图片描述
常用属性设置:

属性 含义
has buttons True 有展开按钮
has lines True 有展开线
lines at root True 有根节点

常用接口:

接口 功能
AfxGetApp() 获取应用程序对象指针
CWinApp::LoadIcon 加载自定义图标
CImageList::Create 创建图像列表
CImageList::Add 图像列表追加图标
CTreeCtrl::SetImageList 设置图形状态列表
CTreeCtrl::InsertItem 插入节点
CTreeCtrl::SelectItem 设置默认选中项
CTreeCtrl::GetSelectedItem 获取选中项
CTreeCtrl::GetItemText 获取某项内容
  • 关联控件变量
    在这里插入图片描述
  • 添加图标资源(icon)
    在这里插入图片描述
  • 资源视图 -> Icon -> 添加资源:
    在这里插入图片描述
  • 导入ico文件
    在这里插入图片描述在这里插入图片描述在这里插入图片描述
  • 3)通过代码加载图标
	//加载图标
	HICON icon[3];
	icon[0] = AfxGetApp()->LoadIconW(IDI_ICON1);
	icon[1] = AfxGetApp()->LoadIconW(IDI_ICON2);
	icon[2] = AfxGetApp()->LoadIconW(IDI_ICON3);

  • 创建图像列表
    .h文件中定义图形列表(CImageList)对象
CImageList m_imageList; //图像列表

.cpp文件中OnInitDialog()函数中完成图像列表的创建、图标的追加

//图像列表,程序完毕不能释放, 创建
	//30, 30: 图片的宽度和高度
	//ILC_COLOR32:样式
	// 3, 3: 有多少图片写多少
	m_imageList.Create(30, 30, ILC_COLOR32, 3, 3);

	//给图像列表添加图片
	for (int i = 0; i < 3; i++)
	{
		//图片列表加载图标
		m_imageList.Add(icon[i]);
	}

  • 树控件的相应操作
	//树控件设置图片列表
	m_treeCtrl.SetImageList(&m_imageList, TVSIL_NORMAL);

	//给树创建节点
	//根节点,父节点,子节点
	HTREEITEM root = m_treeCtrl.InsertItem(TEXT("中国"), 0, 0, NULL);
	HTREEITEM fathter = m_treeCtrl.InsertItem(TEXT("北京"), 1, 1, root);
	HTREEITEM son = m_treeCtrl.InsertItem(TEXT("海淀"), 2, 2, fathter);

	//设置某个节点被选中
	m_treeCtrl.SelectItem(fathter);

程序效果图
在这里插入图片描述树控件常用事件为:TVN_SELCHANGED,当选择某个节点时,自动触发此事件。
在这里插入图片描述

void CMy01_TreeCtrlDlg::OnTvnSelchangedTree1(NMHDR *pNMHDR, LRESULT *pResult)
{
	LPNMTREEVIEW pNMTreeView = reinterpret_cast<LPNMTREEVIEW>(pNMHDR);
	// TODO:  在此添加控件通知处理程序代码
	*pResult = 0;

	HTREEITEM selItem;
	//获得选择项
	selItem = m_treeCtrl.GetSelectedItem();
	//获取选中的内容
	CString cs = m_treeCtrl.GetItemText(selItem);
	MessageBox(cs);
}

标签控件 CTabCtrl

在这里插入图片描述

  • 在UI工具箱拖放 Tab Control
    在这里插入图片描述

  • 把 TabSheet.h和TabSheet.cpp 放在项目文件同级目录,并且添加到工程目录中
    在这里插入图片描述

  • 给UI上 Tab Control 关联Control类型(CTabSheet)
    在这里插入图片描述

  • 添加对话框

    • 资源视图 -> Dialog -> 右击 -> 插入 Dialog
    • 设置相应属性:
      Style -> Child (子窗口)
      Border -> None (无边框)
    • 自定义类:点击对话框模板 -> 右击 -> 添加类(MyDlg1、MyDlg2)
    • 主对话框类中, 定义自定义类对象,需要相应头文件
	//给tab控件添加对话框
//IDD_DIALOG1为dlg1资源ID
	m_tabCtrl.AddPage(TEXT("系统管理"), &dlg1, IDD_DIALOG1); 

//IDD_DIALOG1为dlg2资源ID
	m_tabCtrl.AddPage(TEXT("系统设置"), &dlg2, IDD_DIALOG2); 

	//显示tab控件
	m_tabCtrl.Show();

程序效果图
在这里插入图片描述

综合案例

  • 通过MFC向导创建综合案例(销售信息管理系统)项目。

项目总体框架

整个项目分为登录界面、主界面。而主界面分裂成两个界面显示,分别为左侧选择界面和右侧显示界面。功能上分别有个人信息管理、销售管理窗口、库存信息窗口、库存添加窗口、库存删除窗口以及菜单栏。

文件处理

项目中使用到将数据保存到文件进行保存的功能,这部分设计成一个单独的文件处理类CInfoFile。
在解决方案中添加类–>CInfoFile。

  • CInfoFole.h
#pragma once

#include <list>
#include <fstream>
#include <iostream>
#include <string>
#include "pch.h"

#define _F_LOGIN "./login.ini"
#define _F_STOCK "./stock.txt"

struct msg
{
	int id;
	std::string name;
	int price;
	int num;
};

class CInfoFile
{
public:
	CInfoFile();
	~CInfoFile();

	//读取登录信息
	void ReadLogin(CString& name, CString& pwd);

	//修改密码
	void WritePwd(char* name, char* pwd);

	//读取商品数据
	void ReadDocline();

	//商品写入文件
	void WriteDocline();

	//添加新商品
	void AddLine(CString name, int num, int price);

	std::list<msg> ls; //存储商品容器
	int num; //记录商品个数
};


  • CInfoFole.cpp
#include "pch.h"
#include "CInfoFile.h"

CInfoFile::CInfoFile()
{
}

CInfoFile::~CInfoFile()
{
}

void CInfoFile::ReadLogin(CString& name, CString& pwd)
{
	std::ifstream ifs;//创建文件输入对象
	ifs.open(_F_LOGIN);//打开文件

	char buf[1024] = { 0 };
	ifs.getline(buf, sizeof(buf));//读取第一行
	name = CString(buf);//char * 转为CString
	
	memset(buf, 0, sizeof(buf));//清空buf
	ifs.getline(buf, sizeof(buf));
	pwd = CString(buf);

	ifs.close();//关闭文件

}

void CInfoFile::WritePwd(char* name, char* pwd)
{
	std::ofstream ofs;//创建文件输出对象
	ofs.open(_F_LOGIN);//打开文件

	ofs << name << std::endl;//将name写入文件并换行
	ofs << pwd << std::endl;//将pwd写入文件并换行

	ofs.close();//关闭文件

}

void CInfoFile::ReadDocline()
{
	std::ifstream ifs(_F_STOCK);//输入方式打开文件

	char buf[1024] = { 0 };
	num = 0;//初始化商品数目
	ls.clear();//初始化容器
	//取出表头
	ifs.getline(buf, sizeof(buf));

	while (!ifs.eof())//循环执行到文件结尾
	{
		msg tmp;//声明暂存变量
		
		ifs.getline(buf, sizeof(buf));
		num++;

		char* set = strtok(buf, "|");//以|为分隔符切割字符串
		if (set != NULL)
		{
			tmp.id = std::atoi(set);//第一个为商品id
		}
		else
		{
			break;//找不到分隔符则跳出循环
		}

		set = strtok(NULL, "|");
		tmp.name = set;//商品名称

		set = strtok(NULL, "|");
		tmp.price = std::atoi(set);//商品价格

		set = strtok(NULL, "|");
		tmp.num = std::atoi(set);//商品数目

		ls.push_back(tmp);
	}

	ifs.close();//关闭文件
}

void CInfoFile::WriteDocline()
{
	std::ofstream ofs(_F_STOCK);//输出方式打开文件
	if (ls.size() > 0)
	{
		ofs << "商品ID|商品名称|商品价格|库存" << std::endl;//写入表头
		//通过迭代器写入内容
		for (auto it = ls.begin(); it != ls.end(); ++it)
		{
			ofs << it->id << "|";
			ofs << it->name << "|";
			ofs << it->price << "|";
			ofs << it->num << std::endl;
		}
	}
	ofs.close();//关闭文件
}

void CInfoFile::AddLine(CString name, int num, int price)
{
	msg tmp;
	if (ls.size() > 0)
	{
		if (!name.IsEmpty() && num > 0 && price > 0)
		{
			tmp.id = ls.size() + 1;
			CStringA str;
			str = name;
			tmp.name = str.GetBuffer();
			tmp.num = num;
			tmp.price = price;

			ls.push_back(tmp);//放在链表后
		}
	}
}

登录对话框

添加对话框资源(ID修改为DIALOG_LOGIN),添加所需控件:
在这里插入图片描述选中对话框 -> 右击 -> 添加类 -> 类名:CLoginDlg,添加关联变量。
在这里插入图片描述具体功能实现:

  • 需要重写OnInitDialog
  • 在应用程序类CSaleSystemApp的InitInstance() 里面的APP 创建之前创建登陆对话框。
	CLoginDlg dlg;	//创建登陆对话框,需要头文件#include "LoginDlg.h"
	dlg.DoModal();	//以模态方式运行

  • 登录按钮功能实现。

  • CLoginDlg.h

#pragma once


// CLoginDlg 对话框

class CLoginDlg : public CDialogEx
{
	DECLARE_DYNAMIC(CLoginDlg)

public:
	CLoginDlg(CWnd* pParent = nullptr);   // 标准构造函数
	virtual ~CLoginDlg();

// 对话框数据
#ifdef AFX_DESIGN_TIME
	enum { IDD = IDD_LOGIN };
#endif

protected:
	virtual void DoDataExchange(CDataExchange* pDX);    // DDX/DDV 支持

	DECLARE_MESSAGE_MAP()
private:
	CString m_user;
	CString m_pwd;
public:
	virtual BOOL OnInitDialog();
	afx_msg void OnBnClickedButton2();
	afx_msg void OnBnClickedButton1();
	afx_msg void OnClose();
	virtual void OnOK();
};

  • CLoginDlg.cpp
// CLoginDlg.cpp: 实现文件
//

#include "pch.h"
#include "SaleSystem.h"
#include "CLoginDlg.h"
#include "afxdialogex.h"
#include "CInfoFile.h"


// CLoginDlg 对话框

IMPLEMENT_DYNAMIC(CLoginDlg, CDialogEx)

CLoginDlg::CLoginDlg(CWnd* pParent /*=nullptr*/)
	: CDialogEx(IDD_LOGIN, pParent)
	, m_user(_T(""))
	, m_pwd(_T(""))
{

}

CLoginDlg::~CLoginDlg()
{
}

void CLoginDlg::DoDataExchange(CDataExchange* pDX)
{
	CDialogEx::DoDataExchange(pDX);
	DDX_Text(pDX, IDC_EDIT1, m_user);
	DDX_Text(pDX, IDC_EDIT2, m_pwd);
}


BEGIN_MESSAGE_MAP(CLoginDlg, CDialogEx)
	ON_BN_CLICKED(IDC_BUTTON2, &CLoginDlg::OnBnClickedButton2)
	ON_BN_CLICKED(IDC_BUTTON1, &CLoginDlg::OnBnClickedButton1)
	ON_WM_CLOSE()
END_MESSAGE_MAP()


// CLoginDlg 消息处理程序


BOOL CLoginDlg::OnInitDialog()
{
	CDialogEx::OnInitDialog();

	// TODO:  在此添加额外的初始化
	m_user = TEXT("斧头帮帮主");
	m_pwd = TEXT("123456");

	UpdateData(FALSE);//内容更新到对应控件

	return TRUE;  // return TRUE unless you set the focus to a control
				  // 异常: OCX 属性页应返回 FALSE
}


void CLoginDlg::OnBnClickedButton2()
{
	// TODO: 在此添加控件通知处理程序代码
	UpdateData(TRUE);//更新控件数据到数据区
	CInfoFile file;
	CString user, pwd;
	user = TEXT("斧头帮帮主");
	pwd = TEXT("123456");
	
	//file.ReadLogin(user, pwd);

	if (m_user == user)
	{
		if (m_pwd != pwd)
		{
			MessageBox(TEXT("密码错误"));
			m_user.Empty();
			m_pwd.Empty();
		}
		else
		{
			CDialogEx::OnOK();
		}
	}
	else
	{
		MessageBox(TEXT("用户不存在"));
		m_user.Empty();
		m_pwd.Empty();
	}

}


void CLoginDlg::OnBnClickedButton1()
{
	// TODO: 在此添加控件通知处理程序代码
	exit(0);
}


void CLoginDlg::OnClose()
{
	// TODO: 在此添加消息处理程序代码和/或调用默认值
	exit(0);

	//CDialogEx::OnClose();
}


void CLoginDlg::OnOK()
{
	// TODO: 在此添加专用代码和/或调用基类

	//CDialogEx::OnOK();
}

静态拆分窗口

实现主界面的拆分显示,左侧选择界面显示(CSelectView)和右侧显示界面(CDisplayView)。
CSelectView继承于CTreeView,CDispalyView继承于CFormView。

  • 在CMainFrame类中,声明CSplitterWnd类型的对象:
private:
	CSplitterWnd m_spliter; // 切分窗口类对象

  • 重写框架类CMainFrame的OnCreateClient函数

BOOL CMainFrame::OnCreateClient(LPCREATESTRUCT lpcs, CCreateContext* pContext)
{
	// TODO: 在此添加专用代码和/或调用基类

	//return CFrameWnd::OnCreateClient(lpcs, pContext);

	//静态拆分窗口
	m_spliter.CreateStatic(this, 1, 2);

	m_spliter.CreateView(0, 0, RUNTIME_CLASS(CSelectView), CSize(200, 500), pContext);

	m_spliter.CreateView(0, 1, RUNTIME_CLASS(CDisplayView), CSize(600, 500), pContext);

	return TRUE;
}
  • 显示如下:
    在这里插入图片描述

左侧选择界面树视图实现

  • 添加树视图图标。
  • CSelectView类中声明相应变量:
	CTreeCtrl *m_treeCtrl;	//树控件
	CImageList m_imageList;	//图标列表

  • 重写CSelectView的OnInitUpdate函数

void CSelectView::OnInitialUpdate()
{
	CTreeView::OnInitialUpdate();

	// TODO: 在此添加专用代码和/或调用基类
	HICON icon = AfxGetApp()->LoadIconW(IDI_ICON_RE);

	//图片列表的串讲,指定图标的宽度和高度
	m_imageList.Create(30, 30, ILC_COLOR32, 1, 1);
	m_imageList.Add(icon);

	//获取树视图中的控件
	m_treeCtrl = &GetTreeCtrl();

	//树控件设置图片列表
	m_treeCtrl->SetImageList(&m_imageList, TVSIL_NORMAL);

	//树控件设置节点
	m_treeCtrl->InsertItem(TEXT("个人信息"), 0, 0, NULL);
	m_treeCtrl->InsertItem(TEXT("销售管理"), 0, 0, NULL);
	m_treeCtrl->InsertItem(TEXT("库存信息"), 0, 0, NULL);
	m_treeCtrl->InsertItem(TEXT("库存添加"), 0, 0, NULL);
	m_treeCtrl->InsertItem(TEXT("库存删除"), 0, 0, NULL);


}

在这里插入图片描述

功能节点消息处理

当左侧窗口点击树节点,会发送对应的消息给mainframe,主窗口接收到对应消息后会进行相应处理,将右侧窗口挂载成不同的显示界面。

  • 树节点改变响应处理:

void CSelectView::OnTvnSelchanged(NMHDR* pNMHDR, LRESULT* pResult)
{
	LPNMTREEVIEW pNMTreeView = reinterpret_cast<LPNMTREEVIEW>(pNMHDR);
	// TODO: 在此添加控件通知处理程序代码

	//获取当前项目
	HTREEITEM item = m_treeCtrl->GetSelectedItem();

	//根据所选项目获取内容
	CString str = m_treeCtrl->GetItemText(item);

	//MessageBox(str);
	if (str == TEXT("个人信息"))
	{
		::PostMessage(AfxGetMainWnd()->GetSafeHwnd(), NM_A, (WPARAM)NM_A, (LPARAM)0);
	}
	else if (str == TEXT("销售管理"))
	{
		::PostMessage(AfxGetMainWnd()->GetSafeHwnd(), NM_B, (WPARAM)NM_B, (LPARAM)0);
	}
	else if (str == TEXT("库存信息"))
	{
		::PostMessage(AfxGetMainWnd()->GetSafeHwnd(), NM_C, (WPARAM)NM_C, (LPARAM)0);
	}
	else if (str == TEXT("库存添加"))
	{
		::PostMessage(AfxGetMainWnd()->GetSafeHwnd(), NM_D, (WPARAM)NM_D, (LPARAM)0);
	}
	else if (str == TEXT("库存删除"))
	{
		::PostMessage(AfxGetMainWnd()->GetSafeHwnd(), NM_E, (WPARAM)NM_E, (LPARAM)0);
	}

	*pResult = 0;
}

  • 在CMainFrame.h中声明了对应的消息宏:
#define NM_A	(WM_USER + 100)
#define NM_B	(WM_USER + 101)
#define NM_C	(WM_USER + 102)
#define NM_D	(WM_USER + 103)
#define NM_E	(WM_USER + 104)


  • 在CMainFrame.cpp中的宏节点声明对应的消息处理声明:

BEGIN_MESSAGE_MAP(CMainFrame, CFrameWnd)
	ON_WM_CREATE()
	ON_COMMAND_RANGE(ID_VIEW_APPLOOK_WIN_2000, ID_VIEW_APPLOOK_WINDOWS_7, &CMainFrame::OnApplicationLook)
	ON_UPDATE_COMMAND_UI_RANGE(ID_VIEW_APPLOOK_WIN_2000, ID_VIEW_APPLOOK_WINDOWS_7, &CMainFrame::OnUpdateApplicationLook)
	ON_WM_SETTINGCHANGE()

	//自定义消息
	ON_MESSAGE(NM_A, OnMyChange)
	ON_MESSAGE(NM_B, OnMyChange)
	ON_MESSAGE(NM_C, OnMyChange)
	ON_MESSAGE(NM_D, OnMyChange)
	ON_MESSAGE(NM_E, OnMyChange)
	ON_COMMAND(ID_EDIT_UNDO, &CMainFrame::OnEditUser)
	ON_COMMAND(ID_EDIT_CUT, &CMainFrame::OnEditSell)
	ON_COMMAND(ID_EDIT_COPY, &CMainFrame::OnEditCopy)
	ON_COMMAND(ID_EDIT_PASTE, &CMainFrame::OnEditAdd)
	ON_COMMAND(ID_32771, &CMainFrame::OnEditDel)
END_MESSAGE_MAP()

  • 自定义对应的消息处理函数
//自定义处理消息函数
LRESULT CMainFrame::OnMyChange(WPARAM wParam, LPARAM lParam)
{
	CCreateContext Context;
	switch (wParam)
	{
	case NM_A:
	{
		//MessageBox(TEXT("个人信息界面显示"));
		//界面挂载
		Context.m_pNewViewClass = RUNTIME_CLASS(CUserDlg);
		Context.m_pCurrentFrame = this;
		Context.m_pLastView = (CFormView*)m_spliter.GetPane(0, 1);
		m_spliter.DeleteView(0, 1);
		m_spliter.CreateView(0, 1, RUNTIME_CLASS(CUserDlg), CSize(600, 500), &Context);
		CUserDlg* pNewView = (CUserDlg*)m_spliter.GetPane(0, 1);
		m_spliter.RecalcLayout();
		pNewView->OnInitialUpdate();
		m_spliter.SetActivePane(0, 1);
	}
		break;
	case NM_B:
	{
		Context.m_pNewViewClass = RUNTIME_CLASS(CSellDlg);
		Context.m_pCurrentFrame = this;
		Context.m_pLastView = (CFormView*)m_spliter.GetPane(0, 1);
		m_spliter.DeleteView(0, 1);
		m_spliter.CreateView(0, 1, RUNTIME_CLASS(CSellDlg), CSize(600, 0), &Context);
		CSellDlg* pNewView = (CSellDlg*)m_spliter.GetPane(0, 1);
		m_spliter.RecalcLayout();
		pNewView->OnInitialUpdate();
		m_spliter.SetActivePane(0, 1);
	}
		break;
	case NM_C:
	{
		Context.m_pNewViewClass = RUNTIME_CLASS(CInfoDlg);
		Context.m_pCurrentFrame = this;
		Context.m_pLastView = (CFormView*)m_spliter.GetPane(0, 1);
		m_spliter.DeleteView(0, 1);
		m_spliter.CreateView(0, 1, RUNTIME_CLASS(CInfoDlg), CSize(600, 0), &Context);
		CInfoDlg* pNewView = (CInfoDlg*)m_spliter.GetPane(0, 1);
		m_spliter.RecalcLayout();
		pNewView->OnInitialUpdate();
		m_spliter.SetActivePane(0, 1);
	}
		break;
	case NM_D:
	{
		Context.m_pNewViewClass = RUNTIME_CLASS(CAddDlg);
		Context.m_pCurrentFrame = this;
		Context.m_pLastView = (CFormView*)m_spliter.GetPane(0, 1);
		m_spliter.DeleteView(0, 1);
		m_spliter.CreateView(0, 1, RUNTIME_CLASS(CAddDlg), CSize(600, 0), &Context);
		CAddDlg* pNewView = (CAddDlg*)m_spliter.GetPane(0, 1);
		m_spliter.RecalcLayout();
		pNewView->OnInitialUpdate();
		m_spliter.SetActivePane(0, 1);
	}
		break;
	case NM_E:
	{
		Context.m_pNewViewClass = RUNTIME_CLASS(CDelDlg);
		Context.m_pCurrentFrame = this;
		Context.m_pLastView = (CFormView*)m_spliter.GetPane(0, 1);
		m_spliter.DeleteView(0, 1);
		m_spliter.CreateView(0, 1, RUNTIME_CLASS(CDelDlg), CSize(600, 0), &Context);
		CDelDlg* pNewView = (CDelDlg*)m_spliter.GetPane(0, 1);
		m_spliter.RecalcLayout();
		pNewView->OnInitialUpdate();
		m_spliter.SetActivePane(0, 1);
	}
		break;
	default:
		break;
	}
	/*
	if (wParam == NM_A)
	{
		//MessageBox(TEXT("个人信息界面显示"));
		//界面挂载
		Context.m_pNewViewClass = RUNTIME_CLASS(CUserDlg);
		Context.m_pCurrentFrame = this;
		Context.m_pLastView = (CFormView*)m_spliter.GetPane(0, 1);
		m_spliter.DeleteView(0, 1);
		m_spliter.CreateView(0, 1, RUNTIME_CLASS(CUserDlg), CSize(600, 500), &Context);
		CUserDlg* pNewView = (CUserDlg*)m_spliter.GetPane(0, 1);
		m_spliter.RecalcLayout();
		pNewView->OnInitialUpdate();
		m_spliter.SetActivePane(0, 1);
	}
	*/
	
	return LRESULT();
}

个人信息管理窗口

  • 添加对话框资源(ID修改为DIALOG_USER),添加所需控件,在窗口属性中,Border改为None,Style改为Child:
    在这里插入图片描述在这里插入图片描述
  • 选中对话框 -> 右击 -> 添加类 -> 类名:CUserDlg,基类选择CFormView
  • 根据需求,控件关联所需变量
    身份编辑区关联CString m_user,用户名编辑框关联CString m_name,
    新密码编辑框关联CString m_newPwd,确定密码编辑框关联CString m_surePwd。
    在这里插入图片描述
  • 重写界面初始化函数OnInitDialog

// CUserDlg 消息处理程序
void CUserDlg::OnInitialUpdate()
{
	CFormView::OnInitialUpdate();

	//初始化代码
	m_user = TEXT("销售员");
	CInfoFile file;
	CString name, pwd;
	file.ReadDocline();
	file.ReadLogin(name, pwd);
	m_name = name;

	UpdateData(FALSE);

	// TODO: 在此添加专用代码和/或调用基类
}


  • 实现确定按钮功能实现


void CUserDlg::OnBnClickedButton1()
{
	// TODO: 在此添加控件通知处理程序代码
	UpdateData(TRUE);
	if (m_newPwd.IsEmpty() || m_surePwd.IsEmpty())
	{
		MessageBox(TEXT("输入的内容不能为空"));
		return;
	}

	//密码不一致
	if (m_newPwd != m_surePwd)
	{
		MessageBox(TEXT("新密码与确定密码不一致"));
		return;
	}
	CInfoFile file;
	CString name, pwd;
	file.ReadLogin(name, pwd);

	//新密码与原密码要不一致
	if (m_newPwd == pwd)
	{
		MessageBox(TEXT("新密码与原密码重复!"));
		return;
	}

	//修改密码
	CStringA tmpName;
	CStringA tmpPwd;
	tmpName = name;
	tmpPwd = m_newPwd;

	file.WritePwd(tmpName.GetBuffer(), tmpPwd.GetBuffer());
	MessageBox(TEXT("修改密码成功"));

	//清空输入内容
	m_newPwd.Empty();
	m_surePwd.Empty();
	UpdateData(FALSE);
}

  • 取消按钮功能实现

void CUserDlg::OnBnClickedButton2()
{
	// TODO: 在此添加控件通知处理程序代码
	m_surePwd.Empty();
	m_newPwd.Empty();
	UpdateData(FALSE);
}

销售管理窗口

  • 添加新对话框,
    在这里插入图片描述
  • 选中对话框 -> 右击 -> 添加类 -> 类名:CSellDlg,基类选择CFormView
  • 根据需求,控件关联所需变量
    商品名组合框关联CComboBox m_combo,单价编辑框关联int m_price,个数编辑框关联int m_num,销售信息编辑框关联CString m_sellInfo。
    在这里插入图片描述
  • 重写OnInitDialog 函数。


void CSellDlg::OnInitialUpdate()
{
	CFormView::OnInitialUpdate();

	// TODO: 在此添加专用代码和/或调用基类
	CInfoFile file;
	file.ReadDocline();//读取商品信息
	for (auto it = file.ls.begin(); it != file.ls.end(); ++it)
	{
		m_combo.AddString((CString)it->name.c_str());
	}

	file.ls.clear();
	m_combo.SetCurSel(0);
}


  • 处理组合框处理实现

void CSellDlg::OnCbnSelchangeCombo1()
{
	// TODO: 在此添加控件通知处理程序代码
	CString text;
	int index = m_combo.GetCurSel();
	m_combo.GetLBText(index, text);//获取当前选择的字符

	CInfoFile file;
	file.ReadDocline();
	for (auto it = file.ls.begin(); it != file.ls.end(); ++it)
	{
		if (text == it->name.c_str())
		{
			m_price = it->price;
			m_num = 0;
			UpdateData(FALSE);//内容更新到显示空间
		}
	}

	file.ls.clear();
	
}

  • 购买按钮实现

void CSellDlg::OnBnClickedButton1()
{
	// TODO: 在此添加控件通知处理程序代码
	UpdateData(TRUE);//界面数据更新

	if (m_num == 0)
	{
		MessageBox(TEXT("购买个数不能为0"));
		return;
	}

	CString type;
	//获取当前选择项目
	int index = m_combo.GetCurSel();
	m_combo.GetLBText(index, type);
	CString str;
	str.Format(TEXT("商品:%s \r\n 单价:%d \r\n 总价:%d"), type, m_price, m_num, m_price * m_num);

	m_sellInfo = str;
	UpdateData(FALSE);//更新到控件显示
	MessageBox(str);

	//更新到库存文件
	CInfoFile file;
	file.ReadDocline();
	for (auto it = file.ls.begin(); it != file.ls.end(); ++it)
	{
		if (type == it->name.c_str())
		{
			it->num = it->num - m_num;
		}
	}
	file.WriteDocline();//更新内容
	file.ls.clear();

	m_sellInfo.Empty();//清空
	m_num = 0;
	UpdateData(FALSE);
}

  • 取消按钮实现

void CSellDlg::OnBnClickedButton2()
{
	// TODO: 在此添加控件通知处理程序代码
	//取消按钮处理
	m_combo.SetCurSel(0);//选择第一个项目
	m_sellInfo.Empty();
	m_num = 0;
	OnCbnSelchangeCombo1();
}

库存信息窗口

  • 添加对话框资源(ID修改为DIALOG_INFO),添加所需控件。View 属性为 Report(报表表模式):
    在这里插入图片描述
  • 选中对话框 -> 右击 -> 添加类 -> 类名:CInfoDlg,基类选择CFormView
  • 根据需求,控件关联所需变量列表控件关联CListCtrl m_list:
    在这里插入图片描述
  • 重写 OnInitDialog 函数,进行商品信息初始化:

void CInfoDlg::OnInitialUpdate()
{
	CFormView::OnInitialUpdate();

	// TODO: 在此添加专用代码和/或调用基类
	m_list.SetExtendedStyle(m_list.GetExtendedStyle() | LVS_EX_FULLROWSELECT | LVS_EX_GRIDLINES);
	//初始化表
	CString field[] = { TEXT("商品ID"), TEXT("商品名称"), TEXT("商品价格"), TEXT("库存数量") };
	for (int i = 0; i < sizeof(field) / sizeof(field[0]); ++i)
	{
		m_list.InsertColumn(i, field[i], LVCFMT_CENTER, 90);
	}

	//读取数据
	CInfoFile file;
	file.ReadDocline();

	int i = 0;
	CString str;
	for (auto it = file.ls.begin(); it != file.ls.end(); ++it)
	{
		str.Format(TEXT("%d"), it->id);
		m_list.InsertItem(i, str);
		int column = 1;
		m_list.SetItemText(i, column++, (CString)it->name.c_str());
		str.Format(TEXT("%d"), it->price);
		m_list.SetItemText(i, column++, str);
		str.Format(TEXT("%d"), it->num);
		m_list.SetItemText(i, column++, str);
		i++;
	}
}

库存添加窗口

  • 添加对话框资源(ID修改为DIALOG_ADD),添加所需控件。
    在这里插入图片描述
  • 选中对话框 -> 右击 -> 添加类 -> 类名:CAddDlg,基类选择CFormView
  • 根据需求,控件关联所需变量添加个数:商品组合框关联CComboBox m_combo,单价编辑框关联int m_price1,个数编辑框关联int m_num1。
    添加新产品:商品组合框关联CString m_name2,单价编辑框关联int m_price2,个数编辑框关联int m_num2。
    在这里插入图片描述
  • 重写 OnInitDialog 函数,进行商品信息初始化:

void CAddDlg::OnInitialUpdate()
{
	CFormView::OnInitialUpdate();

	// TODO: 在此添加专用代码和/或调用基类
	//读取文件数据
	CInfoFile file;
	file.ReadDocline();
	for (auto it = file.ls.begin(); it != file.ls.end(); ++it)
	{
		m_combo.AddString((CString)it->name.c_str());
	}

	file.ls.clear();

	m_combo.SetCurSel(0);
}

  • 处理组合框动作处理

void CAddDlg::OnCbnSelchangeCombo1()
{
	// TODO: 在此添加控件通知处理程序代码
	CString text;
	int index = m_combo.GetCurSel();
	m_combo.GetLBText(index, text);

	CInfoFile file;
	file.ReadDocline();
	for (auto it = file.ls.begin(); it != file.ls.end(); ++it)
	{
		if (text == it->name.c_str())
		{
			m_price1 = it->price;
			m_num1 = 0;
			UpdateData(FALSE);//内容更新到空间
		}
	}
	file.ls.clear();
}

  • 添加个数确定按钮实现

void CAddDlg::OnBnClickedButton1()
{
	// TODO: 在此添加控件通知处理程序代码
	UpdateData(TRUE);

	if (m_num1 == 0)
	{
		MessageBox(TEXT("个数不能为0"));
		return;
	}

	CString type;
	int index = m_combo.GetCurSel();
	m_combo.GetLBText(index, type);

	CString str;
	str.Format(TEXT("添加了商品:%s \r\n 单价:%d \r\n 个数:%d"), type, m_price1, m_num1);
	MessageBox(str);

	//更新到数据文件中
	CInfoFile file;
	file.ReadDocline();
	for (auto it = file.ls.begin(); it != file.ls.end(); ++it)
	{
		if (type == it->name.c_str())
		{
			it->num += m_num1;
		}
	}
	file.WriteDocline();//更新内容

	file.ls.clear();
	m_num1 = 0;
	UpdateData(FALSE);
}
  • 添加个数取消按钮实现

void CAddDlg::OnBnClickedButton2()
{
	// TODO: 在此添加控件通知处理程序代码
	//添加个数取消按键
	m_combo.SetCurSel(0);
	m_num1 = 0;
	OnCbnSelchangeCombo1();
}


  • 添加新商品确定按钮实现

void CAddDlg::OnBnClickedButton3()
{
	// TODO: 在此添加控件通知处理程序代码\
	//添加新商品确定按钮
	UpdateData(TRUE);

	if (m_num2 <= 0 || m_price2 <= 0 || m_name2.IsEmpty())
	{
		MessageBox(TEXT("输入信息有误"));
		return;
	}

	CInfoFile file;
	file.ReadDocline();
	file.AddLine(m_name2, m_num2, m_price2);
	file.WriteDocline();
	file.ls.clear();
	MessageBox(TEXT("添加成功!"));

	m_name2.Empty();
	m_num2 = 0;
	m_price2 = 0;
	UpdateData(FALSE);
}
  • 添加新商品取消按钮实现

void CAddDlg::OnBnClickedButton4()
{
	// TODO: 在此添加控件通知处理程序代码
	//添加新商品取消按钮
	m_name2.Empty();
	m_num2 = 0;
	m_price2 = 0;
	UpdateData(FALSE);
}

库存删除窗口

  • 添加对话框资源(ID修改为DIALOG_DEL),添加所需控件。
    在这里插入图片描述
  • 选中对话框 -> 右击 -> 添加类 -> 类名:CDelDlg,基类选择CFormView
  • 根据需求,控件关联所需变量
    商品名组合框关联CComboBox m_combo,单价编辑框关联int m_price,个数编辑框关联int m_num。
    在这里插入图片描述
  • 重写 OnInitDialog 函数,进行初始化。

void CDelDlg::OnInitialUpdate()
{
	CFormView::OnInitialUpdate();

	// TODO: 在此添加专用代码和/或调用基类
	CInfoFile file;
	file.ReadDocline();
	for (auto it = file.ls.begin(); it != file.ls.end(); ++it)
	{
		m_combo.AddString((CString)it->name.c_str());
	}

	m_combo.SetCurSel(0);
}

  • 处理组合框事件实现

void CDelDlg::OnCbnSelchangeCombo1()
{
	// TODO: 在此添加控件通知处理程序代码
	CString text;
	int index = m_combo.GetCurSel();
	m_combo.GetLBText(index, text);

	CInfoFile file;
	file.ReadDocline();
	for (auto it = file.ls.begin(); it != file.ls.end(); ++it)
	{
		if (text == it->name.c_str())
		{
			m_price = it->price;
			m_num = 0;
			UpdateData(FALSE);
		}
	}
}

  • 确定按钮功能实现

void CDelDlg::OnBnClickedButton1()
{
	// TODO: 在此添加控件通知处理程序代码
	//确定按钮实现
	UpdateData(TRUE);
	if (m_num == 0)
	{
		MessageBox(TEXT("个数不能为0!"));
		return;
	}

	CString type;
	int index = m_combo.GetCurSel();
	m_combo.GetLBText(index, type);
	CString str;
	str.Format(TEXT("删除商品:%s \r\n 单价:%d \r\n 个数:%d"), type, m_price, m_num);
	MessageBox(str);

	CInfoFile file;
	file.ReadDocline();

	for (auto it = file.ls.begin(); it != file.ls.end(); ++it)
	{
		if (type == it->name.c_str())
		{
			it->num = it->num - m_num;
		}
	}
	file.WriteDocline();//更新数据内容
	file.ls.clear();

	m_num = 0;
	UpdateData(FALSE);//更新显示控件
}

  • 取消按钮功能实现

void CDelDlg::OnBnClickedButton2()
{
	// TODO: 在此添加控件通知处理程序代码
	//取消按钮实现
	m_combo.SetCurSel(0);
	m_num = 0;
	OnCbnSelchangeCombo1();
}

菜单栏实现

  • 切换到资源视图的Menu,删除掉所有默认(帮助除外):
    在这里插入图片描述在这里插入图片描述
  • 右键菜单栏项,添加事件处理程序,选择COMMAND 消息类型,添加至CMainFrame框架类中。
    在这里插入图片描述在这里插入图片描述
  • 处理事件函数中发送消息给主框架实现界面切换。

void CMainFrame::OnEditUser()
{
	// TODO: 在此添加命令处理程序代码
	::PostMessage(AfxGetMainWnd()->GetSafeHwnd(), NM_A, (WPARAM)NM_A, (LPARAM)0);
}


void CMainFrame::OnEditSell()
{
	// TODO: 在此添加命令处理程序代码
	::PostMessage(AfxGetMainWnd()->GetSafeHwnd(), NM_B, (WPARAM)NM_B, (LPARAM)0);
}


void CMainFrame::OnEditCopy()
{
	// TODO: 在此添加命令处理程序代码
	::PostMessage(AfxGetMainWnd()->GetSafeHwnd(), NM_C, (WPARAM)NM_C, (LPARAM)0);
}


void CMainFrame::OnEditAdd()
{
	// TODO: 在此添加命令处理程序代码
	::PostMessage(AfxGetMainWnd()->GetSafeHwnd(), NM_D, (WPARAM)NM_D, (LPARAM)0);
}


void CMainFrame::OnEditDel()
{
	// TODO: 在此添加命令处理程序代码
	::PostMessage(AfxGetMainWnd()->GetSafeHwnd(), NM_E, (WPARAM)NM_E, (LPARAM)0);
}

最终实现

在这里插入图片描述

发布了112 篇原创文章 · 获赞 39 · 访问量 5万+

猜你喜欢

转载自blog.csdn.net/qq_37596943/article/details/105351166