65.9K
CodeProject 正在变化。 阅读更多。
Home

仅使用 C++ 类编写 Win32 应用程序

2004 年 2 月 5 日

CPOL

21分钟阅读

viewsIcon

137943

downloadIcon

3480

C++ 类和封装器,用于在不使用 MFC、ATL 或其他(第 1 部分?)的情况下编写 W32 应用程序

引言

我写这篇文章有一部分是为了开玩笑,有一部分是为了挑战自己:我曾经是一名 Win32 C 程序员,但是,在使用 C++ 时,我总是依赖 MFC、WTL、.NET、WxWin 或其他一些“框架”或封装器。

其中一个优点是它们通过隐藏某些细节来“简化”某些操作。相反,当你必须做一些不同寻常的事情时,框架类之间存在的纠缠会使工作变得相当困难。

当重写某些 MFC 类或函数时,你是否曾有过那种“侵入”他人定义的行为的感觉?你是否经常发现由于文档、框架和视图通常以不总是明显的顺序相互销毁而导致奇怪的“副作用”?

下面将介绍一种不同的方法。当然,我没有整个 MS AFX 团队的人力,所以不要期望完全替代 WTL 或 ATL。我只是想引入一个不同的可能视角。

因为我希望使所提出的类可重用,所以我尝试了一种能够将类纠缠度降至最低的设计。特别是,如果类 A 需要类 B,我希望避免类 B 也需要类 A。因此,你也可以在我的框架之外使用这些类,甚至可以在 MFC 应用程序中使用。

但是,我只依赖 STL 容器和算法以及一些 CP 作者的想法:特别是 Paul BartlettAlex Farber。(我没有重用他们的代码,但实际上,我重新解释了他们的想法。)

约定

我按照 这篇文章 的约定部署了所有类。我还使用了命名空间,并将所有命名空间放在一个名为“GE_”的根命名空间中。如果这个名字给其他库带来了麻烦,只需将文件中所有出现的这个名字替换为其他名称即可。

共享所有权封装器

这是所有实现的症结所在:Win32 API 中充满了各种句柄,你必须“获取”、“使用”和“释放”它们。

在我看来,封装器不应该重写 C API:我没有特别的理由将 ShowWindow(hWnd, SW_SHOW) 替换为 pWnd->ShowWindow(SW_SHOW):它们的“参与者”完全相同,而仅仅为了将第一个参数移到括号之外而重写数千个原型……对我来说,这不是一个好的投资。

更有趣的是封装句柄,以保护它们免受不当销毁策略的影响,或者避免忘记销毁它们,或者为那些通常有许多参数且参数值通常相同的功能提供更有效的构造函数。

在此实现中,封装器类似于“引用计数智能指针”(实际上,它是一个泛化):它可以是它所封装的句柄的“所有者”(因此,当没有“所有者”持有它时销毁它),也可以是当所有“所有者”都离开时表现为“无效”的“观察者”。

但是,由于我们可能拥有许多具有不同管理策略的不同句柄,我决定将实现拆分为许多相互继承的模板类。

架构

整个机制的核心是 WrpBaseXRefCount 之间的协作。

WrpBase 为封装器提供了“协议”:它引用一个“引用计数器”,并且可以“Attached”和“Detached”到它所封装的对象。它还可以设置为“所有者”或“观察者”,可以“Valid”或不“Valid”,并返回 STL 迭代器以遍历封装器集合。

XRefCount 反过来维护关于有多少(以及最终“谁”)封装器封装给定值或句柄的信息,并为 WrpBase 提供一些辅助函数来检索和更新这些信息。

WrpBase 被设计为可派生(它的一个模板参数 W 是你从中派生的类),并在将其“this”指针 static_castW* 后调用其所有函数。这允许你重写其函数。

此外,WrpBase 的行为取决于由 WrpBase 继承的 trait 类提供的一些“助手”。这个“trait 类”的目的还在于提供一些方便的 typedef,用于定义封装对象在封装器中的存储方式、作为成员函数的参数接收方式、作为返回值给出方式或解引用方式。

Trait 类以各种版本实现,并从“类型类”继承类型定义。

