选项模块实现

接口设计

消息类型的定义

  1. 子页面消息定义:
  • 用于通知主框架,子页面的创建状态(主框架监听的消息处理)

#define MSG_PLUGIN_OPTIONS_PAGE_CREATED “msg.plugin.options.page_created”

扩展数据:{[“pageName”:”Avatar”,”pageHandle”:6878868], [“pageName”:”Avatar”,”pageHandle”:6878868]}
  • 用于通知主框架,子页面的状态改变

#define MSG_PLUGIN_OPTIONS_PAGE_STATE_CHANGED “msg.plugin.options.page_statechanged”

扩展数据:{[“pageName”:”Avatar”,”pageHandle”:6878868, ”state”:1] }
  1. 主框架消息定义(各个选项插件监听消息:)

    通知子页面,主框架页面创建完成

    #define MSG_PLUGIN_OPTIONS_FRAMEWND_CREATED “msg.plugin.options.framewnd_created”

    扩展数据:{”FrameWndHandle”:7878456469}

    通知子页面,主框架页面状态改变

    #define MSG_PLUGIN_OPTIONS_FRAMEWND_STATE_CHANGED “msg.plugin.options.framewnd_statechanged”

    扩展数据:{”state”:1}

流程用例图

在这里插入图片描述

交互流程

  1. 主窗体创建处理

选项主窗口创建完毕后,SendMessage(MSG_PLUGIN_OPTIONS_FRAMEWND_CREATED) 给各个选项插件,等待选项插件创建完毕后,发送消息通知选项主窗口自身创建完毕,然后显示选项对话框。

  1. 子页面创建处理

各个选项插件监听 MSG_PLUGIN_OPTIONS_FRAMEWND_CREATED 消息,当监听到此消息时,解析出主窗口的窗口句柄,并以此窗口句柄作为父窗口,创建各自选项页面,页面创建完毕后,向选项窗口SendMessage (MSG_PLUGIN_OPTIONS_PAGE_CREATED,extData),将自身窗口加入到主窗口中。

  1. 子页面状态改变处理

    当子页面状态改变时,通知主窗口自身状态改变SendMessage(MSG_PLUGIN_OPTIONS_PAGE_STATE_CHANGED),方便主窗口激活到不同的状态。

  2. 主窗口状态改变处理

    当主窗口状态改变时(比如进行了保存、取消等操作),则向各个插件 SendMessage(MSG_PLUGIN_OPTIONS_FRAMEWND_STATE_CHANGED),子页面进行相应的操作。

子页面注册使用

在框架实现类中需要实现,注册容器定义:

COptionDlg* m_pOptionDlg=nullptr;//主窗体框架指针
std::map<const std::wstring, CMFCPropertyPage*> m_optionMap;//存储生成的属性子页面

设置子界面注册管理实现

/**
* @brief  设置界面属性子页面使用前需注册
* @author shixun.zhang
* @param const std::wstring& optionName : 注册的子属性页面标识名
* @param CMFCPropertyPage* propertyPage : 注册的子属性页面指针
* @return bool : 注册分页面管理是否成功( true / false )
*/
virtual bool RegisterMFCOption(const std::string& optionName, CMFCPropertyPage* propertyPage);

/**
* @brief  设置界面属性子页面查询
* @author shixun.zhang
* @param const std::wstring& optionName : 注册的子属性页面标识名
* @return CMFCPropertyPage* : 子页面指针
*/
virtual CMFCPropertyPage* GetMFCOptionInterface(const std::string& optionName);

//存储注册的设置界面分页面
std::map<std::string, CMFCPropertyPage*> m_optionMap;

//源码实现
bool MFCOption::RegistorPage(const std::wstring& optionName, CMFCPropertyPage* propertyPage)
{
	if (propertyPage == nullptr)
	{
		return false;
	}

	auto finder = m_optionMap.find(optionName);
	if (finder == m_optionMap.end())
	{
		m_optionMap.insert(std::make_pair(optionName, propertyPage));
		return true;
	}
	
	return false;
}

CMFCPropertyPage* MFCOption::GetOptionPage(const std::wstring& optionName)
{
	auto finder = m_optionMap.find(optionName);
	if (finder == m_optionMap.end())
	{
		return nullptr;
	}

	return finder->second;
}

选项对话框的构建过程

选项主对话框考虑按如下顺序进行初始化:

创建非模态对话框

在这里插入图片描述

对话框初始化时,尝试加载树结构

  • 如果安装文件夹下存在选项树配置文件$(INSTALLDIR)\ Resource\OptionsTree.xml,则从配置文件中加载

  • 如果配置文件不存在,则构建一颗最基本的选项树(或者提示软件被破坏,请重新安装)

  • 树结构加载类:

选项配置文件格式:

文件格式

  • xml文件存储路径:C:\WorkSpace\1005NewPlatCode\src\bin64\Resource
  • xml文件名:OptionSet.xml
  • xml文件格式
 <?xml version="1.0" encoding="utf-8"?>
  <Define> 
  	<Actions Name="General Setting">
  		<Action Name="Basic"  DependOnUi= "Elemental Setup"></Action> 
  		<Action Name="Effect" DependOnUi= "Effect Setup"></Action> 
  		<Action Name="Display" DependOnUi= "Display Setup"></Action> 
  		<Action Name="Context" DependOnUi= "Context Setup"></Action> 
  	</Actions>
  	<Actions Name="Avatar Setting">
  	</Actions>
  	<Actions Name="Conver Setting">
  	</Actions>
  </Define>

树结构不限制树的深度,只有叶子节点存在关联的页面。

xml文件解析处理

  1. 处理方式:xml文件内容转成Json格式数据。
  2. Json格式数据,传入到界面类COptionDlg中,用来组织生成目录树方式

主对话框内的动作:

  • 任意时刻均需要显示一个页面。当选择非叶子节点时,实际显示的页面为第一个子(孙)节点的页面(递归向下寻找)
  • 恢复默认:还原到软件默认的配置:从软件的安装文件夹下拷贝配置信息到用户目录后,加载相应配置信息
  • 应用:应用配置到当前场景中(可以考虑删除)
  • 确定:应用配置到当前场景中,并保存配置后退出
  • 取消:取消所有修改的配置,并退出

存在问题:

为避免出现某一个节点无孩子节点的情况,可以选择一种方式处理(可以当成特殊情况以后考虑处理或者不予处理):

