用C++和Win32的方式编写GUI [ZT]

WTL delivers the object oriented way to program for the Windows user interface, while keeping the code size small. It also provides a great foundation that developers can extend with their own classes.

-- Nenad Stefanovic

  认识Windows Template Library

  我们直接借用WTL库的作者Nenad Stefanovic先生对于WTL的介绍吧。2002年,Stefanovic先生在接受C-View.ORG采访时,对于他钟爱的WTL库,给出了这样的描述:[2]

  WTL is a template based library for user interface development. It extends ATL to provide classes for implementing user interface for applications, components, and controls. It provides classes for various user interface elements: top-level windows, MDI, standard and common controls, common dialogs, property sheets and pages, GDI objects, UI updating, scrollable windows, splitter windows, command bars, etc.

  WTL is implemented using the same template architecture as ATL, so it is a natural fit for ATL developers. It also doesn't alter or hide Windows specific constructs, thus allowing Windows programmers to use WTL without surprises. The important design goal of WTL was to avoid inter-dependencies - classes themselves do not reference other WTL classes. That means that your program will contain just the code that you actually use, and nothing else. Coupled with the use of templates, this allows creation of very small programs without run-time dependencies.

  这就是Windows Template Library,基于ATL的一个扩展程序库,在其中,C++的高级特性和Win32的编程方式得到完美的结合,它和ATL一起,形成了一个现代C++语言的Win32 GUI Framework。

  说到Win32的Framework,大多数人想到的就是MFC (Microsoft Foundation Classes),它在Visual C++当中得到了很好的支持,但是很多人同时忽略了Visual C++当中同样得到很好支持的另外一个C++程序库:Active Template Library (ATL)。同所有的模板库一样,它看起来是那么的艰深晦涩:使用了几乎所有的C++高级特性,充斥着多继承、虚函数和纯虚函数、模板、type cast,甚至要求你关心对象二进制模型。但是如果你对于这些概念烂熟于心,你会觉得ATL用起来是如此的自然,加上WTL这个extension,更是如虎添翼。你也许会像我一样惊奇的发现,WTL写程序的界面有着如此简单的结构,优雅的语法,同时又不会给你的程序带来太多的额外负担,你也会禁不住惊叹:这才是C++的方式!

  与此同时,就像WTL之父所说,WTL “不会更改或者隐藏Windows自己的结构”,的确,如果你习惯使用Win32 SDK的方式进行程序设计,那么WTL会让你觉得很自然,因为它只是在API上面加了薄薄的一层封装,使用的结构、常数还是过去熟悉的那些结构和常数,你会再次兴奋地说:这才是Win32的习惯!

  下面就让我们用C++的方式和Win32的习惯,来编写一个简单的GUI。

  一个简单的WTL程序:

  为了照顾广大未使用过WTL的同志们,我们先来用WTL写一个简单的程序。

  #include <atlbase.h>

  #include <atlapp.h>

  #include <atluser.h>

  CAppModule  _Module;

  class CDemoWnd

     : public ATL::CWindowImpl<CDemoWnd, ATL::CWindow, ATL::CFrameWinTraits>

  {

  public:

     BEGIN_MSG_MAP(CDemoWnd)

       MESSAGE_HANDLER(WM_PAINT, OnPaint)

     END_MSG_MAP()

  private:

     LRESULT OnPaint(UINT, WPARAM, LPARAM, BOOL&)

     {

        CPaintDC    dc(m_hWnd);

        RECT    rcClient;

        LPCTSTR lpszText = _T("Hello WTL!");

        GetClientRect(&rcClient);

        dc.DrawText(lpszText, lstrlen(lpszText), &rcClient,

        DT_SINGLELINE|DT_VCENTER|DT_CENTER);

        return 0;

     }

   void OnFinalMessage(HWND)

     {

        PostQuitMessage(0);

     }

  };

  int WINAPI _tWinMain(HINSTANCE, HINSTANCE, LPTSTR, int)

  {

     _Module.Init(NULL, ::GetModuleHandle(NULL));

  CMessageLoop    loop;

     _Module.AddMessageLoop(&loop);

   int nRet = 0;

   CDemoWnd    wnd;

     if ( ! wnd.Create(NULL, CWindow::rcDefault, _T("NoWizard") ) )

     {

        MessageBox(NULL, _T("Create window failed!"),

        _T("NoWizard"), MB_ICONSTOP|MB_OK);

        nRet = 1;

        goto clean_up;

     }

  wnd.ShowWindow(SW_SHOW);

     nRet = loop.Run();

  clean_up:

     _Module.RemoveMessageLoop();

     _Module.Term();

  return nRet;

  }

  一个经典的Hello World,仅仅60行,包括空行和大括号。如果我们用SDK进行开发,需要写100行左右。那么代码量的减少有没有带来什么代价?我们使用Visual C++ .NET 2003进行编译,如果我们指定优化大小(/O2),使用多线程运行库(/ML),启用C++异常(/EHsc),进行编译,WTL版本的程序为60k,SDK版本的程序为36k。但是WTL有一个特殊的选项可以使用:“在ATL中使用最小CRT”(/D_ATL_MIN_CRT),如果我们打开这个选项,编译出来的WTL版本的程序仅仅22k,强制在SDK版本的程序当中,使用这个选项,代码大小没有改变。MFC?算了,就不要在大小上面鄙视它了。如果你想比较一下的话,有一篇文章<A Quick MFC and WTL Comparison>[4],可以一看。

  我们再来看看代码的内容,感到如何?是不是很cool?你不用去考虑WinMain到哪里去了,它就在那里;不必去在Document和View之间周旋的不清不楚,不必疑惑我的窗口消息到底经过了怎样的路线,竟然到了我们根本想不到的地方……你仅仅要做的是设计一个窗口的行为,把消息映射到对应的函数上,然后创建窗口,然后让你的消息循环Run起来,直到从中返回……一切都像SDK那样自然和清晰。与此同时,你不必去费尽心机管理一堆堆的HWND,一种种的Window Class,或者一遍遍的写switch case,还要考虑如何防止两个窗口的重绘函数名字冲突……你只需要为每种窗口设计一个class,处理他们的消息到行为的映射,需要的时候,就创建一个这个窗口的实例……一切又像MFC一样轻松。最关键的,你还不必带着一个接近1M的DLL,或者让你的程序莫名的增大到数百k,同时拖慢了程序的运行速度。

  当然,这个简单的程序,还没有让你看到多继承,还没有用到纯虚函数来实现接口,但是不必着急,WTL自带了一个向导,你可以很容易地用向导生成一个WTL的框架,自己来感受一下这种C++的方式好了。

  WTL对Windows对象句柄的封装:

  当我们用SDK写了很多程序的时候,对于SDK下面的那些规则烂熟于心,例如你Create的东西一定要释放,尤其在WM_PAINT的响应当中,一定要把所有创建的GDI对象删除……但是也许你只是打了个哈欠,或者只是和mm聊了几句天,就发现,每刷新一次窗口,任务管理器GDI对象一栏的数值就会触目惊心地增加,然后心中充满了罪恶感,然后用掉更多的时间,来找到底是那个忘掉了。

  然后空闲下来,我们想想,如果有这么一种东西,它会自己释放,这是多么美好的一件事情啊,天空从此也变蓝了,阳光也明媚了,跟mm聊天的时候也不用记着我有一个画笔还没有删除了……

  “用C++封装一下不就行了”,多聪明的孩子,好,这个任务交给你了!怎么样,是不是又觉得工作量太大,这个完全是体力活啊……还好,WTL已经做了。打开atluser.h和atlgdi.h看看,佩服吧,就算是体力活这么干也值得钦佩。WTL已经把常用的每种GDI对象和资源,例如画笔(Pen),刷子(Brush),图标(Icon)等等封装好了。截止到目前,我还没有找到哪种Windows内建的东西没有对应的WTL封装。什么,你说HANDLE?还有CRITICAL_SECTION?啊哈,那个东西ATL就已经封装好了,看看CHandle和CCritSecLock的文档。ATL还封装了IUnknown等COM接口,封装了VARAINT,封装了BSTR……总而言之,几乎所有的需要封装的东西,都封装了吧。不仅如此,它们还通过成员函数的形式提供与API同名的或者具有更友好名字的函数,然后直接转手丢给API,在析构函数中,把该释放的释放掉。

  举个例子吧,随便从atlgdi.h当中摘出来了一小段代码,精简了一下,要不然充满了条件编译,以便支持WinCE的代码,会让人找不到主线。从里面给出的几个函数就可以看出来,WTL对于Windows对象浅浅的,但是有效的封装。

  template <bool t_bManaged>

  class CPenT

  {

  public:

  // Data members

     HPEN m_hPen;

  // Constructor/destructor/operators

     CPenT(HPEN hPen = NULL) : m_hPen(hPen)

     { }

   ~CPenT()

     {

        if(t_bManaged && m_hPen != NULL)

        DeleteObject();

     }

   CPenT<t_bManaged>& operator =(HPEN hPen)

     {

        Attach(hPen);

        return *this;

     }

  void Attach(HPEN hPen)

     {

        if(t_bManaged && m_hPen != NULL && m_hPen != hPen)

        ::DeleteObject(m_hPen);

        m_hPen = hPen;

     }

   HPEN Detach()

     {

       HPEN hPen = m_hPen;

       m_hPen = NULL;

       return hPen;

     }

  operator HPEN() const { return m_hPen; }

  bool IsNull() const { return (m_hPen == NULL); }

  // Create methods

     HPEN CreatePen(int nPenStyle, int nWidth, COLORREF crColor)

     {

        ATLASSERT(m_hPen == NULL);

        m_hPen = ::CreatePen(nPenStyle, nWidth, crColor);

        return m_hPen;

     }

   int GetLogPen(LOGPEN* pLogPen) const

     {

        ATLASSERT(m_hPen != NULL);

        return ::GetObject(m_hPen, sizeof(LOGPEN), pLogPen);

     }

  };

  typedef CPenT<false>   CPenHandle;

  typedef CPenT<true>    CPen;

  t_bManaged是干什么的?嗯,仅仅是用来防止对象被清除的。某些时候,我们可能需要把一个现有的对象句柄封装一下,用完了之后,有人会释放它的,我们不需要管。例如Owner Draw的时候,从lParam当中传过来的hDC。这个时候,我们就需要CDCHandle,而不是CDC了。

  WTL对于窗口的封装(确切地讲这一部分是ATL的工作)更加有意思,这个地方也显示了它和MFC最大的区别。关于这一部分,请看WTL当中实现自己的窗口一部分。

  WTL对Windows标准控件的支持:

  说到现在,如果我要使用Windows的标准控件呢?MFC有一系列的类,可以让我们很容易地使用Windows的标准控件,而SDK下面,很多时候不得不使用直接对控件的窗口句柄发送消息的做法,如果你用SDK写过带有ListView Control的程序,估计你会对一大堆的LVM_xxxx深恶痛绝,当然,你可以用WindowsX.h当中定义的一些可爱的宏,但是看起来总是有点让人感到不是那么爽。幸运的是,WTL已经像MFC那样,把所有的Windows标准控件进行了封装,并且提供了更加友好的接口函数来对控件进行操作。还是随便从头文件当中摘抄一些出来吧:

  

  // CButton - client side for a Windows BUTTON control

  template <class TBase>

  class CButtonT : public TBase

  {

  public:

  // Constructors

     CButtonT(HWND hWnd = NULL) : TBase(hWnd)

     { }

   CButtonT< TBase >& operator =(HWND hWnd)

     {

        m_hWnd = hWnd;

        return *this;

     }

   HWND Create(HWND hWndParent, ATL::_U_RECT rect = NULL,

       LPCTSTR szWindowName = NULL,

       DWORD dwStyle = 0, DWORD dwExStyle = 0,

       ATL::_U_MENUorID MenuOrID = 0U, LPVOID lpCreateParam = NULL)

     {

        return TBase::Create(GetWndClassName(), hWndParent,

        rect.m_lpRect, szWindowName, dwStyle, dwExStyle,

        MenuOrID.m_hMenu, lpCreateParam);

     }

  // Attributes

     static LPCTSTR GetWndClassName()

     {

        return _T("BUTTON");

     }

  UINT GetState() const

     {

        ATLASSERT(::IsWindow(m_hWnd));

        return (UINT)::SendMessage(m_hWnd, BM_GETSTATE, 0, 0L);

     }

  void SetState(BOOL bHighlight)

     {

        ATLASSERT(::IsWindow(m_hWnd));

        ::SendMessage(m_hWnd, BM_SETSTATE, bHighlight, 0L);

     }

  HICON GetIcon() const

     {

        ATLASSERT(::IsWindow(m_hWnd));

        return (HICON)::SendMessage(m_hWnd, BM_GETIMAGE, IMAGE_ICON, 0L);

     }

   HICON SetIcon(HICON hIcon)

     {

        ATLASSERT(::IsWindow(m_hWnd));

        return (HICON)::SendMessage(m_hWnd, BM_SETIMAGE, IMAGE_ICON,

        (LPARAM)hIcon);

     }

  // Many other functions ...

  // Operations

     void Click()

     {

        ATLASSERT(::IsWindow(m_hWnd));

        ::SendMessage(m_hWnd, BM_CLICK, 0, 0L);

     }

  };

  typedef CButtonT<ATL::CWindow>   CButton;

  这就是对Button控件的封装,仍然是“浅浅的,但是有效的”,同时不仅仅进行浅浅的封装,还有更加友好的函数名,比如SetIcon/GetIcon来替代掉BM_SET/BM_GETIMAGE消息,不需要你再去查找每种消息的wParam和lParam代表什么信息,也不再需要自己定义和填充需要的一些数据结构,调用起来更加的简单。而把基类定义成模板参数,不仅仅可以让你创建普通的Button,如果你已经设计了一种窗口,具有某些特殊的行为,比如鼠标划过的时候会变色,那么完全可以把基类设置成你的那个类,那么你所创建出来的Button,就保留了基类当中的功能,还有一个Button的默认行为。

  定义成模板的另外一个好处就是,只有你用到的才会被实例化,你完全不必要为你不需要的东西付出代价。好比你使用了一个CButton,但是仅仅用到了Create成员函数,没有用到其他的类似SetIcon的成员,这些没有用到的成员函数都不会被实例化,你的程序大小也会大幅度的得到减少。

  WTL当中实现自己的窗口:

  一个Win32的GUI Framework的重要工作除了使用C++的方式来对内建的窗口类型(Control等)进行封装之外,还有一项重要的工作,就是实现自己的窗口。

  WTL设计了一个HWND的封装类,CWindow,与对于Handle的封装类似,但是它不会进行manage,也就是说,CWindow对象离开了作用域之后,并不会造成窗口的销毁。

  但是窗口和其他的Windows对象不同的地方就在于,它是可以定制的,也就是说用户可以实现自己的窗口(你听说过有人实现自己的PEN么?),所以WTL提供了另外的一个类来完成这个任务:CWindowImpl。例如我们的那个简单的例子当中,需要做的就是让我们的窗口类从CWindowImpl派生,并传递我们自己的类名给CWindowImpl的第一个参数,就象下面这样:

  class CDemoWnd

     : public ATL::CWindowImpl<CDemoWnd, ATL::CWindow, ATL::CFrameWinTraits>

  然后就像MFC一样,通过一系列的宏,把消息映射到成员函数上。注意这里的消息处理函数不必要是virtual,这样可以大幅度的减少vtbl的大小;同时所有的消息处理函数的prototype都是相同的,所以你可以很容易地把很多的消息映射到一个函数上。

  如果我实现的窗口不是一个标准的窗口,而是某些预定义窗口的子类,比如Edit Control,我们怎么办?看到CWindowImpl的第二个模板参数了没有?这个参数,在WTL的实现当中称为TBase,其实就是表明你的这个窗口派生自哪种窗口,只要你把CWindow换成CEdit,再在类的定义内部加上一个DECLARE_WND_SUPERCLASS宏,那么你所实现出来的窗口就具有了一个CEdit的所有的特性,再加上你自己实现的一些特性了。

  关于如何实现WTL当中的窗口,由于WTL的Windowing部分其实直接使用的是ATL的东西,而这些东西决不是我这篇短文可以说清楚地,所以有兴趣的可以看一看《ATL Internals》[3]一书的第九章Windowing,里面有详细的叙述,或者参考ATL的文档。

  效率:WTL的生命线:

  WTL一直以来为人诟病的地方就是它的晦涩,为什么把很多不需要定义为模板的东西也要模板化?有人甚至讽刺:难道到处都是template关键字很cool么?其实想想,WTL/ATL是为了什么而存在的,他们的出现刚开始是为了ActiveX控件,这些在网络上传播的小程序,最大的特点就是大小和速度。同样的功能,用户的下载延迟越小,运行越快,就越容易得到用户的青睐。正是这个定位,使得WTL把效率(空间和时间)看作它的生命线。

  空间效率自然不必多说,WTL使用模板很大程度上就是为了这个,就像前面多次提到的,你不必为你没有用到的特性付出任何的空间上的代价。那么时间效率呢?

  我们先看看WTL当中用得最多的tricks:把自己当作基类的一个模板参数传递过去。比如我们刚才给出的那个例子当中,CDemoWnd从CWindowImpl继承,然后给CWindowImpl的第一个模板参数传递它本身。为什么这么做?下面我们考虑一下这种常见的情形:在基类当中提供一种行为的默认实现,子类可以对这种行为进行重写,但是我们在基类当中调用这个函数的时候,它会自动的曲调用子类的实现。也就是所谓的多态。例如下面这个例子:我们希望do_print函数由子类来实现,并且由基类调用,我们如何实现?

  首先是一种大家都熟悉的经典的多态的解决方案:虚函数。

  #include <iostream>

  using namespace std;

  struct CBase {

     virtual void do_print() { cout << "CBase" << endl; }

     void print() { cout << "print: "; do_print(); }

  };

  struct CDerive : public CBase {

     virtual void do_print() { cout << "CDerived" << endl; }

  };

  void main() {

     CDerive c;

     c.print();

  }

  这种方法有什么缺点吗?是的。虚函数的实现机制,在目前的大多数的编译器当中都是通过vtbl指针,调用的时候通过vtbl当中的index进行。这样一种运行期的多态实现方式,由于无法在编译期决定实际调用的函数,极大的限制了编译器的优化,比如对小函数的内联展开;同时,编译器需要为基类和子类生成两个虚函数表,这两个vtbl在虚函数很多的时候可能会很大,也占用了很多的无谓的空间;另一方面,即使我们只是调用了子类的虚函数,基类的虚函数仍然被编译进了我们最后的可执行文件,这些空间,再次被浪费了。追究这些缺点的根本原因:virtual是运行期的,编译器无法在编译期做出决定,它只能用最保守的方法:把所有的都保留。

  现在,我们使用模板,换一种方式来实现,注意我用粗体强调的部分:

  #include <iostream>

  using namespace std;

  template < typename TDerive >

  struct CBase {

    void do_print() { cout << "CBase" << endl; }

     void print() {

       cout << "print: ";

       TDerive*    pT = static_cast<TDerive*>(this);

       pT->do_print();

     }

  };

  struct CDerive : public CBase<CDerive> {

     void do_print() { cout << "CDerived" << endl; }

  };

  // main function remain the same

  看到我们怎样调用派生类的do_print函数了没有,我们使用可以在编译期决定的static_cast,进行一个this指针的强制转换,这样编译器很容易确定需要调用的函数就是TDerive::do_print(),而这个TDerive已经在CDerive定义的时候被指定为TDerive=CDerive,所以,不必生成vtbl,如果可能,也会进行内联展开。同时,由于CBase变成了一个template,仍然是一开始说的那个问题,只有你需要的函数才会被实例化,不需要的函数,比如CBase::do_print(),完全不需要为他们付出任何的代价。你可以反汇编一下编译出来的exe文件,看看里面有没有对于”CBase”字符串的引用就可以知道了。

  除此之外,ATL还进行了一个极具创新性的工作,关于如何进行窗口句柄和类的实例进行映射的问题。这个在MFC当中使用的是一个全局的映射表,代价当然就是一遍一遍的查找所带来的巨大的额外时间的消耗。而ATL使用的则是完全不同的一种方法,称为thunk的方法。关于这些,太复杂了,还是大家自己来看看《ATL Internal》上面的叙述吧。

  后记:

  刚刚回头读一读我的文章,发现自己写的东西真是杂乱,而且到了最后再次陷入了细节当中,关于WTL,我实在有太多的东西想说,反而显得杂乱了。最后还是用Nenad的一段话来结束我这篇小文章好了[2]

  And finally - WTL was written with a hope that developers will enjoy using it. I hope you will use it and enjoy it, too.

  是的,I am enjoying it, how about you?

  引用:

  [1]        Windows Template Libraryhttp://sourceforge.net/projects/wtl

  [2]        WTL之父Nenad Stefanovic访谈录,http://www.huihoo.com/gnu/WTL.htm,2002

  [3]        《ATL Internal》, Pearson Education, 科学出版社,2003

  [4]        A Quick MFC and WTL ComparisonKenn Scribner
       