为了将所有内容封装成可赋值、可复制等,然后设计了一个 Wrp 类,它继承自 WrpBase 或派生类,使用 WrpBase 定义的协议(经过你自己的定制,如果有的话)应用赋值运算符、复制构造函数、相等 (==) 和排序 (<) 运算符、访问 (->) 和解引用 (*) 等。

下图概述了所有这些内容。

Wrp architecture

继承从上到下。XRefCount 知道(因为模板参数)“YourClass”,Wrp 知道(因为继承),并以你可以最终重写的方式调用 WrpBase 原型函数。WrpBase 反过来使用从“traits”和“types”继承的内容。

实现

Types

types”以两种不同的方式实现

  • types_wrp_hnd<H>:假设 H 是一个句柄或一个“小对象”或一个指针或...可以无信息丢失地转换为 LPVOID 并“按原样”存储的东西。
  • types_wrp_ptr<H>:假设 H 是堆上的一个对象,我们必须用智能指针引用它。因此,封装器存储一个 H*,并且封装器将具有“指针语义”。

这两个类都定义了以下类型

type description types_wrp_hnd types_wrp_ptr
SH 封装器存储以表示 H 的内容 H H*
IH 封装器将在函数中接收的输入参数 const H& H*
OH 封装器将从其函数返回的内容 const H& H*
RH 封装器将作为“解引用”返回的内容 const H& H&

在特定的实现中,SHIHOHRH 可能会有所不同(例如:你可以返回一个代理对象来存储引用...)。

无论如何,要求在将 IH 转换为 SH、将 SH 转换为 OH 以及将 OH 转换为 IH 时,隐式转换能够正常工作。

特性

traits”以三种不同的方式实现

  • traits_wrp_hnd<H, Types>:假设 H 是一个句柄。一旦 H 已知且其管理策略已定义,它的大部分函数都需要重写。
  • traits_wrp_ptr<H,Types>:假设 H 是一个“指向对象”(封装器作为 H* 操作),并实现 CreatenewDestroydelete。指针之间的转换通过 static_cast 进行。
  • traits_wrp_dynptr<H,Types>:与之前类似,但使用 dynamic_cast

通常,“Types”参数在第一种情况下默认为 types_wrp_hnd<H>,在其他两种情况下默认为 types_wrp_ptr<H>。但对于某些特殊情况,可以采用其他解决方案。

“traits”类的目的是提供一些低级辅助函数,以标准化上层“layer”类操作各种“类型”的接口。

一个例子是 traits_wrp_hnd 本身的代码

template<class H, class Types=types_wrp_hnd<H> >
struct traits_wrp_hnd: public Types
{
    typedef Types TT;
    static bool equal(TT::IH left, TT::IH right) { return left == right; }
    static bool less(TT::IH left, TT::IH right) { return left < right; }
    static LPVOID Key(TT::IH h)  { return (LPVOID)h; }
    template<class A> static void from(const A& a, TT::SH& h) { h = (H)a; }
    static TT::IH invalid() {static TT::SH h(NULL); return h; }
    static TT::OH Access(TT::IH h) { return h; }
    static TT::RH Dereference(TT::IH h) { return h; }
    void Create(TT::SH& h) { h = invalid(); }
    void Destroy(TT::SH& h) { h = invalid(); }
};

请注意,“invalid”实现为返回“NULL”值。对于某些类型的句柄,这可能会被重写。

WrpBase

它定义为

template<
    class H,    //what we are wrapping
    class W,    //outside wrapper (derived from here)
    class t_traits, //invalid H behaviour
    class t_refCount    //reference counter type used
>
class WrpBase:
    public t_traits
{
public:
    typedef WrpBase TWrpBase;
    typedef W TW;
    typedef t_refCount TRefCount;
    typedef t_traits TTraits;
    typedef H TH;
    typedef TTraits::TT TT;
    friend WrpBase;
    friend TRefCount;
    friend TTraits;
protected:
    TT::SH _hnd;
    t_refCount* _pRC;
    bool _bOwn;
    ...
}