a. 当某一个父节点下所有页面均不存在时,向上递归删除此节点

b. 在创建节点前,预先遍历子页面是否存在

树结构代码实现

头文件

#pragma once

//选项树结构加载类
class TiXmlElement;
class TiXmlDocument;
class COptionsTreeCfg  final
{
public:
	COptionsTreeCfg() {};
	~COptionsTreeCfg() {};

	/**
	* @brief  构建选项树组织
	* @author shixun.zhang
	* @return bool:创建成功或失败。
	*/
	bool BuildOptionTree();

private:
	/**
	* @brief  设置xml文件路径,带文件是否存在检查
	* @param const string& sPath:xml文件全路径
	* @author shixun.zhang
	* @return bool:设置xml文件路径成功与否。
	*/
	bool SetOptionCfgPath(const string sPath);

	/**
	* @brief  获取选项树结构是否加载成功
	* @author shixun.zhang
	* @return bool:获取树结构加载是否成功状态。
	*/
	bool IsSuccessLoadCfg() { return m_IsSuccessLoad; };

	/**
	* @brief  创建默认的选项树组织结构(选项xml文件不存在时)
	* @author shixun.zhang
	* @return bool:创建默认选项树组织是否成功状态。
	*/
	bool CreateDefaultTree();

	/**
	* @brief  加载选项树xml配置文件
	* @param const string& sPath:xml文件全路径
	* @author shixun.zhang
	* @return bool:创建选项树组织是否成功状态。
	* @note - 采用递归的方式深度遍历整颗树,如果节点不存在则不创建此节点
	*/
	bool BeginLoadTreeCfg();
	
	/**
	* @brief  根据xml文件节点,创建选项树组织
	* @param const TiXmlElement* elment:传入解析的xml文件节点(默认采用的是xml文件的根节点)
	* @author shixun.zhang
	*/
	bool ParseOptionNode(const TiXmlElement* elment);

private:
	string  m_strPath = "";
	TiXmlElement*  m_pRoot = nullptr;
	TiXmlDocument* m_doc = nullptr;
	bool m_IsSuccessLoad = false;

};

源文件实现

#include "pch.h"
#include <filesystem>
#include <tinyxml.h>
#include "OptionsTreeCfg.h"
#include "MFCOption.h"

bool COptionsTreeCfg::SetOptionCfgPath(const string sPath)
{
	if (sPath.empty() || !std::filesystem::exists(sPath))
	{
		return false;
	}

	m_strPath = sPath;
	return true;
}

bool COptionsTreeCfg::BeginLoadTreeCfg()
{
	string strFilePath = m_strPath;

	m_doc = new TiXmlDocument();
	m_pRoot = nullptr;
	if (m_doc->LoadFile(strFilePath.c_str()) && (m_pRoot = m_doc->RootElement()))
	{
		return ParseOptionNode(m_pRoot);
	}
	return false;
}

bool COptionsTreeCfg::ParseOptionNode(const TiXmlElement* elment)
{
	if (elment == nullptr) return false;

	//通过TiXmlElement* 创建树节点
	std::function<void(const TiXmlElement * elment, HTREEITEM tree, COptionDlg * pDld)> createOptionsTreeFucCall;
	createOptionsTreeFucCall = [&createOptionsTreeFucCall] (const TiXmlElement* rootNode, HTREEITEM parentTreeNode, COptionDlg* pDld){
		if (nullptr != rootNode)
		{
			const TiXmlElement* pParentNode = rootNode->FirstChildElement("Node");//孩子节点
			HTREEITEM currentTreeNode = nullptr;
			bool isCreateNotRefPageNode = false;
			if (nullptr != pParentNode)
			{
				while (pParentNode)
				{
					const char* sName = pParentNode->Attribute("Name");
					const char* sRefPage = pParentNode->Attribute("RefPage");
					//创建孩子树节点

					//判断是否有子节点
					const TiXmlElement* pChildNode = pParentNode->FirstChildElement("Node");//孩子节点
					if (nullptr != pChildNode)
					{
						isCreateNotRefPageNode = true;
						pDld->CreateOptionTree(sName, sRefPage, parentTreeNode, currentTreeNode, isCreateNotRefPageNode);
						
						//当前节点包含孩子节点
						createOptionsTreeFucCall(pParentNode, currentTreeNode, pDld);
						//非叶子节点,则绑定第一个叶子节点属性页
						pDld->BindParentTreeNodeToFirstLeafNodeRefPage(currentTreeNode);

					}
					else
					{
						isCreateNotRefPageNode = false;
						bool isCreateTreeNodeSuccess =	pDld->CreateOptionTree(sName, sRefPage, parentTreeNode, currentTreeNode, isCreateNotRefPageNode);
						if (!isCreateTreeNodeSuccess)//创建子节点失败,当前节点仍为父节点
							currentTreeNode = parentTreeNode;
					}
					pParentNode = pParentNode->NextSiblingElement("Node");
				}

				//树节点创建失败,则递归查找删除父节点,父节点无孩子节点
				pDld->DeleteNonleafTreeNode(currentTreeNode);
			}
		}
	};


	/*IOptionInterface* pOptionInterface = dynamic_cast<IOptionInterface*>(eZPlat::PlatformFramework::GetSingleton()->GetPluginsManager()->GetPluginInterface("ui.MFCOption"));
	assert(pOptionInterface);*/

	COptionDlg* pDld = (COptionDlg*)MFCOption::GetSingleton()->GetWnd();
	if (nullptr == pDld) return false;

	createOptionsTreeFucCall(elment, nullptr, pDld);//root

	m_IsSuccessLoad = true;
	return true;
}

