最近我一个同学在项目中使用到了duilib框架,但是之前并没有接触过,他与我讨论这方面的内容,看着官方给出的精美的例子,我对这个库有了很大的兴趣,我自己也是初学这个东东,我在网上花了不少时间来找相关的资料,但是找到的不多,官方给的文档又不全面,但是我还是找到了一些博主贡献的优秀的博文,现在我是通过博文上的讲解加上自己查看源代码的一些心得,正在艰难的前行。现在正在看的是博主Alberl在博客园中的duilib基础教程中的内容,下面的代码都是在他博客中给出代码的基础上做了一点小小的修改。点击这里跳转到对应的博客,以及博主夜雨無聲的博客,博客地址

duilib的简介

国内首个开源 的directui 界面库,它提供了一个所见即所得的开发工具——UIDesigner,它只有主框架窗口,其余的空间全部采用绘制的方式实现,所以对于控件来说没有句柄和窗口类等内容,它通过UIDesigner工具将用户定义的窗口保存在xml文件中,在创建窗口时读取xml文件中的内容,来绘制相应的控件。目前有许多界面采用duilib编写,大家可以去网上搜集相关资料。

环境的配置

首先我们去github上获取相关的源代码,这个是对应的项目地址:https://github.com/duilib/duilib
下载完后,在目录中找到一个.sln结尾的文件,使用visual studio编译器打开,打开后发现有一个duilib的项目,以及其他,其实真正有用的就是这个duilib,其余的都是官方给出的例子代码。一般只需要编译这个duilib项目就可以了,当初没注意直接点了编译全部的,结果报了一堆错误,其实都是没有对应的lib和dll文件造成的。在VS环境下有一个编译选项,如下图所示
这里写图片描述
上面有4个编译选项,最好将所有的都编译一遍,这样在对应项目的bin目录下会生成四个dll文件,这几个文件分别是debug下的UNICODE编码文件、ANSI文件以及Release版本下的UNICODE编码文件、ANSI文件。u代表unicode d代表debug。另外在lib目录下会生成对应的lib文件。
在新建的工程中,点击属性在属性对话框中选择VC++目录,在源文件,库文件,包含文件中将对应的路径添加进去,分别是项目目录和lib文件目录。如下图(刚开始有点问题所以添加的内容有点多,但是不影响正常使用):
这里写图片描述
最后可以在环境变量的Path变量中添加对应的dll路径,这样就不需要将dll文件拷贝到自己项目的exe文件所在位置处。
其实上述的环境可以不用设置,如果不设置,在编写程序包含相关路径时就需要给定完整的路径。
到此处为止,整个开发环境就已经搭建好了,剩下的就是代码的编写了。

基本的框架窗口

首先新建一个Win32类型的项目,添加主函数。然后创建一个新类,我们叫做CDuiFrameWnd,下面是类的源代码

//头文件
#include <DuiLib\UIlib.h>
using namespace DuiLib;
class CDuiFrameWnd :
    public CWindowWnd
{
public:
    CDuiFrameWnd();
    ~CDuiFrameWnd();
    virtual LPCTSTR GetWindowClassName() const;
    virtual LRESULT HandleMessage(UINT uMsg, WPARAM wParam, LPARAM lParam);
protected:
    CPaintManagerUI m_PaintManager;
};
//cpp文件
LPCTSTR CDuiFrameWnd::GetWindowClassName() const
{
    return _T("DuiFrameWnd");
}
LRESULT CDuiFrameWnd::HandleMessage(UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    LRESULT lRes = 0;
    if (WM_CLOSE == uMsg)
    {
        ::CloseWindow(m_hWnd);
        ::DestroyWindow(m_hWnd);
    }

    if (WM_DESTROY == uMsg)
    {
        ::PostQuitMessage(0);
    }
    return __super::HandleMessage(uMsg, wParam, lParam);
}

为了能够使用对应的dll文件,还需要引入对应的lib文件,我们在公共的头文件中加入如下代码

#ifdef _DEBUG
#   ifdef _UNICODE
#       pragma comment(lib, "Duilib_ud.lib")
#   else
#       pragma comment(lib, "Duilib_d.lib")
#   endif
#else
#   ifdef _UNICODE
#       pragma comment(lib, "Duilib_u.lib")
#   else
#       pragma comment(lib, "Duilib.lib")
#   endif
#endif

在主函数中的代码如下:

int APIENTRY wWinMain(_In_ HINSTANCE hInstance,
                     _In_opt_ HINSTANCE hPrevInstance,
                     _In_ LPWSTR    lpCmdLine,
                     _In_ int       nCmdShow)
{
    CPaintManagerUI::SetInstance(hInstance);
    CDuiFrameWnd duiFrame;

//#define UI_WNDSTYLE_FRAME      (WS_VISIBLE | WS_OVERLAPPEDWINDOW)
    duiFrame.Create(NULL, _T("测试"), UI_WNDSTYLE_FRAME, WS_EX_WINDOWEDGE);
    duiFrame.ShowWindow();
    CPaintManagerUI::MessageLoop();
    return 0;
}

这些代码就可以帮助我们生成基本的框架窗口,另外我们需要时刻记住的是duilib是对win32 API的封装,所以可以直接使用win32的编程方式,如果以后有不会用的地方完全可以使用win32 的API来完成相关的功能的编写。

框架的剖析

既然它能够生成单文档的框架窗口,那么代码中所做的几步基本上与用纯粹的win32 API相同,所以我们沿着这个思路来进行框架的简单剖析。
主函数中首先是代码CPaintManagerUI::SetInstance(hInstance);至于类CPaintManagerUI到底有什么作用,这个我也不太清楚,现在我还没有仔细看关于这个类的相关代码,这句话主要还是获取了进程的实例句柄。现在先不关心这个。下面的几步主要是在类CDuiFrameWnd中完成或者说在它的基类CWindowWnd中完成。

创建窗口类

主函数中的第二段代码主要完成的是类CDuiFrameWnd对象的创建,我们跟到对应的构造函数中发现它并没有做多余的操作,现在先不管它是如何构造的,它下面就是调用了类的Create函数创建了一个窗口,这个函数的代码如下:

HWND CWindowWnd::Create(HWND hwndParent, LPCTSTR pstrName, DWORD dwStyle, DWORD dwExStyle, int x, int y, int cx, int cy, HMENU hMenu)
{
    if( GetSuperClassName() != NULL && !RegisterSuperclass() ) return NULL;
    if( GetSuperClassName() == NULL && !RegisterWindowClass() ) return NULL;
    m_hWnd = ::CreateWindowEx(dwExStyle, GetWindowClassName(), pstrName, dwStyle, x, y, cx, cy, hwndParent, hMenu, CPaintManagerUI::GetInstance(), this);
    ASSERT(m_hWnd!=NULL);
    return m_hWnd;
}

我们主要来看第二个if中的代码,首先获得了父窗口的字符串为NULL,然后执行RegisterWindowClass,我们进一步跟到RegisterWindowClass中,它的代码如下:

bool CWindowWnd::RegisterWindowClass()
{
    WNDCLASS wc = { 0 };
    wc.style = GetClassStyle();
    wc.cbClsExtra = 0;
    wc.cbWndExtra = 0;
    wc.hIcon = NULL;
    wc.lpfnWndProc = CWindowWnd::__WndProc;
    wc.hInstance = CPaintManagerUI::GetInstance(); //之前设置的实例句柄在这个地方使用
    wc.hCursor = ::LoadCursor(NULL, IDC_ARROW);
    wc.hbrBackground = NULL;
    wc.lpszMenuName  = NULL;
    wc.lpszClassName = GetWindowClassName();
    ATOM ret = ::RegisterClass(&wc);
    ASSERT(ret!=NULL || ::GetLastError()==ERROR_CLASS_ALREADY_EXISTS);
    return ret != NULL || ::GetLastError() == ERROR_CLASS_ALREADY_EXISTS;
}

我们发现首先进行的是窗口类的创建,在创建窗口类时主要关心的是窗口类的lpfnWndProc成员和lpszClassName 。lpszClassName 调用了函数GetWindowClassName,这个函数我们在派生类中进行了重写,所以根据多态它会调用派生类的GetWindowClassName函数,将我们给定的字符串作为窗口类的类名

注册窗口类

从上面的代码可以看出注册的代码也是放在RegisterWindowClass中。在最后调用了RegisterClass函数完成了注册。

创建窗口

当RegisterWindowClass执行完成后,会接着执行下面的代码,也就是 m_hWnd = ::CreateWindowEx(dwExStyle, GetWindowClassName(), pstrName, dwStyle, x, y, cx, cy, hwndParent, hMenu, CPaintManagerUI::GetInstance(), this);完成创建窗口的任务。

显示窗口

Create函数执行完成后,会接着执行下面的duiFrame.ShowWindow();我们跟到这个函数中,函数代码如下:

void CWindowWnd::ShowWindow(bool bShow /*= true*/, bool bTakeFocus /*= false*/)
{
    ASSERT(::IsWindow(m_hWnd));
    if( !::IsWindow(m_hWnd) ) return;
    ::ShowWindow(m_hWnd, bShow ? (bTakeFocus ? SW_SHOWNORMAL : SW_SHOWNOACTIVATE) : SW_HIDE);
}

函数ShowWindow默认传入参数为bShow = true bTakeFocus = false;在最后进行ShowWindow函数的调用时,根据bShow和bTakeFocus来进行值得传入,根据代码我们发现,当不传入参数时调用的其实是这样的代码ShowWindow(m_hWnd, SW_SHOWNOACTIVATE);

消息循环

消息循环其实是通过代码CPaintManagerUI::MessageLoop();完成,我们跟到MessageLoop函数中看

    MSG msg = { 0 };
    while( ::GetMessage(&msg, NULL, 0, 0) ) {
        if( !CPaintManagerUI::TranslateMessage(&msg) ) {
            ::TranslateMessage(&msg);
            ::DispatchMessage(&msg);
        }
    }

在这个函数中完成了消息循环。

回调函数

上面我们留了一个lpfnWndProc函数指针没有说,现在来说明这个部分,跟进到对应的构造函数中,发现类本身不做任何操作,但是父类的构造函数进行了相关的初始化操作,下面是对应的代码

CWindowWnd::CWindowWnd() : m_hWnd(NULL), m_OldWndProc(::DefWindowProc), m_bSubclassed(false)
{
}

这样就将lpfnWndProc指向了__WndProc,用于处理默认的消息。
这是一个静态的处理函数,下面是它的代码:

LRESULT CALLBACK CWindowWnd::__WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    CWindowWnd* pThis = NULL;
    if( uMsg == WM_NCCREATE ) {
        LPCREATESTRUCT lpcs = reinterpret_cast<LPCREATESTRUCT>(lParam);
        pThis = static_cast<CWindowWnd*>(lpcs->lpCreateParams);
        pThis->m_hWnd = hWnd;
        //当开始创建窗口将窗口类对象的指针放入到对应的GWLP_USERDATA字段中
        ::SetWindowLongPtr(hWnd, GWLP_USERDATA, reinterpret_cast<LPARAM>(pThis));
    } 
    else {
    //取出窗口类对象的指针
        pThis = reinterpret_cast<CWindowWnd*>(::GetWindowLongPtr(hWnd, GWLP_USERDATA));
        if( uMsg == WM_NCDESTROY && pThis != NULL ) {
            LRESULT lRes = ::CallWindowProc(pThis->m_OldWndProc, hWnd, uMsg, wParam, lParam);
            ::SetWindowLongPtr(pThis->m_hWnd, GWLP_USERDATA, 0L);
            if( pThis->m_bSubclassed ) pThis->Unsubclass();
            pThis->m_hWnd = NULL;
            pThis->OnFinalMessage(hWnd);
            return lRes;
        }
    }
    if( pThis != NULL ) {
        return pThis->HandleMessage(uMsg, wParam, lParam);
    } 
    else {
        return ::DefWindowProc(hWnd, uMsg, wParam, lParam);
    }
}

上述的代码,在创建窗口时将窗口类对象指针存入到对应的位置便于在其他位置取出并使用。通过return pThis->HandleMessage(uMsg, wParam, lParam);这句话调用的具体对象的HandleMessage,我们在对应的派生类中定义了相应的虚函数,所以根据多态它会调用我们重写的虚函数来处理具体消息,至于我们不关心的消息,它会调用LRESULT lRes = ::CallWindowProc(pThis->m_OldWndProc, hWnd, uMsg, wParam, lParam);或者DefWindowProc,通过对基类的构造函数的查看,我们发现其实m_OldWndProc就是DefWindowProc。

总结

上面我们说明了duilib的基本框架,下面来总结一下:
1. CPaintManagerUI::SetInstance(hInstance);设置进程的实例句柄,这个值会在注册窗口类时使用
2. 在CWindowWnd类中由Create函数完成窗口类的创建于注册,以及窗口的创建工作
3. CWindowWnd类中的ShowWindow函数用于显示窗口
4. 消息循环由CPaintManagerUI::MessageLoop();代码完成
5. 最后需要重写MessageHandle函数用于处理我们感兴趣的消息。并且在最后需要调用基类的MessageHandle函数,主要是为了调用DefWindowProc处理我们不感兴趣的消息。

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