它提供了一个 SH 成员的存储空间,一个指向 t_refCount 类的指针(假设具有与 XRefCount_base 相同的原型),以及一个定义此封装器是“所有者”还是“观察者”的 bool。它还实现以下内容

  • void Attach(TT::IH h) 将封装器附加到传入的“哑值”。搜索该值的现有引用计数器,如果未找到则创建。
  • void AttachWrp(const WrpBase& w) 将封装器附加到另一个封装器携带的值。
  • template<class A> void AttachOther(const A& a) 将封装器附加到可以通过从 A 类型转换检测到的值。
  • void Detach() 分离封装器。如果我们是与封装值关联的最后一个所有者封装器,则调用 Destroy(继承自 TTraits)。
  • bool IsValid() const 返回封装器是否应被视为有效,从而返回封装对象。
  • bool IsOwner() const 返回封装器是否是“所有者”封装器。
  • void SetOwnership(bool bOwn) 将封装器设置为“所有者”(true)或“观察者”(false)。
  • typedef TRefCount::iterator iterator; static void get_range(iterator& first, iterator& last, TT::IH h) 从与封装值关联的最终封装器集合中获取迭代器范围。如果 first==last,则集合为空或不存在。

它们是根据其他函数(总是通过 pThis() 函数,它将 this 转换为 W*)和 TRefCount 函数实现的。它还向 TRefCOunt 提供一组“不执行任何操作”的回调函数

  • void OnFirstAttach(XRefCount_base* pRC) 当引用计数器对封装对象进行第一次引用计数时调用。
  • void OnAddRef(XRefCount_base* pRC) 每次添加新引用时调用。
  • void OnRelease(XRefCount_base* pRC) 每次释放引用时调用。
  • void OnLastRelease(XRefCount_base* pRC) 当最后一次释放发生时调用。

引用计数

必须从 XRefCount_base 派生,重写其函数

class XRefCount_base
{
public:
    bool _bValid;
    XRefCount_base() { _bValid = true; }

    int owners() { return 0; }
    int observers() { return 0; }
    int reference() { return 0; }

    void AddRef(W* pW, bool bOwn)
    {}
    
    bool Release(W* pW, bool bOwn)
    {return false;}

    //doesn't iterate
    iterator begin() { return NULL; }
    iterator end() { return NULL; }
};

它实际上以两个版本实现

  • XRefCount<W>W 被假定为 WrpBase 派生类。实现所有者和观察者引用计数。(reference() 是两者的总和)。当不再有引用时,它在 Release() 上自动销毁,返回 true(否则返回 false)。不提供迭代器(返回“first”和“last”到随机相等值)。
  • XRefChain<W>:维护一个 W* 列表,在 AddRef 时推入前面,在 Release 时删除。当列表变空时自动删除。

第一个可以用于只需要引用计数的类型,最后一个可以用于你需要跟踪“谁”是封装器的类型。

反向映射

XMap 托管一个静态 std::map<LPVOID, XRefCount_base*>,提供 MapUnmapFind 函数。

封装器知道它们封装了什么,并且知道谁是关联的引用计数器。但是,如果我们将一个值分配给一个封装器,我们必须有一种方法来找出(如果该“值”已经被其他人封装)是否存在(以及谁是)关联的 XRefCount

traits 类附带的函数之一是 Key(IH)。它将封装的类型转换为 LPVOID

这可以通过 C 风格的转换(如 traits_wrp_hnd)、static_cast(如非多态指针,在 traits_wrp_ptr 中)和 dynamic_cast(用于多态指针,如 traits_wrp_dynptr)来完成。

特别有趣的是最后一个:给定一个复杂对象(具有许多基类),指向不同组件的不同指针可能不同(在绝对值方面),但是 dynamic_castLPVOID 总是返回相同的基地址(整个对象的地址)。这允许在引用不同组件的不同智能指针之间共享复杂对象的所有权:它们都可以找到相同的引用计数对象。

运算符 ( Wrp<B> )

转换构造函数、复制构造函数、赋值运算符等不能作为普通函数继承。它们的重写需要特别注意,因此我更喜欢将所有这些内容集中到一个特定的模板类中。Wrp<B> 是“上层类”。BWrp 派生自的类。它可以是任何 WrpBase 派生类。它根据 Detach/Attach 实现赋值、复制、转换和销毁。