bool COptionsTreeCfg::CreateDefaultTree()
{
	// 定义一个TiXmlDocument类指针
	TiXmlElement* RootElement = new TiXmlElement("OptionRoot");

	/************************************************************************
	General Setting参数
	************************************************************************/
	TiXmlElement* GeneralElement = new TiXmlElement("Node");
	GeneralElement->SetAttribute("Name", "General Setting");
	RootElement->LinkEndChild(GeneralElement);

	//Basic
	TiXmlElement* BasicElement = new TiXmlElement("Node");
	GeneralElement->LinkEndChild(BasicElement);
	BasicElement->SetAttribute("Name", "Basic");
	BasicElement->SetAttribute("RefPage", "Option.PageBasic");
	//Effect
	TiXmlElement* EffectElement = new TiXmlElement("Node");
	GeneralElement->LinkEndChild(EffectElement);
	EffectElement->SetAttribute("Name", "Effect");
	EffectElement->SetAttribute("RefPage", "Option.PageEffect");

	//Display
	TiXmlElement* DisplayElement = new TiXmlElement("Node");
	GeneralElement->LinkEndChild(DisplayElement);
	DisplayElement->SetAttribute("Name", "Display");
	DisplayElement->SetAttribute("RefPage", "Option.PageDisplay");

	//Camera
	TiXmlElement* CameraElement = new TiXmlElement("Node");
	GeneralElement->LinkEndChild(CameraElement);
	CameraElement->SetAttribute("Name", "Camera");
	CameraElement->SetAttribute("RefPage", "Option.PageCamera");

	//Context
	TiXmlElement* ContextElement = new TiXmlElement("Node");
	GeneralElement->LinkEndChild(ContextElement);
	ContextElement->SetAttribute("Name", "Context");
	ContextElement->SetAttribute("RefPage", "Option.PageContext");

	//Cache
	TiXmlElement* CacheElement = new TiXmlElement("Node");
	GeneralElement->LinkEndChild(CacheElement);
	CacheElement->SetAttribute("Name", "Cache");
	CacheElement->SetAttribute("RefPage", "Option.PageCache");

	//Save
	TiXmlElement* SaveElement = new TiXmlElement("Node");
	GeneralElement->LinkEndChild(SaveElement);
	SaveElement->SetAttribute("Name", "Save");
	SaveElement->SetAttribute("RefPage", "Option.PageSave");

	//Undo
	TiXmlElement* UndoElement = new TiXmlElement("Node");
	GeneralElement->LinkEndChild(UndoElement);
	UndoElement->SetAttribute("Name", "Undo");
	UndoElement->SetAttribute("RefPage", "Option.PageUndo");

	/************************************************************************
	Avatar Setting参数
	************************************************************************/
	TiXmlElement* AvatarElement = new TiXmlElement("Node");
	AvatarElement->SetAttribute("Name", "Avatar Setting");
	RootElement->LinkEndChild(AvatarElement);

	//Param
	TiXmlElement* ParamElement = new TiXmlElement("Node");
	AvatarElement->LinkEndChild(ParamElement);
	ParamElement->SetAttribute("Name", "Param");
	ParamElement->SetAttribute("RefPage", "Avatar.PageParam");

	/************************************************************************
	Conver Setting参数
	************************************************************************/
	TiXmlElement* ConverElement = new TiXmlElement("Node");
	ConverElement->SetAttribute("Name", "Conver Setting");
	RootElement->LinkEndChild(ConverElement);
	
	//Common
	TiXmlElement* CommonElement = new TiXmlElement("Node");
	ConverElement->LinkEndChild(CommonElement);
	CommonElement->SetAttribute("Name", "Common");
	CommonElement->SetAttribute("RefPage", "Conver.PageConverCommon");
	//Dgn
	TiXmlElement* DgnElement = new TiXmlElement("Node");
	ConverElement->LinkEndChild(DgnElement);
	DgnElement->SetAttribute("Name", "Dgn");
	DgnElement->SetAttribute("RefPage", "Conver.PageConverDgn");
	//Caita
	TiXmlElement* CaitaElement = new TiXmlElement("Node");
	ConverElement->LinkEndChild(CaitaElement);
	CaitaElement->SetAttribute("Name", "Caita");
	CaitaElement->SetAttribute("RefPage", "Conver.PageConverCaita");
	//Dwg
	TiXmlElement* DwgElement = new TiXmlElement("Node");
	ConverElement->LinkEndChild(DwgElement);
	DwgElement->SetAttribute("Name", "Dwg");
	DwgElement->SetAttribute("RefPage", "Conver.PageConverDwg");
	//PointCloud
	TiXmlElement* PointCloudElement = new TiXmlElement("Node");
	ConverElement->LinkEndChild(PointCloudElement);
	PointCloudElement->SetAttribute("Name", "PointCloud");
	PointCloudElement->SetAttribute("RefPage", "Conver.PageConverPointCloud");

	/************************************************************************
	Simulation Setting参数
	************************************************************************/
	TiXmlElement* SimulationElement = new TiXmlElement("Node");
	SimulationElement->SetAttribute("Name", "Simulation Setting");
	RootElement->LinkEndChild(SimulationElement);

	//Common
	TiXmlElement* PositionElement = new TiXmlElement("Node");
	SimulationElement->LinkEndChild(PositionElement);
	PositionElement->SetAttribute("Name", "Position");
	PositionElement->SetAttribute("RefPage", "Simulation.PagePosition");

	ParseOptionNode(RootElement);

	return true;
}

bool COptionsTreeCfg::BuildOptionTree()
{
	COptionDlg* pDld = (COptionDlg*)MFCOption::GetSingleton()->GetWnd();
	if (nullptr == pDld) return false;

	//根据xml文件创建选项树组织
	if (!IsSuccessLoadCfg())
	{
		//指定Option配置文件路径
		std::filesystem::path menuPath(eZPlat::PlatformFramework::GetSingleton()->GetResourceFolderPath());
		menuPath /= "OptionsTree.xml";

		if (!SetOptionCfgPath(menuPath.string()))
			CreateDefaultTree();
		else
		{
			BeginLoadTreeCfg();
			if (!IsSuccessLoadCfg())
				CreateDefaultTree();
		}
	}

	//显示指定树节点
	pDld->ShowTreeItemRefPage((void*)"General Setting", 0);

	return true;
}

参数读取与保存

  1. 在安装目录下存储有参数的原始配置数据;当软件第一次启动时,自动将配置数据拷贝到用户目录下

  2. 参数数据统一保存到一个配置文件中;主框架提供数据目录,用于访问不同分组的参数数据;

const wchar_t* GetConfigFolderPath() const { return m_strConfigFolderPath.c_str(); }

  1. 当点击恢复默认功能时,将安装文件夹下的配置数据拷贝到用户目录下形成默认数据。

  2. 启动多个客户端时,以最后一次保存的数据为准。

  3. 参数存储格式均为json,所以所有参数均为u8

  4. 对于所有参数,请创建一个描述表,且在默认参数配置中进行配置,以方便后续查询使用

