所涉及的知识:
(1)MFC模块状态的切换
(2)在DLL中封装一个非模态对话框,可被任何WIN32程序调用
(3)非模态对话框的销毁 (4)怎样将层次特征的信息自私树状列表控件中显示
一.要做的一些准备
1.粗通一些MFC和DLL的知识。
得知道什么是窗口对象,什么是程序对象,什么是事件,什么是消息,什么是消息映射及怎样实现消息映射。对于DLL,知道怎样输出一些函数就差不多了,怎样在应用程序中加载一个DLL,并使用它们提供的接口。
2.控件通知消息(Notification message)
控件中发生了一些事件,是在控件的父窗口中响应事件,而不是在控件的窗口中响应。程序实现时,就相应的将这些事件的处理统统放在控件对象的父窗口对象的消息成员函数中。控件通知消息只适用于标准的窗口控件如按钮、列表框、组合框、编辑框,以及树状视图、列表视图等公共控件。例如,单击或双击一个控件、在控件中选择部分文本、操作控件的滚动条都会产生通知消息。
3.用到的控件是Tree Control
MSDN中的说法:MFC 提供了两个封装树控件 (Tree Control) 的类:CTreeCtrl 和CTreeView。
CTreeView的使用过于复杂。Tree Control则是它的简化版本,主要用做对话框上的控件。
4. 采用的DLL格式:
由于我们只是想使用DLL导出的对话框资源,而且还想着有可能在非MFC环境中使用该对话框资源,规则的MFC DLL可以胜任,就没必要使用MFC扩展DLL了,另外如果使用了扩展的MFC DLL,那么你的应用程序也必须使用MFC库。
5.开发工具
能不用VC6就不用,用VS.net吧,它更支持ANSI C/C++,功能也更强大
二.建立一个可以包含MFC对话框的DLL
树形控件仅能以子窗口的形式出现,它要依附对话框这样的父窗口。 可以在UG二次开发的DLL项目中直接添加对话框资源和对话框类来实现,手段虽不怎么高明,但能实现所需的功能。但我觉得最好将与UG没有多大关系的功能分离出来,作为一个模块单独实现,这也是软件工程所鼓励的方式。
本文的方法是首先做一个DLL,让包含一个MFC对话框,这个DLL可以在UG二次开发的项目中被加载。被包含在DLL中的对话框通常称为资源对话框。
下面为建立可以包含对话框资源的DLL的过程。
1.建一个MFC Regular DLL项目 . VS.net(没找到英文版,菜单只能给出中文名称)的建立一个DLL的过程如下:
[1]菜单 文件→新建→项目,在弹出的项目对话框的左栏,选择Visual C++项目,在右栏选择MFC DLL。然后在下面的文本框中输入项目的名字,确定,进入MFC DLL向导。[2]MFC DLL向导,在"应用程序设置"中,选择'使用共享MFC DLL的规则DLL",完成向导设置后,生成一个空的MFC DLL项目。
[3]菜单:项目→添加资源,在添加资源对话框中,选择"Dialog",然后点击按钮"新建"。VS.net会自动切换到资源视图界面,删去默认的"OK"和"CANCEL" 按钮。
[4]将默认的对话框ID修改成你能记得住的名字。
[5]添加对话框类CTreeDlg。
//-------------------- .
以上过程都是IDE在工作,下面就要动脑,动手了。
[6] 建立输出函数ShowTreeDlg
DLL是无法自动进入内存开始运行的,要被其它可执行文件的加载才可以。对话框是在DLL中创建的,我们期望在UG二次开发的项目中,在某个UIStyler控件的触发下弹出这个DLL中创建的对话框。而常规的MFC DLL是无法导出MFC对象给其它应用程序使用的,只能通过输出函数来做。
向工程中添加两个文件ExportFunc.h和ExportFunc.CPP,我打算将输出函数的声明和定义统一放在这两个文件中,便于管理。代码实现如下:
//--------------------------------------------
//ExportFunc.h,
//声明欲输出的函数
//-------------------------------------------
#ifndef _EXPORTFunc_H
#define _EXPORTFunc_H
#ifdef _cplusplus
extern "C"{
#endif
void ShowTreeDlg(HWND hMainWnd);
#ifdef _cplusplus
}
#endif
#endif
//--------------------------------------------
//ExportFunc.cpp文件
//定义输出函数
//-------------------------------------------
// 4 E- }- y* H6 w; H) M
//功能:DLL的输出函数,当其他应用程序加载该DLL后,调用这个函数,可以显示该DLL内建的对话框
//输入参数1:HWND hMainWnd,对话框父窗口的句柄
//
void ShowTreeDlg(HWND hMainWnd)
{
AFX_MANAGE_STATE(AfxGetStaticModuleState());
CTreeDlg *pTreeDlg=new CTreeDlg;
CWnd * pMainWnd=CWnd:: FromHandle(hMainWnd);
ASSERT(pMainWnd);
BOOL retValue=pTreeDlg->Create(IDD_TREE,pMainWnd);
if(!retValue)
{
AfxMessageBox("创建包含树列表控件的对话框失败了!");
}
ShowWindow(SW_SHOW);
}
下面是模块定义文件的内容,通常我们是使用_declspec(dllexport)直接修饰输出的函数,这样导出的接口很容易就被查看DLL文件的工具观察到,保密性不够好。为了向外界隐藏你的DLL对外接口的名称,只有def文件可以做。
; DlgWithTreeCtrl.def : 声明 DLL 的模块参数。
LIBRARY "DlgWithTreeCtrl"
EXPORTS
//此处可以是显式导出
ShowTreeDlg @1 NONAME
知识点1:MFC模块状态的切换
上贴,在ExportFunc.CPP中,定义输出函数ShowTreeDlg时,函数体开头一句是AFX_MANAGE_STATE(AfxGetStaticModuleState());
对MFC DLL不熟悉的兄弟在二次开发中使用MFC的库时,常常会丢了这句,以致出现一些莫名奇妙的错误。有的兄弟能记着添加它,但不见得就知道它的作用。不要求非要知道它的作用,但如果你会开车,又懂得修车,这样会更好。
[1]模块的定义:一段可执行的程序,其程序代码,数据,资源被加载到内存中了(这种加载可以是操作系统来做,也可以是已被加载到内存中的模块来做),这段程序进入内存后,系统会为之建立一个数据结构来管理,这个数据结构在WINDOWS中,就是PE文件头。一个活动在内存中的EXE文件是不是模块?一个活动在内存中的DLL文件是不是模块?它们都是。
[2]MFC模块:使用了MFC库的模块。
[3]MFC模块的状态:是一个数据结构,里面存了许多与模块相关的数据,这些数据具体是什么,我也不清楚,但我知道我的DLL中所包含的对话框是存于资源模板中的,而资源模板是存于这个MFC模块的状态数据结构中的。只关心这一点就可以了。
[4]一个MFC程序运行时,默认状态下,当它需要资源时,比如它需要一个对话框,那么它会从自身的模块状态中找到资源模板,然后从资源模板中找到相应的对话框,它是怎么找到自己所需要的对话框模板的呢?根据对话框ID,这是一个整型数。
然而,当一个MFC的应用程序在运行时,可能要加载多个MFC模块。譬如程序A.exe在运行时,加载了B.dll。在B.dll中提供了一个输出函数ShowDlg,函数体定义如下:
//-------------------------------------------------------------------------
//该函数显示一个非模态对话框,写法极简,仅作示例
//-------------------------------------------------------------------------
void ShowDlg(void)
{
CDlg *pDlg = new CDlg;
Create(IDD_DLG,NULL);
ShowWindow(SW_SHOW);
}
我们可以看到,B.dll中肯定是存在一个ID为IDD_DLG的对话框模板的,按照上面所说的,这个对话框模板被存到了B.dll的状态模块中了。当A.exe中调用这个输出函数时,运行到pDlg->gt;Create(IDD_DLG,NULL)时,它就会去自己的模块状态中去查找标号为IDD_DLG的对话框模板,结果有可能找得到,也有可能找不到。假如A.exe的模块状态中有一个对话框模板的ID恰好等于IDD_DLG时,就能找到,但弹出的对话框并不是你在B.dll中定义的那个。当A.exe的模块状态中没有ID恰好等于IDD_DLG的对话框时,那就是找不到,程序会报错。
[5]怎么解决
在[4]中,当程序A调用B.dll中的输出函数ShowDlg时,也就是说程序A进入了函数ShowDlg。倘若在ShowDlg的入口点处,把应用程序默认的自身模块状态切换成B.dll的模块状态,这样再查找标号为IDD_DLG的对话框时,就会从B.dll的模块状态中查找了,结果就能找到。这个切换语句就是在ShowDlg函数体的第一句处添加:AFX_MANAGE_STATE(AfxGetStaticModuleState());
三.测试这个包含对话框资源的DLL
由于这个Dll是采用MFC Regular DLL的格式写的,MSDN中说它可以被任何WIN32程序调用,要测试它,简单起见,只需写个Console程序来测即可。
任务:测试DlgWithTreeCtrl.dll
所需的文件:ExportFunc.h,DlgWithTreeCtrl.lib,DlgWithTreeCtrl.dll。 9 W3 E" m# K( W& W
常规的测试步骤
[1]菜单 文件→新建→项目,在弹出的项目对话框的左栏,选择Visual C++项目,在右栏选择Win32控制台项目。然后在下面的文本框中输入项目的名字testDll,确定,进入Win32控制台程序向导。
[2]Win32控制台程序向导中,在"应用程序设置"中,选择'控制台应用程序",没有必要让你的控制台程序支持MFC,其它都采用默值,完成向导设置。
[3]如果此时testDll项目是debug模式的,将DlgWithTreeCtrl.lib和DlgWithTreeCtrl.dll拷贝到testDll目录中的debug文件夹下。
[4]testDll的主程序文件testDll.cpp中内容如下:
//---------------------------
//testDll.cpp
//--------------------------
#include "stdafx.h"
#include "../DlgWithTreeCtrl/ExportFunc.h"
#pragma comment(lib, "..//DlgWithTreeCtrl//release//DlgWithTreeCtrl.lib") ;
void main(void)
{
ShowTreeDlg(NULL);
}
[5]编译连接,运行。运行结果是一个控制台窗口出现,然后一个对话框一闪就不见了。呵呵,对话框确实被显示了,只是控制台程序瞬间就结束了,所以对话框就被自动销毁了。可以在main函数中加上一句:
MessageBox(NULL,"stop",NULL,MB_OK);
让控制台程序不那么快就消亡。
四. 还需要继续考虑的问题
我在DlgWithTreeCtrl.dll中做的那个输出函数,new了一个对话框的对象,程序中从始至终都没有释放所new的(堆)空间。会不会内存泄露?这不是一个小问题。换个说法:MFC的窗口对象是怎样从系统中清除的呢?要弄清楚这个问题,必须先搞清楚窗口对象的成分。
一个MFC窗口对象包括两方面的内容:一是窗口对象封装的窗口,即存放在m_hWnd成员中的HWND(窗口句柄),二是窗口对象本身是一个C++对象。要删除一个MFC窗口对象,应该先删除窗口对象封装的窗口,然后删除窗口对象本身。也就是窗口的C++对象的生存期要比窗口长。
[1]窗口的删除
删除窗口最直接方法是调用CWnd:: DestroyWindow或全局API函数:: DestroyWindow,前者封装了后者的功能。前者不仅会调用后者,而且会使成员m_hWnd保存的HWND无效(NULL)。如果DestroyWindow删除的是一个父窗口或拥有者窗口,则该函数会先自动删除所有的子窗口或被拥有者,然后再删除父窗口或拥有者。在一般情况下,在程序中不必直接调用DestroyWindow来删除窗口,因为MFC会自动调用DestroyWindow来删除窗口。例如,当用户退出应用程序时,会产生WM_CLOSE消息,该消息会导致MFC自动调用CWnd:: DestroyWindow来删除主框架窗口。当用户在对话框内按了OK或Cancel按钮时,可调用CWnd:: DestroyWindow来删除对话框及其控件。
[2]窗口对象的删除
窗口对象本身的删除则根据对象创建方式的不同,分为两种情况。在MFC编程中,会使用大量的窗口对象,有些窗口对象以变量的形式嵌入在别的对象内或以局部变量的形式创建在堆栈上,有些则用new操作符创建在堆中。
对于一个以变量形式创建的窗口对象,程序员不必关心它的删除问题,因为该对象的生命期总是有限的,若该对象是某个对象的成员变量,它会随着父对象的消失而消失,若该对象是一个局部变量,那么它会在函数返回时被清除。
对于一个在堆中动态创建的窗口对象,其生命期却是任意长的。因为用new在堆中创建对象,就不能忘记用delete删除对象。有些MFC窗口对象具有自动清除的功能。不需要程序员显示的调用delete来删除它。
[3]对于我new的那个非模态的对话框,该如何销毁呢?
这个问题缠绕我好几天,至今为止还是没有解决的。经测试,发现上面来自网上的和来自MSDN的资料说的不正确。
例如这句话:"用户退出应用程序时,会产生WM_CLOSE消息,该消息会导致MFC自动调用CWnd:: DestroyWindow来删除主框架窗口。"
经过测试,当我点了对话框窗口的系统菜单上的"关闭"按钮后,产生WM_CLOSE消息,而它并没有调用CWnd:: DestroyWindow来销毁窗口!!!!!!
五. 开始制作带有树形列表控件的对话框了
插语:回头看了看那么长的篇幅,许多东西想整的再细致一点,实在有心无力了.也许用MFC做界面真的太繁琐了。这可能也是许多人更热衷于学VB,DELPHI,而逃避VC的原因吧。总之Windows给了人们一个直观的界面,也浪费了无数程序员大好的青春,他们本来可以做更多有用的事,仅因为程序的门脸而废脑筋。
前面的问题都解决了,下面就一马平川了。打开前面完成的DlgWithTreeCtrl项目。 [1]切换到资源视图,从工具箱中拖一个tree control到对话框面板上
[2]试着随便修改这个tree control的属性,试了一遍就基本知道一个tree control有哪些风格。
[3]在原对话框类CTreeDlg中添加一个控件变量m_treeCtrl,使用添加变量向导进行添加,这样IDE会自动在类CTreeDlg的实现代码中添加DDX语句,从而将控件对象与控件连接起来。
[4]剩下的工作就是如何操纵这个tree control了。
假如我们在UG下开发一套标准件库,各零件模型都可以产生了,也就是一大堆DLL,每个DLL产生一种或几种零件。我们需要一个窗体专门用来显示整个标准件库所有零件的信息,当在列表中选中一种零件时,就可以在UG中自动创建它。这样,采用树形列表是最为直观的了。
六.与树形列表相对应的数据结构的设计
树形列表中的条目是由根,分枝,叶构成的。叶结点中存储零件信息,我们应该怎样将信息写到树形列表中对应的条目里,这是使用树形控件首先要考虑的问题,不然只有一个空壳界面是没什么意义的。
我想做一个数据结构,用它来存储根,分枝和叶的信息,我们自然会想到它应该是一棵树,可以采用C++编写这样一个树的类,称之为信息树。如果我们在对话框类添加一个指向这样一个信息树对象的指针,那么就可以通过深度优先遍历实现树状信息在树形列表中的显示了。
[1]信息树节点的设计
//------------------------------
//信息树的节点
//------------------------------
typedef struct INFO_NODE_s
{
int state; //
char name[_STR_LENGTH]; //节点对应条目的名称
void *pObj; //指向要在树列表控件中显示其信息的对象
void *pOtherObj ; //备用指针
//每个节点都有一个ID链表,来表示它在树中的位置
//根结点第一个子节点就是链表的第一个单元
CListTemplate<int> *pPath;
//指向存储子节点的链表对象的指针
INFO_NODE_s * pParent;
CListTemplate<INFO_NODE_s *> *pSubNodeList;
INFO_NODE_s,*INFO_NODE_p_s;
[2]信息树的类声明
///
//信息树类
//
class CInfoTree (
{
protected:
INFO_NODE_p_s m_pRoot; //根节点
public:
CInfoTree(void);
~CInfoTree(void);
//-----------------------------------------------
//接口
//-----------------------------------------------
public:
INFO_NODE_p_s GetRoot(); //取指向根节点的指针
static INFO_NODE_p_s MallocNode(); //分配一个新的节点
//用户输入结点名称,ID字符串,指向用户对象的指针来分配新结点
static INFO_NODE_p_s MallocNode(char *name,char *nodeIdStr,void *pObj,void *pOtherObj);
static void FreeNode(INFO_NODE_p_s p); //销毁一个节点
INFO_NODE_p_s Insert(INFO_NODE_p_s p); //向树中插入节点
INFO_NODE_p_s Delete(INFO_NODE_p_s p); //从树中删除节点
};
七 利用树形列表控件来显示信息树
[1]深度优先遍历树
为了在控件中显示树的信息,要写一个递归函数来遍历信息树
//-------------------------------------------------------------
// 功能:遍历信息树,在树列表控件中显示信息
//-------------------------------------------------------------
void CTreeDlg::FSTraverInfoTree(INFO_NODE_p_s p,HTREEITEM hItem)
{
HTREEITEM h;
if(p==NULL) 2
{
return;
} //向对话框中的控件对象m_treeCtrl中添加各结点信息
h = m_treeCtrl.InsertItem(p->name,0,0,hItem);
INFO_NODE_p_s tempPtr;
NODE_POSITION(INFO_NODE_p_s) iteratorPtr=p->pSubNodeList->GetHeadPosition();
while(iteratorPtr != NULL)
{
tempPtr = p->pSubNodeList->GetNext([iteratorPtr)] )
DFSTraverInfoTree(tempPtr,h);
}
}
[2]修改原来的DLL
由调用该DLL的应用程序传入信息树对象,在对话框的初始化函数中,将信息树对象中包含的信息在树状列表控件中显示出来。
为了得到应用程序传入的信息树对象,需要在DLL中的对话框类中添加一个信息树的成员变量,再将输出函数修改为:
//
//功能:DLL的输出函数,当其他应用程序加载该DLL后,调用这个函数,可以显示该DLL内建的对话框
//输入参数1:HWND hMainWnd,对话框父窗口的句柄
//输入参数2:指向信息树对象的指针,信息树对象中封装了要在控件中显示的信息
//
void ShowTreeDlg(HWND hMainWnd,CInfoTree *pInfoTree)
{
AFX_MANAGE_STATE(AfxGetStaticModuleState());
CTreeDlg *pTreeDlg=new CTreeDlg;
pTreeDlg->m_pInfoTree = pInfoTree;
CWnd * pMainWnd=CWnd:: FromHandle(hMainWnd);
ASSERT(pMainWnd);
BOOL retValue=pTreeDlg->Create(IDD_TREE_DLG, NULL);
if(!retValue)
{
AfxMessageBox("创建包含树列表控件的对话框失败了!");
}
pTreeDlg->ShowWindow(SW_SHOW);
}
[3]在列表中显示树的信息
BOOL CTreeDlg:: OnInitDialog()
{
CDialog:: OnInitDialog();
INFO_NODE_p_s p = m_pInfoTree->GetRoot();
DFSTraverInfoTree(p,NULL);
return TRUE;
}
八。程序运行结果 pInfoTree->Insert(p); ShowTreeDlg(NULL,pInfoTree);
|