一些特殊情况:智能指针

要专门化一个包装器,一个方法可以是这样:

  1. 声明一个从 WrpBase 派生并指定所需参数的 struct,然后
  2. typedef 一个 Wrp<yourstruct>,以提供赋值和复制功能。

一个非常简单的情况是智能指针的定义

template<class T>
struct Ptr
{
    struct StatBase:
        public WrpBase<T, StatBase, 
          traits_wrp_ptr<T>, XRefCount<StatBase> >
    {};

    struct DynBase:
        public WrpBase<T, DynBase, 
          traits_wrp_dynptr<T>, XRefCount<DynBase> >
    {};

    typedef Wrp<StatBase > Static;
    typedef Wrp<DynBase > Dynamic;
};

这两个结构定义了 static_castdynamic_cast 指针封装器的行为。

如果 A 是一个类,那么指向 A 的智能指针可以是...

typedef Ptr<A>::Dynamic PA;.

智能句柄

同样,我们使用以下方式获取“智能句柄”

template<class H, class W>
struct HndBase:
    public WrpBase<H, W, traits_wrp_hnd<H>, XRefCount<W> >
{};

请注意,虽然 Ptrxxx structs 关闭了封装器的继承(它们将自己作为 W 参数传递给 WrpBase),但 HndBase 没有:仍然存在一个 W 参数。

这是因为,对于指针来说,很清楚 newdelete 是创建和销毁策略(CreateDestroy 以这种方式来自 trait 类),而对于句柄来说,所有这些都仍待定义。不同的句柄有不同的创建和销毁 API。那么 HDC 就有很多!所以我们必须稍后完成。

当然,对于某些特定对象,你可能还想做一些除了“new”或“delete”之外的事情:在这种情况下,你必须自己定义一个从 WrpBase 派生(并关闭其继承)的 struct,然后将其包装到 Wrp 中。

链式动作

使用 XRefChain 作为 WrpBase 参数,我们可以将“同一实体的封装器”链接在一起并遍历它们。

一个有趣的可以做的事情是将动作与这个“遍历”关联起来。这个想法是提供一种方式,例如,将 Windows 消息分派给 HWND 封装器。但不仅如此。

EChainedAction<W, I, A> 是一个设计为可派生的类(W 是其派生类),它将一个以 A 为参数的动作与迭代器 I 的遍历相关联,该迭代器必须是一个 STL 兼容的前向迭代器,它解引用为 W*(其 I::operator*() 返回一个 W*)。例如,这样的迭代器可以通过 WrpBase::get_range(...) 获得。

函数 Act(const A& a, iterator first, iterator last) 遍历从 first 到 last,调用成员函数 OnAction(const A& a),该函数假定在 W 中被重写。

然而,迭代的方式不是一个纯粹的扁平循环:它是一个对存储参数的递归:Act 填充栈上的数据结构,并将其地址保存到每个线程保存的静态变量中,保留以前的地址(每个线程变量是通过映射访问的变量,其键是当前线程 ID),然后调用 Default()Default 检索数据结构并调用 OnAction 进行迭代。如果 OnAction 返回 true,则 Default 立即返回 true,否则迭代到下一个元素,直到到达“last”。此时返回 false

因此,在你的 OnAction 重写中,你可以

  • 简单地返回 false。迭代将继续到下一个链式元素。
  • 简单地返回 true。迭代停止。
  • 在适当的时候调用 OnAction。对下一个元素的迭代被提前,控制权返回给你。

或多或少,这就像用另一个子类化窗口过程,如果需要,就调用前一个。任意多次。

事件

智能指针和链式动作的组合是事件。

在这里,它们被实现为“函数对象”,声明为类的成员变量(事件源),由内部对象(“接收器”)附加,这些内部对象将动作分派给注册的“接收器”。

Event<A> 是一个 struct,它只是一个 operator()(const A&) 成员,它假设事件必须由可链式封装器封装。运算符获取链,并调用 Act

EventRcv<D> 是一个通用派生类 (D) 的基类,提供 RegisterEvent<A>(Event<A>&, bool(D::*pmemfun)(consty A&) )UnregisterEvent<A>(Event<A>&)。第一个函数创建一个 XTypedSync<A,R>,它将事件与给定的成员函数关联起来。第二个清除关联。