访问参数时:

 ConfigKit* configKit = ConfigKit::Using();

  go.m_bForceRawMode = configKit->GetBoolean("ForceRawMode", false, ”xre_setting”);

(为方便兼容原有代码,将分组数据放在接口的末尾)

访问子结构时:

Json::*Value* jItem = configKit–>GetObject(”PartItem”, ”xre_setting”);

子页面实现对数据的读写

//配置文件与ui界面进行想换交互
bool CPropRollBack::ConfigKitDataToUi(bool bMode)
{
	ConfigKit* pSettingPtr = ConfigKit::Using();
	if (!pSettingPtr)
		return false;

	if (bMode)//读操作 配置文件到ui界面
	{
		m_spinRollBackCnt.SetBuddy(GetDlgItem(IDC_EDIT_ROLLBACK_CNT));
		m_spinRollBackCnt.SetRange(0, 100);

		CString str;
		str = StringAnsiToWide(pSettingPtr->GetString("RollbackCnt", "50")).c_str();
		m_editRollBackCnt.SetWindowTextW(str);
	}
	else
	{
		CString cstr;
		std::string str;
		m_editRollBackCnt.GetWindowTextW(cstr);
		str = StringWideToAnsi(cstr.GetBuffer());
		pSettingPtr->Set("RollbackCnt", str);
	}

	return true;
}

Dll资源切换

问题原因:在MFC使用过程中,遇到DLL资源与主EXE资源冲突问题。

基于RAII 技术 ,MFC DLL资源动态切换实现

#pragma once
#include "afxwin.h"
class ModualResource
{
public:
	ModualResource(LPCWSTR strModualName) :m_hOldRes(NULL)
	{
		m_hCurHandle = ::GetModuleHandle(strModualName);
		if (m_hCurHandle != NULL)
		{
			m_hOldRes = ::AfxGetResourceHandle();
			::AfxSetResourceHandle(m_hCurHandle);
		}
	}
	~ModualResource()
	{
		if (m_hOldRes != NULL)
		{
			::AfxSetResourceHandle(m_hOldRes);
			m_hOldRes = NULL;
		}
	}
	HMODULE  GetCurModuleHandle() { return m_hCurHandle; }

private:
	HINSTANCE   m_hOldRes;
	HMODULE     m_hCurHandle;
};

//使用例子
CString strModul(_T("MFCAvatar.dll"));
CModualResource CurrentModualResource(strModul);

主窗体界面代码实现

头文件

#pragma once
#include "CPropElement.h"
#include "CPropCamera.h"
#include "CPropContext.h"
#include "CPropDisplay.h"
#include "CPropEffect.h"
#include "CPropSave.h"
#include "CPropRollBack.h"
#include "CPropCache.h"
#include "CPageScenes.h"
#include "MFCOption.h"

// COptionDlg 对话框

class IOptionPropertyPage;
class COptionDlg : public CDialogEx, public AutoRegMessageListener
{
	DECLARE_DYNAMIC(COptionDlg)

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

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

protected:
	virtual void DoDataExchange(CDataExchange* pDX);    // DDX/DDV 支持
	DECLARE_MESSAGE_MAP()
public:
	virtual BOOL OnInitDialog();
	virtual BOOL DestroyWindow();	
	afx_msg void OnBnClickedButtonApply();//应用
	afx_msg void OnBnClickedOk();//确定
	afx_msg void OnBnClickedButtonDefault();//恢复
	afx_msg void OnNMClickListtree(NMHDR* pNMHDR, LRESULT* pResult);
	afx_msg void OnMove(int x, int y);
	virtual void OnCancel();//取消
	bool HandlerMessage(Message& msg);

public:
	void SetModify(CMFCPropertyPage* propPtr);
	//保存“属性页面”对话框的指针
	void SetPtr(CString name, CMFCPropertyPage* preptr, CMFCPropertyPage* ptr);
	
	/**
	* @brief  绑定父节点(非叶子节点)到第一个叶子节点关联子页面
	* @author shixun.zhang
	* @param HTREEITEM parentNode : 需要绑定的父节点(非叶子节点)
	* @return bool : 绑定成功与否( true / false )
	* @note - 递归查找第一个叶子节点
	*/
	bool BindParentTreeNodeToFirstLeafNodeRefPage(HTREEITEM parentNode);
	
	/**
	* @brief  创建选项界面树组织
	* @author shixun.zhang
	* @param const char* nodeName: 树节点名称
	* @param const char* sRefPage: 关联子页面标识名
	* @param HTREEITEM parentNode: 创建树节点依赖的父级树节点
	* @param HTREEITEM& currentNode: 当前树节点
	* @param bool isCreateNotRefPageNode:是否创建非关联子页面树节点
	* @return bool : 绑定树节点成功与否( true / false )
	*/
	bool CreateOptionTree(const char* nodeName, const char* sRefPage, HTREEITEM parentNode, HTREEITEM& currentNode, bool isCreateNotRefPageNode);
	
	/**
	* @brief  删除无子节点的非叶子节点
	* @author shixun.zhang
	* @param HTREEITEM parentNode : 需要删除的非叶子节点
	*/
	void DeleteNonleafTreeNode(HTREEITEM hItem);

	/**
	* @brief  恢复安装目录中的Option配置文件,拷贝到用户目录中。
	* @author shixun.zhang
	*/
	void ReserveConfigFolderOptionDataToAppRootFolder();

	/**
	* @brief  显示树节点关联的子页面
	* @param void* data : 显示树节点关联的子页面依赖条件
	* @param int nType : 0:(const char* 类型,即树节点名称);1:(HTREEITEM类型,即树节点)
	* @return bool : 显示树节点关联的子页面成功与否( true / false )
	* @author shixun.zhang
	*/
	bool ShowTreeItemRefPage(void* data, int nType);
protected:
	/**
	* @brief  根据指定的树节点名称,查找树节点
	* @param HTREEITEM  hroot : 从此树节点进行遍历查找子节点()
	* @param CString strtext : 查找的树节点名称
	* @author shixun.zhang
	*/
	HTREEITEM  FindTreeItemByName(HTREEITEM  hroot, CString strtext);

private:
	CPropSave* m_pPropSave = nullptr;
	CPropEffect* m_pPropEffect = nullptr;
	CPropDisplay* m_pPropDisplay = nullptr;
	CPropContext* m_pPropContext = nullptr;
	CPropCamera* m_pPropCamera = nullptr;
	CPropElement* m_pPropElemental = nullptr;
	CPropCache* m_pPropCache = nullptr;
	CPropRollBack* m_pPropRollBack = nullptr;
	CPageScenes* m_pPageScenes = nullptr;

private:
	bool m_bEdit = false;
	std::vector<CMFCPropertyPage*> m_editPtrVec;//修改了的页面的指针数组
	friend class MFCOption;
public:
	std::map<HTREEITEM, CMFCPropertyPage*> m_ptrMap;//属性对话框指针字典
public:
	CTreeCtrl m_tree;
	CButton m_butOK;
	CButton m_butApply;
	CButton m_butDefault;
};

