仅使用 C++ 类编写 Win32 应用程序(第三部分)






4.76/5 (12投票s)
C++ 类和包装器,用于在不使用MFC、ATL或其他库的情况下编写W32应用程序(第三部分)。
引言
好了,我又回来了,继续讲述这个故事的另一部分。
对于感兴趣的GUI(呃……各位),之前的部分在这里:
既然我们又回到了起点……让我坦白,这最初并非打算作为主文章的第三部分。为什么?因为在开发停靠框架时,我得出的结论是——代码已达到一定的成熟度——有必要进行一次全面修订。
首先,修复一些bug和设计缺陷;其次,提高代码的可用性和灵活性。因此,我开始了“第2.5部分”,原打算将下一部分命名为“第33.3部分”。但有人已经这样做了,所以我又回到了传统的编号方式。
开始之前
本文探讨了代码从先前版本演进到新实现的原因。尽管总体思路相同,但存在一些实质性差异。如果您对当前实现感兴趣,只需下载本文提供的代码。如果您对整体讨论感兴趣,也请下载上一篇文章的代码:它已经过时,但它是进行比较的基础。
本文假定您已熟悉第二部分中讨论的内容。
已修复的Bug
以下是先前版本中一些已修复的Bug。
“诺曼·贝茨崩溃”
正如之前文章留言板中已提及,我非常感谢诺曼·贝茨,他发现了一个bug——经过分析,我发现这是一个“设计缺陷”。
WrpBase::~WrpBase
最初调用 pThis->Detach()
。这有两个错误原因:
pThis()
将WrpBase
转换为派生W
类型,但在WrpBase
析构函数内部...W
已经销毁了!Detach
调用引用计数器对象的Release
函数,该函数可能会回调W
对象(仍然已被销毁)。
似乎没有办法摆脱这种情况:包装器的自动分离是包装器的优势,但是...这很棘手。
解决方案是...从每个 WrpBase
派生类的析构函数中调用 Detach()
。为了避免遗漏,我在 WrpBase
析构函数中放置了一个 ASSERT
:此时不应再有 _pRC
存在!
这相当于在MFC CWnd
派生析构函数中调用“DestroyWindow
”。
其他讨厌的bug隐藏在 Detach
和 XRefCount_base
中,与类型安全有关。但由于我进行了重构,它们已通过新设计完全消除。不过,让我先说完这些bug。
SShare<T> 和 SString
实际上 SString
是可以工作的,但我它的父类中存在一个缺陷。
SShare::GetBuffer
是通过 _OwnBuffer
实现的,但 _OwnBuffer
错误地通过 lstrcpyn
实现。这使得 SShare
仅适用于 TCHAR
。现在,该实现已使用 XBuffData::copyfrom
重写,它使用“=
”复制数据并使用“==
”进行比较。现在它适用于每个可赋值、可比较相等并具有“空值”的类。
当然,SString
仍然是 SShare<TCHAR, _T('\0')>
。但现在,如果合理的话,你甚至可以有 SShare<double, 0.>
或 SSHrae<SSomeStruct, SSomestruct()>
!
拆分库
好了,解决了烦人的bug,我们继续前进。
在添加功能时,我看到解决方案资源管理器并打开 NWin
命名空间上的类视图时,产生了一种不好的预感:它们有变成“怪物”的风险。
因此,我决定通过拆分库来引入更模块化的概念:NLib将只作为“核心库”。所有“功能”将放入独立的库中,让核心库可以进行不同的改进。
NLIB 核心库
那么:哪些模块将被指定为“核心”的一部分呢?
在 stdafx.h 中 |
|
GE_INLINE 和 GE_ONCE 定义 |
用于获取既可以作为库编译也可以作为“内联”编译的代码。 |
全局符号 | interface 关键字定义(应该来自 windows.h,但是...),crtdbg.h,ASSERT 和 VERIFY 定义 |
STL集合和算法 |
exception, functional, utility, vector, list, set, map, deque, stack, queue, bitset。 |
Windows头文件和常用头文件 | windows.h, commctrl.h, olectrl.h, tchar.h |
Nlib “核心”头文件 | Wrp.h, Wnd.h, WinString.h, MessageMap.h, ...以及它们加载的头文件: Misc.h, Coords.h (和 windowlongptr.h) 和 Evt.h |
stdafx.h 之外 |
|
GDI 包装器 | GdiWrp.cpp 和 GdiWrp.h |
消息循环相关 | Msgloop.h 和 Msgloop.cpp |
资源加载器 | ResWrp.h |
文件包装器和序列化 | 尚未部署在此库中(即将推出)。 |
NLIB 中的所有模块将来只会更改它们的接口以添加新函数或成员。不再应修改任何现有的函数原型。
NGUI 库
将包含所有使用NLib实现GUI及其他功能(如自绘菜单、停靠等)的模块。
它实际包含 CmdUpdt.cpp、CmdImgs.cpp 以及要在应用程序资源脚本中包含的关联资源脚本 (Ngui.rc 和 NGui_res.h),以及此阶段的所有部署。
资源脚本的编号约定
当使用不同的模块时,必须就资源文件中ID的使用建立一个通用约定。
考虑到 Windows 对消息码、命令码等的用法,最好避免使用低于 0x2000 (WM_USER
+ OCM__BASE
) 的 ID,这可以四舍五入到十进制的 8200。
因此,NLIB_FIRST
是 8200,NGUI_FIRST
是 8300。这些数字适用于控件 ID。
对于命令,最好保留在 WORD
的较高部分(从 32768 开始向上),以避免将命令与控件通知混淆。因此,命令可以采用与之前相同的编号约定,但增加一个十进制的 40000 偏移量。
因此,83xx 将是 NGUI 资源,483xx 将是 NGUI 命令。
请注意,32768 (0x8000) 也是 EMenuUpdator
用于决定是否发送其查询消息的默认值。值小于 32768 的命令将不会自动更新。
重新设计的功能
最重大的重新设计是关于 Wrp.h。是的,核心已被重新设计以实现更好的性能和操作。
包装器映射重新设计
在之前的版本中,所有包装器都继承一个将包装类型转换为 LPVOID
的 traits 函数。此值用作静态全局反向映射(XMap
)中的键,允许找到与包装值(通常是 Windows 句柄或指针)关联的 XRefCount_base
。
这样做有两个主要缺点。
首先:考虑一个拥有数百个窗口的应用程序,以及一个由智能指针(它们是指针的包装器!)支持的数千个多态对象集合组成的“文档”(应用程序数据)。结果是,反向映射变得庞大(因此很慢),并且访问频率非常高(几乎每次Windows发送消息时)。
其次:考虑一个对象,它被不同类型的包装器使用不同的引用计数类型进行包装(例如,存储不同类型的信息)。如果该对象的同一实例被多个不同类型的包装器包装,因为只能存在一个引用计数,某些包装器将无法找到所需的数据。这是一个问题,因为没有“类型安全”。
映射特化
这可以通过特化映射来避免。
为此
- “
types_wrp_xxx
”struct
定义了一个新的typedef
,名为TKey
,其值比LPVOID
更不通用。这允许有针对不同types_wrp
的不同EMap
。TKey
通常是类型H
的别名,但在特定情况下可能会有所不同。 traits_wrp_xxx::Key
函数现在返回TT::TKey
。WrpBase
中的Attach
和Detach
函数已根据新类型进行了修改:使用TT::TKey
代替LPVOID
。
特别是
types_wrp_hnd<H> |
typedef typename H TKey; |
types_wrp_ptr<H> |
typedef typename LPVOID TKey; |
types_wrp_rsrc<Data> |
typedef typename HRSRC TKey; |
由于这些定义,句柄每种类型有一个映射(每个 H
有一个),而指针有一个单一的全局映射(LPVOID
)。这是为了让多态性工作(不同类型的指针可能指向同一复杂对象的不同组件——基类:它必须有一个单一的标识)。
另一个必须解决的问题是,这些映射,由于是在静态函数内部声明的,所以它们在第一次需要时才创建。但是,如果需要它们的是一个全局包装器、指针或事件,这将导致它们的构造发生在全局“需求者”的构造函数之后。结果,它们的销毁将发生在之前,导致内存损坏。
为了避免这种情况,它们的销毁必须尽可能延迟。这通过在堆上创建映射,并让它们将自己链接到一个全局列表来实现,该列表被全局实例化并尽快创建,并在删除时销毁其内容。
这就是 EMap_base
的作用,以及 EMap<>
和 EMap<>::TMap
以及隐藏的 XInit
。
此外,在调试模式下,我添加了一些关于映射的统计功能,这些功能会在程序终止时输出。
PtrCreator
这是一个 NWrp::Ptr
,在 NULL
非 const 解引用情况下不会失败。在这种情况下,它会调用一个“自动创建函数”,作为模板参数传递(如果为 NULL
,则假定一个执行“new T
”的静态函数)。SetAutoCreateFn
可以在运行时调用,以更改创建函数(例如,传递一个为 T
的派生类型执行“new
”的函数)。
因为这种功能通常在需要动态和多态类型时才需要,所以它仅以 NWrp::PtrCreator<class, creatorfunction>::Dynamic
的形式存在。
类型安全
为了更好地保证类型安全,包装器、引用计数器和映射之间的关系已得到审查。
XRefCount
现在通过所有引用计数器继承的公共基类实现,并一致地命名为 SRefCount
。但“所有者计数器”(即定义包装对象生命周期的计数器)不再位于 SRefCount
本身中,而是从静态映射中引用,该映射使用与 SRefCount
-er 对应的包装器的相同 TKey
。
这允许不同类的引用计数器在与同一键控对象关联的同一值上进行共同计数,即使维护不同的全局映射。
XRefChain
(现在名为 SRefChain<W>
) 派生自 XRefChain_base<W,D>
,而后者又派生自 XRefCount_base<D>
。
W
是引用计数器或链所针对的包装器,D
是引用计数器本身的最终派生类(W
期望的 TRefCount
)。
WrpBase<H,W,t_trais,t_refCount>
现在默认使用 SRefCount
struct
作为 t_refCount
。
可链式包装器从 WrpBase
(以前它们是同一个东西)派生为 WrpChainBase<H,W,t_traits,t_refCount>
,并具有一些额外的函数来获取包装器链上的迭代器。它们期望具有 SRefChain
或派生的引用计数器和链器(t_refCount
默认为 SRefChain<W>
)。
由于引用计数和包装器现在知道它们各自的类,所有函数和回调现在都是类型安全的(不再需要类型转换)。
拥有的智能指针现在可以作为 Ptr<Type>::Static
或 Ptr<Type>::Dynamic
获取(前者使用 static_cast
转换类型,后者使用 dynamic_cast
)。观察指针也是 Qtr<Type>::Static
和 ::Dynamic
。请注意,每个包装器都可以通过调用 SetOwnership
成员函数从观察者更改为所有者。Ptr
和 Qtr
只是具有不同默认行为的快捷方式,但在其功能上基本等效。
EAutodeleteFlag
这个类最初是布尔标志和检索它的函数的提供者。它用于 EWnd
,其中“自动删除”是通过“delete this
”实现的,该“this”指的是一个看到 WM_NCDESTROY
(窗口在其生命周期中看到的最后一条消息)的包装器。这对于存在于堆上并由它们包装的窗口拥有的窗口包装器来说是很好的。
但这里有一个潜在问题:假设你的程序实例化了一个无模式、无主人的工具窗口:它是一个弹出窗口,所以它不是主窗口的子窗口。现在,假设你的主窗口正在关闭。所有子窗口都被销毁,并发送 WM_QUIT
(如果窗口周围有一个 EWnd
包装器,这是自动的)。所有消息处理完毕后,消息循环退出。但弹出窗口仍然存在:没有人移除它。
对于操作系统来说,这不是问题(它会在 WinMain
返回后处理),但没有消息分发仍在进行。因此,没有 WM_NCDESTROY
被在程序终止后仍然存在的弹出窗口包装器处理(实际上,这是一个内存泄漏!)。
为避免这种情况,现在 EAutodeleteFlag
在设置为“开启”时会链式连接到静态列表,并在设置为“关闭”时移除。列表销毁(在程序终止时)会删除所有仍在位置上的对象。
诀窍在这里
class EAutodeleteFlag { protected: bool _bHasAutodelete; struct XChain: public std::list<EAutodeleteFlag*> { ~XChain() { ... } }; static XChain& AutoDeleteChain() { static XChain c; return c; } public: EAutodeleteFlag() { _bHasAutodelete = false; } virtual ~EAutodeleteFlag() { AutoDeleteChain().remove(this); } bool HasAutodelete() const { return _bHasAutodelete; } void Autodelete(bool bOn) { if(bOn && !_bHasAutodelete) AutoDeleteChain().push_back(this); if(!bOn && _bHasAutodelete) AutoDeleteChain().remove(this); _bHasAutodelete = bOn; } };
SRange<I> 和 SLimit
刚刚添加了 bool IsEmpty()
和 bool IsUnit()
,含义很明显。此外,当其中一个操作数是类型 I
(模板参数)时,compare
现在被别名为 operator&
。
相反,SLimit
是一个空类,所有静态模板成员函数(只是为了将我不想“全局化”的东西分组)执行一些频繁的“比较和赋值”操作,例如“让一个值不超过给定最大值”等。
struct SLimit { template<class A> static A Min(const A& left, const A& right) {return (left<right)? left:right; } template<class A> static A Max(const A& left, const A& right) {return (right<left)? left:right; } template<class A> static A& OrMin(A& ref, const A& val) { if(val<ref) ref=val; return ref; } template<class A> static A& OrMax(A& ref, const A& val) { if(ref<val) ref=val; return ref; } template<class A> static A OrRange(A& rmin, A& rmax, const A& val) { OrMin(rmin, val); OrMax(rmax, val); return val; } template<class A> static A& AndRange(A& ref, const A& min, const A& max) { OrMax(ref, min); OrMin(ref, max); return ref; } };
消息映射处理程序
为了避免ATL、WTL、MFC和我的宏之间不当的混用,我决定给它们都加上GE_
前缀。当然,所有命名空间也以GE_
开头,所以如果这些首字母不适合你……全局查找并替换所有文件,搞定!没有与同名宏(做几乎相同的事情,但不一定完全相同)不当混淆的风险。尤其是在混合环境项目中。
它们都在 MessageMap.h 中,我在那里还添加了一些专门处理 WM_PARENTNOTIFY
变体的宏。
命令转发/反射和自动更新
在上一篇文章中,我介绍了一种管理命令更新的方法,基于 NWin::ICmdState::SendQueryNoHandler
和 NWin::ICmdState::SendQueryUpdate
。
这些函数发送两个私有消息 GE_QUERYCOMMANDHANDLER
和 GE_UPDATECOMMANDUI
。现在,由于这些消息与命令相关,因此在进行通知转发或反射时,使用与命令相同的逻辑来处理它们是正确的。
为了避免每次需要新通知时都修改 EWnd
的行为,我决定重新实现此功能(并实现基于通知消息的未来功能),不再使用 WM_USER+xxx
消息,而是使用新的私有 WM_NOTIFY
(以便可以转发或反射)。
顺便说一下,我使用了 SNmHdr
(参见下文)来注册通知代码。
ICmdState
已移至 NGDI,并简化为处理命令状态(启用、灰色、选中和文本)。
图像从位图加载并以各种效果存储在 SCmdImgs
中,并且已定义了一个新的接口 ICmdImage
用于处理图像与命令的关联设置和检索。
这些接口与派生自接口本身和 NUtil::SNmHdr<>
(见下文)的抽象结构相关联。这使我们能够发送携带这些接口的通知消息(这些 struct
是 SCmdStateNotify
和 SCmdImageNotify
)。
通过派生这些接口,可以为各种接口(菜单、工具栏、状态栏或其他)专门实现虚函数。
这在支持 EMenuUpdator
和 EMenuImager
时完成,它们现在发送这些消息。
用于处理命令的宏已根据新实现(GE_COMMAND_xx_HANDLER_U
系列)进行了修改,而 GE_UPDATECOMMANDUI
系列宏已删除。命令更新可以使用新的 GE_NOTIFY_xx_REGISTREDHANDLER(..., func, type)
钩住,其中 type
是所需的 SCmdXxxxNotify
。
EMenuImager, SCmdImages
这些类已重新安排,以使绘图可自定义。
特别是,SCmdImgs
现在是抽象的,SCmdImgsIDE
通过处理和绘制命令图像来实现它。您现在可以自己实现其他 SYourCmdImgs
,以不同的方式处理和绘制这些图像。
SCmdDraw
使用了一个新的 NWrp::PtrCreator
智能指针。这个指针旨在永不解引用失败,通过调用(如果为 NULL
)一个给定的“创建”函数来实现。在 SCmdDraw
的情况下,我们有 typedef PtrCreator <SCmdImgs, SCmdImgsIDE::New> PCmdImgs
,其中“New
”是一个返回 new SCmdImgsIDE
的静态函数。
PCmdImgs
“创建体”可以通过 static SCmdImgs& GetCmdImgs()
函数检索。另一个函数(SetCmdImgsType(SCmdImgs* (*pfn)()
)清除 PCmdImgs
并将其创建函数设置为传递的值。下次解引用指针时,将创建一个新的 SCmdImgs
派生类。
结果是,只有一个 SCmdImgs
存在,可以通过 SCmdDraw::GetCmdImgs()
检索,但其类型可以在运行时设置。也就是说:如果您部署了许多“绘图器”,您还可以设计一个接口,让用户选择他们喜欢的绘图器。
新增功能
NOTIFY_xxx_HANDLER 宏
它们是类似于 ATL 消息映射的宏,用于分派 WM_NOTIFY
消息。它们已得到轻松改进,可以接受一个额外的“type
”参数。
新形式现在是 GE_NOTIFY_xxx_TYPEDHANDLER( ... , func, type)
。(注意:根据特定宏的不同,“...”可以是 code
、id
、range
的 ID,或它们的组合)。这允许在宏中指定将转换到的类型,即 LPARAM
消息参数携带的 LPNMHDR
。
这允许您声明消息处理程序,将其参数直接设置为所需结构的引用(例如,NMLISTVIEW&
),而不是在函数体中进行类型转换的 LPNMHDR
。
类型化通知
为了让 Windows 能够相互发送事件信号,Windows 提供了一个基于消息 (MSG
) 的消息分发架构,以及一些用于发送 (SendMessage
)、发布 (PostMessage
)、检索 (GetMessage
, PeekMessage
) 和分发 (DispatchMessage
, WINDOWPROC
) 的 API。在这个框架中,WINDOWPROC
总是内部子类化窗口过程,分发通过消息映射完成。发送消息则需要更多关注。
如果发送已经定义的消息,我们可以简单地调用 SendMessage
API,传递所需的参数。如果发送其他类型的消息,我们至少需要定义一种识别它们的方式。这可以通过定义一些清单常量来实现,例如 #define WM_MYMESSAGE (WM_USER+xxx)
,但是,想象一个由各种库模块和不同组件(可能来自不同的开发人员)组成的源代码。需要一个非常严格的编号约定(以避免在不同源代码中重复使用相同的 ID),或者某种能够自动化此过程的东西。
NUtil::XId_base
提供一个静态函数(UINT NewVal()
),它在每次调用时返回一个递增的静态计数器的值。
NUtil::SId<T>
提供一个 UINT _getval()
函数,该函数在首次调用 _getval
时返回一个静态变量的值,该变量已初始化为 XId_base::NewVal()
。它还具有一个返回该值的 operator UINT()
。这允许我们将任意数量的 UINT
与任意数量的 T
类型关联起来,以便与 SId
一起使用。
SNmHdr<N>
是一个 struct
,其第一个成员是 NMHDR
,并将“code
”成员初始化为 (UINT)SId<N>()
。Windows 习惯于在“commoncontrol”库中以 (OU - xxxU)
的形式定义 WM_NOTIFY
代码(因此:从 0xFFFFFFFF 到大约 3000 个代码)。由于我让 XId_base
从 0x2000 开始递增……有很多 ID 可以使用。
SNmHdr<N>
还有一个 LRESULT Send(HWND hTo)
函数,它执行 SendMessage(hTo, WM_NOTIFY, nmhdr.idFrom, (LPARAM)this);
。因此,我们可以从 SNnHdr<SMyNotification>
派生一个 struct
(例如 SMyNotification
),根据需要填充其成员,然后调用 Send
。
为了检索这样的消息,我提供了一些消息映射宏(GE_NOTIFY_REGISTREDHANDLER
、GE_NOTIFY_CODE_REGISTREDHANDLER
、GE_NOTIFY_RANGE_CODE_REGISTREDHANDLER
),它们接受一个“type
”参数,检查 uMsg == WM_NOTIFY
和 GE_::NUtil::SId<type><TYPE>() == ((LPNMHDR)lParam)->code)
,然后调用一个函数,其形式为 lResult = func((int)wParam, *(type*)lParam, bHandled)
。
因此,我们可以将一个 GE_NOTIFY_CODE_REGISTREDHANDLER(OnMyHandler, SMyNotification)
条目放置在窗口的消息映射中,以调用成员函数 LRESULT OnMyHandler(int nID, SMyNotification& myntf, bool& bHandled)
。
命令路由
想象一个框架窗口,其中包含一个子视图。菜单和工具栏通常属于框架,并向框架发送 WM_COMMAND
和 WM_NOTIFY
。
但是,您可能对从子视图处理这些命令感兴趣。您可以通过将框架的消息映射链到视图的备用消息映射来实现(但这意味所有消息都将传输)。或者,您只转发 WM_COMMAND
或 WM_NOTIFY
。
这就是 GE_ROUTE_MSG_MAP_xxxx
宏的作用。您可以路由到一个类、一个成员,或通过一个指针(会进行 NULL
指针检查)。
命令和通知有两组不同的宏。但请记住,如果您使用自动更新命令(GE_COMMAND_ID_HANDLER_U
),那么自动更新本身就是一个 WM_NOTIFY
消息。因此,如果您路由命令...也以相同的方式路由 WM_NOTIFY
。或者使用同时调用这两个系列的宏。
另请注意,“ROUTE”宏调用 some::ProcessWindowMessage
。这与“发送”消息不同:如果一个窗口有多个包装器,调用 SendMessage
会使所有包装器都能在其默认消息映射中接收消息,而“ROUTE”只会使传递的包装器处理路由的消息或命令。
如果你想将命令重新发送给一个给定的窗口,而不是“路由”给一个给定的包装器,请使用 GE_FORWARD_COMMANDS
。它会重新发送 WM_COMMAND
和 WM_NOTIFY
。
转发消息
告知一个窗口处理最初发送给另一个窗口的消息(而不与为该窗口准备的消息混淆)的另一种方法是“通过封装转发”。原始消息被重新发送到另一个窗口的另一条消息中。这个技巧来自ATL (ATL_FORWRARD_MESSAGE
),但在这里,我对其进行了泛化。
GE_FORWARD_MESSAGE(hWndTo, code)
发送一个 WM_GE_FORWARDMSG
,其参数是一个代码 ID(作为 WPARAM
)和一个 NWin::XWndProcParams*
(作为 LPARAM
:它携带原始消息参数)。
您可以在目标 HWND
包装器消息映射中使用 GE_WM_FORWARDMSG(func)
处理它,其中“func
”是 LRESULT func (NWin::XWndProcParams& msg, DWORD nCode, bool& bHandled);
。
或者... 您可以通过在从 XWndProcParams
提取参数后递归调用 ProcessWindowMessage
来“解封装”原始消息。
这可以通过以下方式完成
GE_WM_FORWARDMSG_ALT(msgMapID)
:解封装原始消息,并将其提供给同一消息映射中的另一个 ALT_MSG_MAP
部分。这使您能够使用 GE_WM_xxx
处理器处理消息。
GE_WM_FORWARDMSG_ALT_CODE(code, msgMapID)
:与之前相同,但只处理在重新发送时被“code”标记的封装消息。
消息映射链
GE_CHAIN_MSG_MAP
系列宏已进行调整,以保持一致的宏数量:您可以链式连接默认映射或特定的“备用映射”(xxx_ALT(..., msgMapID)
:与 ATL 概念相同)。
并且你可以链接到
- 一个类:用于派生类引用其基类。
- 一个成员:用于在其内部作为成员托管其他类的类。
- 一个指针:用于更复杂的结构,其中存在各种引用。在调用指向的
ProcessWindowMessage
之前会检查空指针。
消息处理注意事项
结合所有这些消息路由技术,现在几乎可以做任何事情。考虑到每个类都可以是 NWin::IMessageMap
派生的(不需要它本身是窗口包装器),消息映射可以是一种有用的方式,让类进行通信,而无需以静态方式纠缠自己(被设计成了解它们的相互接口)。
如果需要更强的动态性,正确的解决方案可能是使用“事件”(NWrp::Event<A>
和 NWrp::EventRcv<D>
:参见第一部分以获取描述)。
请注意两种方法的主要区别:消息映射是提供代码片段的宏。事件是数据结构。消息映射链在编译时定义(翻译宏时)。事件分发是一个完全的运行时机制。
当然,通过消息映射定义类之间的通信在技术上是可行的,也将Windows消息转换为事件也是可行的。然而,我不认为我支持事件的方式可以原样用于消息:链式消息映射允许许多消息从一个映射传递到另一个映射。目前,事件是一对一的:接收者必须单独注册它想要接收的所有事件。
查找给定类型的包装器(RTTI)
考虑一个窗口可以有许多附加的包装器。您可能对寻找给定类型(或从给定类型继承)的包装器感兴趣。由于 HWND
包装器是链式的,这可以很容易地通过使用模板函数和 dynamic_cast
来完成,通过遍历链直到类型转换非 NULL
。更一般地,bool WrpChainBase::DynamicFind<A>(A*&, TT::IH)
正是如此。但它是在 WrpChainBase
中实现的,因此它适用于所有链式包装器。
当然,由于我们基于 dynamic_cast
,因此必须启用 RTTI。
模态对话框
模态对话框是Windows API中的一个不对称之处:DialogBox
Windows函数需要一个 DLGPROC
,但该“proc”并非真正的 WNDPROC
。
真正的 WNDPROC
对系统是私有的。它调用您的过程,如果返回 false,则调用 DefDlgProc
。所有这些都在 DialogBox
API 内部的模态循环中。
为了将其重新纳入已有的包装器中,我实现了一个 EWnd::CreateModalDialog
,它像 CreateWnd
一样,设置一个钩子并将一个内部隐藏函数作为 DLGPROC
传递。该钩子将正在创建的新窗口(在这种情况下是对话框)附加到请求的包装器上,并自动解除自身。
钩子过程已进行审查,以使钩子在最短的时间内保持活动状态(避免在窗口嵌套创建的情况下(例如父窗口在创建过程中创建其子窗口)反复进入钩子函数)。所提供的隐藏 DLGPROC
始终返回 false,除了代码在 1 到 7 之间(包括 IDOK
、IDCANCEL
、...、IDCLOSE
:只是为了有一个返回的默认处理。否则您可能会将程序卡在“关于”对话框中!)。
经过审查的钩子过程还纠正了一个bug:在以前的版本中,如果一个仍在创建中的窗口通过捕获 WM_CREATE
来创建更多窗口(例如一个带有子窗口的主窗口),则会实例化多个嵌套钩子,但返回时只会解除最后一个。尽管这在功能上没有影响(钩子过程除了附加第一个包装器之外什么也不做),但在某些情况下可能会对性能产生影响。现在,这种情况不再发生:钩子的“解除钩子”是在钩子过程本身内部完成的,在任何消息被分派给任何包装器之前。不会出现递归。
将所有功能付诸实践
在一个简单的应用程序中演示所有这些并不容易,它或多或少什么也没做,但允许您检查几乎所有内容。
W3 项目在实现此功能时同时使用了 Nlib 和 NGUI。我首先创建了一个框架,用 EMenuUpdator
和 EMauImagerIDE
包装它,并在 WM_CREATE
期间添加了一个子窗口。
我还将命令路由到子窗口,并在主窗口中处理 ID_FILE_EXIT
,在子窗口中处理 ID_HELP_ABOUT
(好的:这很不寻常,但为了演示命令路由,没问题!)。
为了响应 ID_HELP_ABOUT
,我实例化了一个模态 DialogBox
。
使用通用控件
需要链接 ComCtrl32.lib 导入库,并调用 InitCommonControlsEx
。这是一个烦人的、总是需要的东西,所以我把所有这些都放在一个类中 (NUtil::SInitCommonControlsEx
)。只需实例化一个临时对象,调用构造函数并传递所需的值(我默认设置为 ICC_WIN95_CLASSES
),就可以了。该库通过 NGUI/CommCtrl.h 头文件中的 #pragma comment(lib ...)
进行链接。
注意:我没有将此初始化设置为隐式(即通过静态实例化对象),因为它不一定所有应用程序都以相同的方式需要通用控件。
为了对“关于”对话框做一些事情,我用 CAboutBox
包装了它,并在创建时实例化了一个计时器,该计时器带有一个进度条进行倒计时。当倒计时到零时,对话框会自动关闭。(按下确定键,可以提前关闭)。这只是为了演示消息映射与 DLGPROC
正常工作。
处理更复杂的布局
这个想法是让一个框架通过类似于 IDE 的算法来操作客户端窗口和一组停靠的“条”。
DockMan.h 和 DockMan.cpp 包含了所需的内容。
特别是,两个接口定义了可停靠对象和框架之间的交互。
停靠管理器
ILayoutManager
定义了 RedoLayout
函数的原型,而 IAutoLayout
定义了 DoLayout
和 GetSideAlignment
函数的原型。通常,ILayoutManager
的实现应该包含或引用多个 IAutolayout
元素,以便在移动或调整大小时进行排列。调用 RedoLayout
时会传递请求的 HWND
(如果布局算法要跳过它。通常此参数为 NULL
)。它应该反过来调用包含的 IAutoLayout
的 DoLayout
,并传递一个矩形。包含的 IAutoLayout
应该根据该矩形重新设计自身,并修改它以适应未覆盖的矩形部分。
简而言之,ILayoutManager
定义了布局应该如何完成。IAutoLayout
是被布局的组件。
所提供的实现为了避免类之间的纠缠,将 ILauoutManager
的实现拆分为两部分。
一个内部类(XLayoutProvider
)在堆上实现了 ILayoutManager
,可以通过调用静态函数 ILayoutManager::Get(HWND)
来获取:它(通过 DynamicFind
)检索与传入 HWND
关联的 ILayoutManager
,或者(如果没有附加)创建一个 XLayoutProvider
并附加它,使其成为一个自动删除的观察者。
XLayoutProvider
通过获取客户端矩形并将其传递给数据结构为 ILayoutManager::Autonotify
的类型化通知消息来处理 WM_SIZE
消息。
此时,您可以附加任意数量的包装器,这些包装器实现对此消息的句柄,并根据数据结构携带的矩形和一些拥有的数据,决定如何处理任意数量的嵌入或引用的 IAutoLayout
元素。
特别是,EDockBarManager
通过包含一个 EClientWnd
和四个(每边一个)EDockBar
来实现 ILayoutManager::Autonotify
。每个 EDockBar
可以接收任意数量的 HWND
进行嵌入,并在嵌入时将一个 EDockBar::XElement
附加到传入的 HWND
。这个其他内部类被设计为与 EDockBar
协同工作,它是一个观察者自动删除 EWnd
。它存在于堆上,并在其所依附的窗口被销毁时销毁,并维护停靠状态(位置),管理包装 HWND
的停靠和取消停靠。因此,停靠功能不需要在传入窗口中设计,而是在窗口附加时“即插即用”。
这些 HWND
不需要是任何特殊的类型:它们只是由 EDockBar
的父级(通常是 EDockBarManager
)拥有的常规弹出式窗口。它们在停靠时成为工具栏的子窗口,在浮动时又变回弹出式窗口。
当然,这些窗口可以是控件、工具栏或……更复杂的窗口(文章的第四部分将探讨这个主题)。
关于命令,EDockBar
和 EDockBar::XElem
都将接收到的命令转发给父级(或所有者),而 EDockBarManager
则将它们转发给客户端窗口。这创建了一种类似于 MFC 的命令路由。
示例应用程序
在示例应用程序中,我创建了一个 CMainWnd
,它又创建了 8 个位置不同的工具栏(参见 CMainWnd::OnCreate
)。有些是可移动的,有些是可调整大小的。
有些工具栏有一个常规的“关闭按钮”。如果你“关闭”它们,它将像往常一样,销毁工具栏(并且由于它被一个内部的自动删除观察包装器包装,该包装器也会被销毁)。我没有提供任何接口来管理工具栏的创建或隐藏,因为它不属于这些类的范围。
注意 WinMain 中的 NUtil::STrace::_Filter() = 2;
:这是为了避免 STrace
类在调试输出中显示大量来自消息调度和 GDI 句柄包装-解包装活动的消息。
进一步的工作
我目前正在研究工具栏、状态栏、菜单栏等统一模型,以及用于内部窗口的类IDE界面。它们将是第四部分的主题。
历史
- 2004年5月27日发布。
- Bug 修复:C4346 (Misc.h 中缺少 typename):2004年6月18日发布。