XTypedSync<A,R> 派生自 XSync<A>,后者是具有参数 AEvent 的可链式封装器。XSync 通过委托给 virtual DoAction 函数来实现 OnAction,该函数在派生类 XTypedSync 中实现,调用给定的成员函数(通过在关联期间保存的成员指针)。需要使用虚函数,因为不同的 XTypedSync(引用不同的接收器,因此具有不同的 R 参数)必须都作为 XSync<A> 链接在一起。

因此,调用一个事件将为所有关联的接收器调用注册的成员函数。

注册 WNDCLASS

如果你觉得填写 WNDCLASSEX 等数据结构的字段是一项极其烦人的任务,那么这个封装器就是为你准备的

SWndClassExReg 注册一个窗口类,当不再有引用可用时,将其注销。

它派生自 WrpBase,并定制了 Destroy

构造函数尝试使用提供的参数填充 WNDCLASSEX

  • 一个版本从资源中加载,带有传入的 ID(图标和光标),另一个版本使用传入的原始值。
  • 这两个构造函数都注册窗口类并保存返回的 ATOM
  • 封装器的 Destroy 函数(当最后一个所有者封装器分离时调用)调用 UnregisterWndClass,并传入保存的 ATOM

消息循环(SMsgLoop)

消息循环由 SMsgLoop 实现。你可以根据需要创建任意数量的实例。通常,一个将在 WinMain 中,而其他可能在需要“模态循环”的地方(例如:当询问鼠标坐标以检测对象放置位置时)。

“循环”本身由一个 LoopBody 函数(它本身不是一个循环)实现,该函数实现消息检索和分派,还带有一些消息过滤/翻译和空闲处理。这样的函数由 LoopWhile 调用,LoopWhile 循环直到传入的 bool 引用变为 false 或直到收到 WM_QUITLoopWhileLoop 调用,LoopLoopWhile 提供一个“总是真”的值。

这种将循环分解为三个组件的方法允许在应用程序内部(使用 LoopWhile)运行模态循环,或者在循环计算期间(从宿主循环内部调用 LoopBody)分派消息。

SMessageLoop 还提供了一个成员来存储 HWND(可以是应用程序主窗口),其销毁将导致发布 WM_QUIT

如果 SMsgLoopbAutoQuit 参数设置为 true 创建,则通过该消息循环创建的第一个窗口将成为消息循环主窗口。

空闲处理和消息过滤

空闲处理和消息过滤通过全局事件实现。

此解决方案避免了在消息循环实现和(例如)窗口实现之间产生纠缠的风险。你也可以在不使用我的消息循环的其他框架中使用我的窗口。唯一的要求是正确生成事件。

SIdleEventSMsgFilterEvent 都是 EGlobalEvent

SIdleEvent 参数是调用事件的 SMsgLoop。如果你自己发起事件,可以传递 NULL

SMsgFilterEvent 参数是 Win32 MSG 结构。在事件处理程序中,返回 true 以指示已过滤传入的消息。

封装 HWND:子类化窗口

HWND 封装器必须能够接收窗口消息。这意味着它们既要链接 WrpBase,又要链接 EChainedAction,但这还不够。

还需要从系统获取消息。这需要子类化/重类化机制。此外,还需要将消息分派给可能不同的成员函数。有不同的方法可以做到这一点:

  • MFC 风格:成员函数作为地址与相应的消息 ID 映射。当子类窗口过程收到消息时,它扫描映射以查找匹配项,然后调用相应的函数。可以提供一组宏来协助静态映射填充。
  • ATL 风格:一组宏提供识别消息并将其分派到指定函数所需的代码片段。这些宏还会重写一个由子类化窗口过程调用的众所周知的虚函数。
  • .NET 风格:子类化窗口过程解析消息,将其分派到一组众所周知的成员,每个成员都会引发不同的事件。

这些方法都不是完美的:前两种强烈依赖宏,第三种需要大量数据结构并且不对称:有一个对象“拥有”HWND,其他对象附加到它。