源文件

// COptionDlg.cpp: 实现文件
//

#include "pch.h"
#include <filesystem>
#include <fstream>
#include "COptionDlg.h"
#include "afxdialogex.h"


// COptionDlg 对话框

IMPLEMENT_DYNAMIC(COptionDlg, CDialogEx)

COptionDlg::COptionDlg(CWnd* pParent /*=nullptr*/)
	: CDialogEx(IDD_MAINDLG, pParent)
{
	PlatformFramework::GetSingleton()->GetMessageBus()->RegisterMsgListener(MSG_PLUGIN_OPTIONS_FRAMEWND_CREATED, GetMessageListener());
	PlatformFramework::GetSingleton()->GetMessageBus()->RegisterMsgListener(MSG_PLUGIN_OPTIONS_PAGE_STATE_CHANGED, GetMessageListener());
	PlatformFramework::GetSingleton()->GetMessageBus()->RegisterMsgListener(MSG_PLUGIN_OPTIONS_FRAMEWND_STATE_CHANGED, GetMessageListener());
}

COptionDlg::~COptionDlg()
{
}

void COptionDlg::DoDataExchange(CDataExchange* pDX)
{
	CDialogEx::DoDataExchange(pDX);
	DDX_Control(pDX, IDC_LISTTREE, m_tree);
	DDX_Control(pDX, IDOK, m_butOK);
	DDX_Control(pDX, IDC_BUTTON_APPLY, m_butApply);
	DDX_Control(pDX, IDC_BUTTON_DEFAULT, m_butDefault);
}


BEGIN_MESSAGE_MAP(COptionDlg, CDialogEx)
	ON_BN_CLICKED(IDC_BUTTON_APPLY, &COptionDlg::OnBnClickedButtonApply)
	ON_BN_CLICKED(IDOK, &COptionDlg::OnBnClickedOk)
	ON_BN_CLICKED(IDC_BUTTON_DEFAULT, &COptionDlg::OnBnClickedButtonDefault)
	ON_WM_MOVE()
	ON_NOTIFY(NM_CLICK, IDC_LISTTREE, &COptionDlg::OnNMClickListtree)
END_MESSAGE_MAP()


// COptionDlg 消息处理程序

void COptionDlg::SetPtr(CString name, CMFCPropertyPage* preptr, CMFCPropertyPage* ptr)
{
	if (ptr)
	{
		HTREEITEM parentitem = nullptr;
		if (preptr)//有父节点
		{
			std::map<HTREEITEM, CMFCPropertyPage*>::iterator it;
			for (it = m_ptrMap.begin(); it != m_ptrMap.end(); it++)
			{
				if ((*it).second == preptr)
				{
					parentitem = (*it).first;//父节点在目录树中的句柄
				}
			}
		}
		
		HTREEITEM item = m_tree.InsertItem(name, parentitem);
		m_tree.SetItemData(item, (DWORD_PTR)ptr);
		m_ptrMap.insert(std::pair<HTREEITEM, CMFCPropertyPage*>(item, ptr));
		ptr->ShowWindow(SW_HIDE);

		CRect rec;
		ptr->GetClientRect(&rec);
		rec.left += 140;
		rec.right += 140;
		rec.top += 25;
		rec.bottom += 25;
		ClientToScreen(&rec);
		ptr->MoveWindow(rec);

		m_bEdit = false;
		m_editPtrVec.clear();
		m_butOK.EnableWindow(m_bEdit);
		m_butApply.EnableWindow(m_bEdit);
	}
}

bool COptionDlg::BindParentTreeNodeToFirstLeafNodeRefPage(HTREEITEM parentNode)
{
	std::function<void(HTREEITEM parentNode, COptionDlg * pDld)> blendFucCall;
	blendFucCall = [&blendFucCall](HTREEITEM parentNode, COptionDlg* pDld)  mutable->bool{
		if (parentNode != NULL)
		{
			//判断当前选中的节点是否有子节点	
			if (pDld->m_tree.ItemHasChildren(parentNode))
			{
				//如果有子节点
				HTREEITEM hChildItem = pDld->m_tree.GetChildItem(parentNode);
				if (hChildItem)
				{
					CMFCPropertyPage* pPropertyPage = (CMFCPropertyPage*)pDld->m_tree.GetItemData(hChildItem);
					if (pPropertyPage)
					{
						pDld->m_tree.SetItemData(parentNode, (DWORD_PTR)pPropertyPage);
						pDld->m_ptrMap.insert(std::pair<HTREEITEM, CMFCPropertyPage*>(parentNode, pPropertyPage));
						return true;
					}
					else
					{
						blendFucCall(hChildItem, pDld);
					}
					return false;
				}
			}
		}
	};

	blendFucCall(parentNode, this);

	return true;
}

bool COptionDlg::CreateOptionTree(const char* nodeName, const char* sRefPage, HTREEITEM parentNode, HTREEITEM & currentNode, bool isCreateNotRefPageNode)
{
	if (nodeName == nullptr) return false;
	if(isCreateNotRefPageNode)
	{
		currentNode = m_tree.InsertItem(TR(Xre::StringAnsiToWide(nodeName)), parentNode);
	}
	else
	{
		if (sRefPage == nullptr) return false;
		CMFCPropertyPage* pPropertyPage = MFCOption::GetSingleton()->GetOptionPage(Xre::StringAnsiToWide(sRefPage).c_str());
		if (nullptr == pPropertyPage) return false;
		if (pPropertyPage)
		{
			currentNode = m_tree.InsertItem(TR(Xre::StringAnsiToWide(nodeName)), parentNode);
			//设置树结构体与分页面相关联
			m_tree.SetItemData(currentNode, (DWORD_PTR)pPropertyPage);
			m_ptrMap.insert(std::pair<HTREEITEM, CMFCPropertyPage*>(currentNode, pPropertyPage));
		}
	}
	
	return true;
}