http://www.endurasoft.com/vcd/mfcwtl.htm

  [5]        作者ftofficer < zhangc@ustc.edu >,本文的Web版本可以在作者的个人主页:
        http://202.38.73.222/~zhnagc找到。

 
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Win32程序是指使用Windows操作系统提供的API编写的应用程序。SDI(Single Document Interface)是一种界面设计模式,用于窗口应用程序中只打开一个文档的情况。 在编写Win32程序的过程中,首先需要使用C语言进行编程。C语言是一种面向过程的编程语言,它的优点是执行效率高、灵活性好,适合编写底层的系统程序。 在实现SDI模式的程序中,通常会包含一个主窗口(MainFrame)和一个文档窗口(DocumentFrame)。主窗口是整个应用程序的入口,用于显示菜单、工具栏和状态栏等用户界面元素,并负责响应用户的操作。文档窗口用于显示编辑的文档内容,并提供编辑操作,例如保存文档、复制粘贴等。 在编写程序时,需要处理窗口消息,例如鼠标点击、键盘输入等。可以使用Windows API提供的函数来处理这些消息,并根据具体的处理逻辑来实现相应的功能。例如,当用户点击保存按钮时,可以通过SendMessage函数向程序发送保存消息,并在消息处理函数中实现相应的保存操作。 此外,还可以使用Windows API提供的其他功能来增强应用程序的功能,例如创建对话框、绘制图形等。这些功能都可以根据具体的需求来调用相应的函数,从而实现相应的功能。 综上所述,编写Win32程序SDI模式是一个需要深入了解Windows API的过程,通过使用C语言和相关函数,可以实现一个具有图形界面和编辑功能的应用程序。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值