这些“不完美”已通过一系列向导得到弥补,这些向导有助于编译消息映射或链接事件处理程序。考虑到我们需要处理的消息数量巨大,它们可能是编程技术和工具可用性之间的最佳折衷。根据我的经验,我发现 ATL/WTL 方法更灵活,因此我做了以下工作:

  1. 同一 HWND 的封装器链在一起。第一次附加和最后一次释放用于对封装的 HWND 进行子类化/重新类化。
  2. OnFirstAttach 被重写以进行窗口子类化。
  3. 一个子类化窗口过程对与 HWND 相关的链式动作执行“Act”。
  4. OnAction 被重写以调用一个众所周知的虚函数,其原型与 ATL 的 ProcessWindowMessage 相同。
  5. 如果操作返回未处理,则重写 Default 以调用上一个窗口过程。
  6. 为了存储上一个窗口过程,我们需要一个所有相同窗口的包装器都共享的地方。因此,XRefChain 必须派生以托管此值。

所有这些的结果就是 EWnd

其声明如下

class EWnd:
    public IMessageMap,
    public NWrp::WrpBase<HWND, EWnd,NULL,traits_wrp_types<HWND>, XRefChainWnd>,
    public NWrp::EChainedAction<EWnd, EWnd::iterator, LPWndProcParams>,
    public NUtil::EAutodeleteFlag
{
public:
    void OnFirstAttach(NSmart::XRefCount_base* pRC); //do window subclassing
    static bool Default();  //do superwndproc calling
    void Destroy(HWND& hWnd);  //destroy HWND (only owners does !)
    bool OnAction(const LPWndProcParams& a); //call ProcessWindowMessage
    
    //copy and assign
    EWnd() {SetOwnership(false);}
    EWnd(const EWnd& w) { SetOwnership(false); AttachRefCounting(w); }
    EWnd& operator=(const EWnd& w) { AttachRefCounting(w); return *this; }
    EWnd& operator=(HWND h) { Attach(h); return *this; }
    explicit EWnd(HWND h) { SetOwnership(false);  Attach(h); }

    //IMessageMap defaul implementation
    virtual bool ProcessWindowMessage(
        HWND hWnd, UINT nMsg, WPARAM wParam, LPARAM lParam, 
        LRESULT& rResult, DWORD msgMapID) { return false; }

    //some helpers common for all the windows
    LRESULT ForwardNotifications(UINT uMsg, 
        WPARAM wParam, LPARAM lParam, BOOL& bHandled);
    LRESULT ReflectNotifications(UINT uMsg, 
        WPARAM wParam, LPARAM lParam, BOOL& bHandled);
    BOOL DefaultReflectionHandler(HWND hWnd, UINT uMsg, 
        WPARAM wParam, LPARAM lParam, LRESULT& lResult);

    //window creation
    bool CreateWnd(
        HINSTANCE hInst,
        LPCTSTR clsName,
        LPCTSTR wndName,
        DWORD dwStyle,
        LPCRECT lprcPlace,
        HWND hwndParent,
        HMENU hMenu,
        LPVOID lpParams
        );
};

注释

  1. IMessageMap 是一个以 ATL 风格声明 ProcessWindowMessage 函数的接口。
  2. 派生自 ERefChaining,但它使用 XRefChainWnd。它派生自 XRefChain,但也托管一个 WNDPROC 函数指针和一个递归计数器。
  3. 派生自 EChainedAction,使用 LPWndProcParamsXWndProcParams 是一个 struct,它包含 hWnduMsgwParamlParamlResult
  4. 派生自 EAutoDeleteFlag。如果设置为 true(默认值为 false),则在窗口销毁时,封装器会自行删除。
  5. 封装器总是以“非所有者”(观察者)身份创建。将其设置为“所有者”将在所有所有者分离时销毁 HWND

“白痴”程序(形容词,非所有格!)

太简单了,除了演示所有东西的运作之外,没有其他用处。

#include "stdafx.h"

#include "NLib/MsgLoop.h"
#include "NLib/Wnd.h"

using namespace GE_;