void COptionDlg::DeleteNonleafTreeNode(HTREEITEM hItem)
{
	if (hItem != NULL)
	{
		//判断当前选中的节点是否有子节点,非叶子节点不包含数据	
		if (!m_tree.GetItemData(hItem) && !m_tree.ItemHasChildren(hItem))
		{
			m_tree.DeleteItem(hItem);
		}
	}
}


void COptionDlg::ReserveConfigFolderOptionDataToAppRootFolder()
{
	//获取安装目录和用户目录的配置文件路径
	string sFileName = "setting.json";
	std::filesystem::path configPath = eZPlat::PlatformFramework::GetSingleton()->GetConfigFolderPath();
	std::filesystem::path setupConfigPath(eZPlat::PlatformFramework::GetSingleton()->GetAppRootFolderPath()/*GetResourceFolderPath()*/);
	//configPath /= "RINVR";

	std::filesystem::path configFolderPath = configPath / "config/";
	std::filesystem::path reloadPath = configFolderPath;
	configFolderPath /= sFileName;
	configFolderPath.make_preferred();

	std::filesystem::path rootFolderPath = setupConfigPath / "config/";
	rootFolderPath /= sFileName;
	rootFolderPath.make_preferred();

	char content[512];//一次获取字节数
	std::ifstream ifs(rootFolderPath, std::fstream::in);//读取文件
	std::ofstream ofs(configFolderPath, std::fstream::trunc | std::fstream::out);//写方式打开
	if (ifs.is_open() && ofs.is_open())
	{
		while (!ifs.eof()) {
			ifs.getline(content, 256);
			ofs << content << endl;
		}
		ifs.close();
		ofs.close();
	}
	//配置文件重加载,否则内存里还是启动时加载的数据
	ConfigKit::Initialize(reloadPath);
}

bool COptionDlg::ShowTreeItemRefPage(void* data, int nType)
{
	HTREEITEM treeItem = nullptr;
	if (0 == nType)//name 树节点名称
	{
		const char* sName = nullptr;
		try
		{
			sName = reinterpret_cast<const char*>(data);
		}
		catch (const std::exception&)
		{

		}

		if (nullptr == sName) return false;
		treeItem = FindTreeItemByName(m_tree.GetRootItem(), TR(Xre::StringAnsiToWide(sName)));
	}
	else if (1 == nType)//HTREEITEM 树节点
	{
		try
		{
			treeItem = reinterpret_cast<HTREEITEM>(data);
		}
		catch (const std::exception&)
		{

		}

	}

	if (treeItem)
	{
		std::map<HTREEITEM, CMFCPropertyPage*>::iterator it;
		for (it = m_ptrMap.begin(); it != m_ptrMap.end(); it++)
		{
			(*it).second->ShowWindow(SW_HIDE);//全部属性窗口隐藏
		}
		CMFCPropertyPage* propPage = (CMFCPropertyPage*)m_tree.GetItemData(treeItem);
		if (propPage)
		{
			propPage->ShowWindow(SW_SHOW);//显示点击的窗口
			PlatformFramework::GetSingleton()->GetMessageBus()->SendMessage(MSG_PLUGIN_OPTIONS_FRAMEWND_STATE_CHANGED, "OnReset", propPage);
		}

		return true;
	}

	return false;

}

HTREEITEM COptionDlg::FindTreeItemByName(HTREEITEM hroot, CString strtext)
{
	HTREEITEM hfind;

	//空树,直接返回NULL
	if (NULL == hroot)
		return  NULL;

	//否则遍历查找
	while (hroot != NULL)
	{
		//当前节点即所需查找节点
		if (m_tree.GetItemText(hroot) == strtext)
			return hroot;

		//查找当前节点的子节点
		if (m_tree.ItemHasChildren(hroot))
		{
			hroot = m_tree.GetChildItem(hroot);
			//递归调用查找子节点下节点
			hfind = FindTreeItemByName(hroot, strtext);
			if (hfind)
				return hfind;
			else //子节点中未发现所需节点,继续查找兄弟节点
				hroot = m_tree.GetNextSiblingItem(m_tree.GetParentItem(hroot));
		}
		else {

			//若无子节点,继续查找兄弟节点
			hroot = m_tree.GetNextSiblingItem(hroot);
		}
	}

	return hroot;
}

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

	// TODO:  在此添加额外的初始化

	//发消息通知模块注册设置对话框

	//初始时“确定、应用、默认”按钮不可用
	m_bEdit = false;
	m_editPtrVec.clear();
	m_butOK.EnableWindow(m_bEdit);
	m_butApply.EnableWindow(m_bEdit);
	SetDialogLanguage(m_hWnd, IDD_MAINDLG, _T("MFCOption"));

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

void COptionDlg::SetModify(CMFCPropertyPage* propPtr)
{
	m_bEdit = true;
	bool hasPtr = false;
	for (int i=0;i<m_editPtrVec.size();i++)
	{
		if (m_editPtrVec[i] == propPtr)
		{
			hasPtr = true;
			break;
		}
	}
	if (!hasPtr)
		m_editPtrVec.push_back(propPtr);
	m_butOK.EnableWindow(m_bEdit);
	m_butApply.EnableWindow(m_bEdit);
}

BOOL COptionDlg::DestroyWindow()
{
	// TODO: 在此添加专用代码和/或调用基类

	if (m_pPropEffect)
	{
		delete m_pPropEffect;
		m_pPropEffect = nullptr;
	}
	if (m_pPropContext)
	{
		delete m_pPropContext;
		m_pPropContext = nullptr;
	}
	if (m_pPropCamera)
	{
		delete m_pPropCamera;
		m_pPropCamera = nullptr;
	}
	if (m_pPropSave)
	{
		delete m_pPropSave;
		m_pPropSave = nullptr;
	}
	if (m_pPropDisplay)
	{
		delete m_pPropDisplay;
		m_pPropDisplay = nullptr;
	}
	if (m_pPropElemental)
	{
		delete m_pPropElemental;
		m_pPropElemental = nullptr;
	}
	if (m_pPropCache)
	{
		delete m_pPropCache;
		m_pPropCache = nullptr;
	}
	if (m_pPropRollBack)
	{
		delete m_pPropRollBack;
		m_pPropRollBack = nullptr;
	}
	if (m_pPageScenes)
	{
		delete m_pPageScenes;
		m_pPageScenes = nullptr;
	}

	return CDialogEx::DestroyWindow();
}


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

		//传apply消息
		//数据交互由各页面自己完成
	for (int i=0;i<m_editPtrVec.size();i++)
	{
		//m_editPtrVec[i]->OnApply();
		PlatformFramework::GetSingleton()->GetMessageBus()->SendMessage(MSG_PLUGIN_OPTIONS_FRAMEWND_STATE_CHANGED, "OnApply", m_editPtrVec[i]);
	}

	m_editPtrVec.clear();//所有修改了的页面交换数据完成后数组重置;
	ConfigKit* ptr = ConfigKit::Using();//这里只保存setting.json文件,其他配置文件在发生修改后自行保存!!
	if (ptr)
		ptr->Save();
}


void COptionDlg::OnBnClickedOk()
{
	// TODO: 在此添加控件通知处理程序代码
	OnBnClickedButtonApply();
	std::map<HTREEITEM, CMFCPropertyPage*>::iterator it;
	for (it = m_ptrMap.begin(); it != m_ptrMap.end(); it++)
	{
		(*it).second->ShowWindow(SW_HIDE);
	}
	CDialogEx::OnOK();
}


void COptionDlg::OnBnClickedButtonDefault()
{
	// TODO: 在此添加控件通知处理程序代码
	//当点击恢复默认功能时,将安装文件夹下的配置数据拷贝到用户目录下形成默认数据。
	ReserveConfigFolderOptionDataToAppRootFolder();

	std::map<HTREEITEM, CMFCPropertyPage*>::iterator it;
	for (it = m_ptrMap.begin(); it != m_ptrMap.end(); it++)
	{
		//(*it).second->OnReset();//恢复默认
		//界面恢复默认
		PlatformFramework::GetSingleton()->GetMessageBus()->SendMessage(MSG_PLUGIN_OPTIONS_FRAMEWND_STATE_CHANGED, "OnReset", (*it).second);
		//应用默认设置
		PlatformFramework::GetSingleton()->GetMessageBus()->SendMessage(MSG_PLUGIN_OPTIONS_FRAMEWND_STATE_CHANGED, "OnApply", (*it).second);
	}
}


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

	if (m_editPtrVec.size())
	{
		int ret = MessageBox(TR(_T("Wish to waive this amendment?")), TR(L"Tips"),MB_YESNO);
		if (ret == IDYES)
		{
			for (int i=0;i<m_editPtrVec.size();i++)
			{
				//发还原消息
				//m_editPtrVec[i]->OnCancel();
				PlatformFramework::GetSingleton()->GetMessageBus()->SendMessage(MSG_PLUGIN_OPTIONS_FRAMEWND_STATE_CHANGED, "OnCancel", m_editPtrVec[i]);

			}
			m_bEdit = false;
			m_editPtrVec.clear();//数组重置
			m_butOK.EnableWindow(m_bEdit);
			m_butApply.EnableWindow(m_bEdit);
		}
		else if (ret == IDNO)
			return;
	}

	std::map<HTREEITEM, CMFCPropertyPage*>::iterator it;
	for (it = m_ptrMap.begin(); it != m_ptrMap.end(); it++)
	{
		(*it).second->ShowWindow(SW_HIDE);
	}
	CDialogEx::OnCancel();
}