int APIENTRY _tWinMain(HINSTANCE hInstance, 
    HINSTANCE hPrevInstance, LPTSTR lpCmdLine, int nCmdShow)
{
    NWin::SMsgLoop loop(true);  //instantiate a loop
    
    LPCTSTR wcname = _T("W1MainWnd");
    NWin::SWndClassExReg wc(true, hInstance, wcname); 
    //instantiate a window class
    
    NWin::EWnd wnd; 
    wnd.CreateWnd(hInstance, wcname, NULL, 
        WS_VISIBLE|WS_OVERLAPPEDWINDOW, 
        NWin::SRect(NWin::SPoint(CW_USEDEFAULT,CW_USEDEFAULT), 
        NWin::SSize(600,400)), NULL, NULL, NULL);

    loop.Loop();

    return 0;
}

创建一个基于 DefWndProc(作为默认参数传递给 SWndClassExReg)的窗口,该窗口...真的什么都不做...在一个 76KB 的可执行文件中。

不是我老生常谈中最短的 exe,但请考虑所有 traits 函数、std::maplist 等。它们提高了灵活性,但有代价!

Hello World:GDI 封装器

作为第一步,我们需要将 EWnd 派生为 SWnd 并实现一个分派 WM_PANT 的消息映射。

然后我们需要消息映射宏,...

然后我们需要“特色”GDI 对象封装器...或者包含 GDI+ 库。

消息映射宏

受 ATL 宏启发,它们具有相同的名称,以允许 IDE 向导以相同的方式完成其肮脏的工作,但是……它们与 ATL 不同(它们被设计为在 EWnd 封装器中工作,而不是 CWndCWindow!)。它们包含在 MessageMap.h 中。ATL 基本宏也很好用,但 WTL 宏不行。

消息解析宏,同样具有与 WTL 相同的名称和参数,但实现不同。

注意:我是通过大量使用“查找和替换”从原始源文件获得的。它们有数百个,所以我无法单独测试所有它们,因此,因为所有这些过程都是相同的,而且我测试过的那些工作正常,...我假设所有它们都工作正常。

但是它们的使用方式很容易测试。简单地...不要忘记!

此时,一个非常简单的“hello world”程序可以是这样的

// W2.cpp: W32 test application

#include "stdafx.h"

#include "NLib/MsgLoop.h"
#include "NLib/Wnd.h"

using namespace GE_;

class CMainWnd: public NWin::EWnd
{
public:
    BEGIN_MSG_MAP(CMainWnd)
        GETMSG_WM_PAINT(OnPaint)
    END_MSG_MAP()
    void OnPaint();
};


int APIENTRY _tWinMain(HINSTANCE hInstance, 
  HINSTANCE hPrevInstance, LPTSTR lpCmdLine, int nCmdShow)
{
    NWin::SMsgLoop loop(true);  //instantiate a loop
    
    LPCTSTR wcname = _T("W1MainWnd");
    NWin::SWndClassExReg wc(true, hInstance, wcname); 
    //instantiate a window class
    
    CMainWnd wnd; 
    wnd.CreateWnd(hInstance, wcname, NULL, 
        WS_VISIBLE|WS_OVERLAPPEDWINDOW,
        NWin::SRect(NWin::SPoint(CW_USEDEFAULT,CW_USEDEFAULT), 
        NWin::SSize(600,400)), NULL, NULL, NULL);

    loop.Loop();

    return 0;
}

void CMainWnd::OnPaint()
{
    PAINTSTRUCT ps;
    BeginPaint(*this, &ps);
    
    NWin::SRect R; 
    GetClientRect(*this, &R);
    DrawText(ps.hdc, _T("Hallo World !"),-1, 
          R, DT_CENTER|DT_VCENTER|DT_SINGLELINE);

    EndPaint(Value(), &ps);
}

这不是很棒吗?还没有:那些 BeginPaint - EndPaint 对是什么?为什么我们不将 HDC 封装到可以正确管理 Begin-End 或 Get-Release 对的封装器中呢?退出之前保存和恢复状态又如何呢?

封装 HDC

在封装时保存 DC 状态的想法似乎不错,但是...状态应该存储在哪里呢?

有两种可能的答案

  • 在封装器本身中:同一 DC 的每个封装器都保留自己的状态,在 Attach 时保存,在 Detach 时恢复。这对于“短生命周期封装器”很有用:你在函数开头附加一个,并且确定它的销毁(在结尾)将恢复 DC 并关闭它。嵌套函数调用会创建一系列封装器,它们将按相反顺序销毁。所以没问题。
  • 在引用计数对象中:保存的状态由同一 HDC 的所有封装器共享,并在最后一个所有者分离时恢复。这对于“长生命周期封装器”很有用,这些封装器可以在函数之间作为值传递,或者保持封装对象活动直到其他对象引用它。EWnd 中的“SuperWndProc”就是这种情况,但 HDC 不是:它的正常生命周期不会在命令处理过程中存活。

鉴于上述考虑,我提供了一组可以互操作(它们使用相同的 XRefCount)但具有不同 Attach/Detach 策略的 HDC 封装器。

所有类都基于 SHdcSave_base<W>,它重写 OnAddRefOnRelease 以保存和恢复 DC 状态。然后

  • SHdcSave(HDC):它只是 SHdcSave_base 的闭包,带有一个将 HDC 作为参数的构造函数。
  • SHdcPaint(HWND):重写 Create 以调用 BeginPaint,重写 Destroy 以调用 EndPaint。构造函数保留一个 HWNDPAINSTRUCT 可作为 read_only 访问。
  • SHdcClient(HWND)Create 调用 GetDCDestroy 调用 ReleaseDC
  • SHdcWindow(HWND)Create 调用 GetWindowDCDestroy 调用 ReleaseDC
  • SHdcMem(SIZE):创建一个与屏幕兼容的内存 DC,并在其中选择一个与给定 SIZE 兼容的位图。适用于双缓冲。

使用 SHdcPaintOnPaint 方法变为这样

void CMainWnd::OnPaint()
{
    NGDI::SHdcPaint dc(*this);

    NWin::SRect R;
    GetClientRect(*this, R);
    DrawText(dc, _T("Hallo World !"),-1, 
         R, DT_CENTER|DT_VCENTER|DT_SINGLELINE);
}

HPEN, HBRUSH, HFONT

GDI 对象可以通过多种方式创建,但始终使用相同的 API 销毁:DeleteObject

因此,SGdiObj<H> 使用 HndBase 封装任何 HGDIOBJECT,通过调用 DeleteObject 重写 Destroy 回调。

重要的是,GDI 对象的销毁发生在对象未选择到任何 HDC 中时。这可以通过在初始化 HDC 封装器之前初始化 GDI 封装器来实现。这将使 HDC 封装器在其状态恢复之前超出范围,并释放 GDI 对象,这些对象可以由它们自己的封装器销毁。

这是使用不同字体的“OnPaint”方法

void CMainWnd::OnPaint()
{
    NWin::SRect R; 
    GetClientRect(*this, R);

    NGDI::TFont font(CreateFont(60,0,0,0, FW_BOLD,
        0,0,0, 0,0,0,ANTIALIASED_QUALITY, FF_SWISS, NULL));
    
    NGDI::SHdcPaint dc(*this);
    SetTextColor(dc, GetSysColor(COLOR_WINDOWTEXT));
    SetBkColor(dc, GetSysColor(COLOR_WINDOW));
    SelectObject(dc, font);

    DrawText(dc, _T("Hallo World !"),-1, 
        R, DT_CENTER|DT_VCENTER|DT_SINGLELINE);
}

注意 fontdc 之前创建,因此 dc 将首先销毁。当这种情况发生时,将调用“RestoreDC,然后是 EndPaint”。RestoreDC 将取消选择字体,EndPaint 将关闭绘图。此时,font 销毁将删除 GDI 对象。

进一步的问题

好的:现在大局已定。我将进行进一步的完善,并介绍一些更具体和详细的类。但这可能是后续文章的更好主题。

历史

  • 2004 年 2 月 5 日:首次发布。
  • 2004 年 2 月 17 日:一些代码更新:在模板类中使用 typename 关键字。
  • 2004 年 3 月 2 日:修复了一些“coords.h”错误 (SRect 算术)
© . All rights reserved.