bool COptionDlg::HandlerMessage(Message& msg)
{
	if (MSG_PLUGIN_OPTIONS_FRAMEWND_CREATED == msg.msgName)
	{
		auto parentPtr = msg.GetDataValue<void*>(0);
		CWnd*& pWnd = *(CWnd**)(*parentPtr);
		if (nullptr == pWnd) return false;

		//创建属性页
		CMFCPropertyPage* pPropPage = nullptr;
		auto CreatePropPage = [&pPropPage](UINT nID, CWnd* pParentWnd) mutable->bool{
			if (nullptr != pPropPage) return false;
			switch (nID)
			{
			case IDD_PROPPAGE_ELEMENT://基础设置
			{
				pPropPage = new CPropElement();
				pPropPage->Create(nID, pParentWnd);
				break;
			}
			case IDD_PROPPAGE_SAVE://保存设置
			{
				pPropPage = new CPropSave();
				pPropPage->Create(nID, pParentWnd);
				break;
			}
			case IDD_PROPPAGE_EFFECT://效果设置
			{
				pPropPage = new CPropEffect();
				pPropPage->Create(nID, pParentWnd);
				break;
			}	
			case IDD_PROPPAGE_DISPLAY://显示设置
			{
				pPropPage = new CPropDisplay();
				pPropPage->Create(nID, pParentWnd);
				break;
			}
			case IDD_PROPPAGE_CONTEXT://环境设置
			{
				pPropPage = new CPropContext();
				pPropPage->Create(nID, pParentWnd);
				break;
			}
			case IDD_PROPPAGE_CAMERA://相机设置
			{
				pPropPage = new CPropCamera();
				pPropPage->Create(nID, pParentWnd);
				break;
			}
			case IDD_PROPPAGE_CACHE://缓存设置
			{
				pPropPage = new CPropCache();
				pPropPage->Create(nID, pParentWnd);
				break;
			}
			case IDD_PROPPAGE_ROLLBACK://回退设置
			{
				pPropPage = new CPropRollBack();
				pPropPage->Create(nID, pParentWnd);
				break;
			}
			case IDD_PROPPAGE_SCENES://模拟设置
			{
				pPropPage = new CPageScenes();
				pPropPage->Create(nID, pParentWnd);
				break;
			}
			default:
				break;
			}

			if (nullptr == pPropPage) return false;

			CRect rec;
			pPropPage->GetClientRect(&rec);
			rec.left += 140;
			rec.right += 140;
			rec.top += 25;
			rec.bottom += 25;
			pParentWnd->ClientToScreen(&rec);
			pPropPage->MoveWindow(rec);
			pPropPage->ShowWindow(SW_HIDE);

			return true;
		};

		if (!m_pPropElemental)//基础设置
		{
			pPropPage = nullptr;
			if (CreatePropPage(IDD_PROPPAGE_ELEMENT, pWnd) && pPropPage)
			{
				m_pPropElemental = (CPropElement*)pPropPage;
				PlatformFramework::GetSingleton()->GetMessageBus()->SendMessage(MSG_PLUGIN_OPTIONS_PAGE_CREATED, "Option.PageBasic", pPropPage);
			}
		}
		if (!m_pPropSave)//保存设置
		{
			pPropPage = nullptr;
			if (CreatePropPage(IDD_PROPPAGE_SAVE, pWnd) && pPropPage)
			{
				m_pPropSave = (CPropSave*)pPropPage;
				PlatformFramework::GetSingleton()->GetMessageBus()->SendMessage(MSG_PLUGIN_OPTIONS_PAGE_CREATED, "Option.PageSave", pPropPage);
			}
		}
		if (!m_pPropEffect)//效果设置
		{
			pPropPage = nullptr;
			if (CreatePropPage(IDD_PROPPAGE_EFFECT, pWnd) && pPropPage)
			{
				m_pPropEffect = (CPropEffect*)pPropPage;
				PlatformFramework::GetSingleton()->GetMessageBus()->SendMessage(MSG_PLUGIN_OPTIONS_PAGE_CREATED, "Option.PageEffect", pPropPage);
			}
		}
		if (!m_pPropDisplay)//显示设置
		{
			pPropPage = nullptr;
			if (CreatePropPage(IDD_PROPPAGE_DISPLAY, pWnd) && pPropPage)
			{
				m_pPropDisplay = (CPropDisplay*)pPropPage;
				PlatformFramework::GetSingleton()->GetMessageBus()->SendMessage(MSG_PLUGIN_OPTIONS_PAGE_CREATED, "Option.PageDisplay", pPropPage);
			}
		}
		if (!m_pPropContext)//环境设置
		{
			pPropPage = nullptr;
			if (CreatePropPage(IDD_PROPPAGE_CONTEXT, pWnd) && pPropPage)
			{
				m_pPropContext = (CPropContext*)pPropPage;
				PlatformFramework::GetSingleton()->GetMessageBus()->SendMessage(MSG_PLUGIN_OPTIONS_PAGE_CREATED, "Option.PageContext", pPropPage);
			}
		}
		if (!m_pPropCamera)//相机设置
		{
			pPropPage = nullptr;
			if (CreatePropPage(IDD_PROPPAGE_CAMERA, pWnd) && pPropPage)
			{
				m_pPropCamera = (CPropCamera*)pPropPage;
				PlatformFramework::GetSingleton()->GetMessageBus()->SendMessage(MSG_PLUGIN_OPTIONS_PAGE_CREATED, "Option.PageCamera", pPropPage);
			}
		}
		if (!m_pPropCache)//缓存设置
		{
			pPropPage = nullptr;
			if (CreatePropPage(IDD_PROPPAGE_CACHE, pWnd) && pPropPage)
			{
				m_pPropCache = (CPropCache*)pPropPage;
				PlatformFramework::GetSingleton()->GetMessageBus()->SendMessage(MSG_PLUGIN_OPTIONS_PAGE_CREATED, "Option.PageCache", pPropPage);
			}
		}
		if (!m_pPropRollBack)//回退设置
		{
			pPropPage = nullptr;
			if (CreatePropPage(IDD_PROPPAGE_ROLLBACK, pWnd) && pPropPage)
			{
				m_pPropRollBack = (CPropRollBack*)pPropPage;
				PlatformFramework::GetSingleton()->GetMessageBus()->SendMessage(MSG_PLUGIN_OPTIONS_PAGE_CREATED, "Option.PageUndo", pPropPage);
			}
		}
		if (!m_pPageScenes)//模拟设置
		{
			pPropPage = nullptr;
			if (CreatePropPage(IDD_PROPPAGE_SCENES, pWnd) && pPropPage)
			{
				m_pPageScenes = (CPageScenes*)pPropPage;
				PlatformFramework::GetSingleton()->GetMessageBus()->SendMessage(MSG_PLUGIN_OPTIONS_PAGE_CREATED, "Simulation.PagePosition", pPropPage);
			}
		}

		return false;
	}
	if (MSG_PLUGIN_OPTIONS_PAGE_STATE_CHANGED == msg.msgName)//子页面状态改变
	{
		if (msg.msgDatas.empty()) return false;
		CMFCPropertyPage* pProp = msg.GetDataObj<CMFCPropertyPage>(0);
		if (nullptr == pProp) return false;

		SetModify(pProp);
	}

	if (MSG_PLUGIN_OPTIONS_FRAMEWND_STATE_CHANGED == msg.msgName)//主窗体页面改变
	{
		const char* sAction = msg.GetDataObj<const char>(0);
		if (nullptr == sAction) return false;

		CMFCPropertyPage* pProp = msg.GetDataObj<CMFCPropertyPage>(1);
		if (nullptr == pProp) return false;

		if (0 == strcmp(sAction, "OnApply"))
		{
			pProp->OnApply();
		}
		else if (0 == strcmp(sAction, "OnCancel"))
		{
			pProp->OnCancel();
		}
		else if (0 == strcmp(sAction, "OnReset"))//OnReset
		{
			pProp->OnReset();
		}
		return false;
	}

	return true;
}

void COptionDlg::OnMove(int x, int y)
{
	CDialogEx::OnMove(x, y);

	// TODO: 在此处添加消息处理程序代码

	std::map<HTREEITEM, CMFCPropertyPage*>::iterator it;
	for (it = m_ptrMap.begin(); it != m_ptrMap.end(); it++)
	{
		CRect rec;
		(*it).second->GetClientRect(&rec);
		(*it).second->MoveWindow(x + 140, y + 25, rec.Width(), rec.Height());
	}
}


void COptionDlg::OnNMClickListtree(NMHDR* pNMHDR, LRESULT* pResult)
{
	// TODO: 在此添加控件通知处理程序代码
	CPoint lpt;//记录左键点击位置
	GetCursorPos(&lpt);
	m_tree.ScreenToClient(&lpt);
	UINT UFlags;
	HTREEITEM CurItem = m_tree.HitTest(lpt, &UFlags);
	ShowTreeItemRefPage(CurItem, 1);
	*pResult = 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值