【问题标题】:How to encapsulate a WinAPI application into a C++ class如何将 WinAPI 应用程序封装到 C++ 类中
【发布时间】:2011-03-03 11:58:15
【问题描述】:

有一个简单的 WinAPI 应用程序。它目前所做的只是:

  • 注册一个窗口类
  • 使用菜单注册托盘图标
  • 在注册表中创建一个值以自动启动
  • 最后,它使用互斥体检查它是否是唯一的

由于我习惯于主要使用 C++ 编写代码,并且不允许使用 MFC,因此我不得不以某种方式将其封装到 C++ 类中。到目前为止,我想出了这样一个设计:

  • 有一个代表应用程序的类
  • 它保留所有 wndclass、hinstance 等变量,其中 hinstance 作为构造函数参数以及 icmdshow 和其他参数传递(参见 WinMain 原型)
  • 具有注册窗口类、托盘图标、注册表信息的功能
  • 它将消息循环封装在一个函数中

在 WinMain 中,完成以下操作:

Application app(hInstance, szCmdLIne, iCmdShow);
return app.exec();

并且构造函数执行以下操作:

registerClass();
registerTray();
registerAutostart();

到目前为止一切顺利。现在的问题是:如何创建窗口过程(必须是静态的,因为它是指向函数的 c 风格指针)并跟踪应用程序对象是什么,也就是说,保持指向应用程序的指针。

主要问题是:这是通常的做法吗?我是不是把事情复杂化了?可以将 hInstance 作为参数传递给 Application 构造函数吗? WndProc 在哪里?

也许 WndProc 应该在类之外并且应用程序指针是全局的?然后 WndProc 调用 Application 方法来响应各种事件。

还有另一种可能的解决方案:将应用程序类设为单例。然后从 WndProc 中获取该对象的句柄就很简单了。

【问题讨论】:

  • 这就像你正在重新实现 Qt 已经做得很好的东西。我想使用外部库不是一种选择?
  • 不,这是一个大学项目,我自己也是 Qt 的忠实粉丝 :) 是的,真的,这里的事情比 Qt 复杂得多,但你迟早必须学习一切...
  • 我同意。我也是这样学的。但是,您仍然可以观看Qt 源代码,看看他们是如何做到的。不过这可能很复杂。
  • 如果应用程序作为 C 程序运行良好,我看不出有任何理由将其“转换”为 c++。我可以看到很多不这样做的原因。

标签: c++ winapi


【解决方案1】:

答案是 SetWindowLongPtr。它允许您将 void* 与给定的 hWnd 相关联。然后,在 WndProc 中,您只需提取 void*、强制转换并调用成员方法。问题解决。 SetWindowLongPtr有一些起伏,必须调用其他函数才能看到效果或某些BS,并且Windows会在CreateWindowEx返回之前发送消息,因此您必须准备GetWindowLongPtr(hWnd,GWL_USERDATA)返回NULL。

这当然意味着对于一个给定的 WindowProc,所有使用它的实例都必须有一个公共接口,因为你不能用 void* 做很多事情。

而且,是的,可以将 HINSTANCE 传递给 App 构造函数。我见过一些样本会做一些奇怪的事情来避免这种情况,但我自己从来没有让它起作用。

编辑: 不要将 Get/SetWindowLong 与 Get/SetWindowLongPtr 混淆。 Get/SetWindowLong 已弃用且不安全。

【讨论】:

  • 这正是我一直在寻找的。那么 WndProc 应该在应用程序类之外,这是首选的设计吗? (一般来说,使 C 风格的 API 与 C++ 设计一起工作可能看起来有缺陷并且令人头疼,但我必须这样做)......
  • 您可以将 WndProc 作为类的静态成员。通过 SetWindowLongPtr 传递 this,在 WndProc 中检索它,然后分派给其他类成员(然后它们也可以是虚拟的)。问题解决了。
  • 即使 MFC 也不再使用它,因为它不安全!如果其他人使用 SetWindowLongPtr 并将您的 this 指针更改为某个随机值怎么办?
  • MFC 使用全局映射 <HWND, CWnd *>(大致)。 ATL 使用为每个窗口动态生成的 WNDPROC-Stub,并传递实例指针而不是 HWND。无论如何,如果这三种方法(包括 setWindowLongPtr)都可以,但后两种方法也适用于您不控制 WNDCLASS 的窗口(例如当您将其他人的 EDIT 控件子类化时)。
  • Lorenzo,这就是你封装 HWND 的原因。我的意思是,如果其他人取消引用 NULL 指针怎么办?如果其他人有缓冲区溢出怎么办?您无法强制他人代码的安全性,他们有责任不要以愚蠢的方式弄乱您的代码。
【解决方案2】:

不要按照提示使用Get/SetWindowLongPtr 来存储您的this 指针,因为这是一个巨大的安全漏洞!您只需使用映射将 HWND 与指向类实例的指针相关联。您可以使用 STL 中的 <map> 类。

顺便说一句,你可以在那里找到关于这个话题的很好的讨论:http://blogs.msdn.com/b/oldnewthing/archive/2005/04/22/410773.aspx

【讨论】:

  • 好点。攻击者可以通过从另一个进程中检索此指针来翻阅找到的数据。
  • 感谢您的链接;但是,如果我做对了,该代码也使用 SetWindowLogPtr。
  • 当然:您必须关注 cmets 中的有趣讨论。
  • 在cmets中Raymond Chen不关心其他人设置GWLP_USERDATA的讨论? (除非编写一个供其他人使用的控件。)在看到 ATL 为实现这种情况而采用的(诚然,对最终用户非常干净)可怕的 thunking 诡计之后,我决定也采用 SetWindowLongPtr 路线。最初我和@Lorenzo 有同样的担忧,但实际上——除了你的应用程序崩溃之外会发生什么?一个可以使用“this”指针搜索的攻击者已经可以访问你的地址空间了!
  • 您说的完全正确,已经有权访问您的地址空间的攻击者可能不会更改您的 this 指针。但是,即使恶意代码可能会使您的代码崩溃,这仍然可能导致拒绝服务形式的安全问题。我同意 ATL 方式是可怕的,但我们不得不承认它是类用户最干净的路径。
【解决方案3】:

你可以扩展这个类(我 用于此处的答案)如您所愿, 取决于你想要什么消息 处理。

#pragma once 

#include <windows.h> 
#include <process.h> 
#include <iostream> 

using namespace std; 

static const char *g_AppName  = "Test"; 

class CMyWindow 
{ 
    HWND  _hWnd; 
    int _width; 
    int _height; 
public: 
    CMyWindow(const int width,const int height):_hWnd(NULL),_width(width),_height(height) 
    { 
        _beginthread( &CMyWindow::thread_entry, 0, this); 
    } 

    ~CMyWindow(void) 
    { 
        SendMessage(_hWnd, WM_CLOSE, NULL, NULL); 
    } 


private: 
    static void thread_entry(void * p_userdata) 
    { 
        CMyWindow * p_win = static_cast<CMyWindow*> (p_userdata); 
        p_win->create_window(); 
        p_win->message_loop(); 
    } 

    void create_window() 
    { 
        WNDCLASSEX wcex; 

        wcex.cbSize             = sizeof(WNDCLASSEX); 
        wcex.style              = CS_HREDRAW | CS_VREDRAW; 
        wcex.lpfnWndProc    = &CMyWindow::WindowProc; 
        wcex.cbClsExtra         = 0; 
        wcex.cbWndExtra         = 0; 
        wcex.hInstance          = GetModuleHandle(NULL); 
        wcex.hIcon              = LoadIcon(NULL, IDI_APPLICATION); 
        wcex.hCursor            = LoadCursor(NULL, IDC_ARROW); 
        wcex.hbrBackground  = (HBRUSH)(COLOR_WINDOW+1); 
        wcex.lpszMenuName   = NULL; 
        wcex.lpszClassName  = g_AppName; 
        wcex.hIconSm            = LoadIcon(NULL, IDI_APPLICATION); 

        RegisterClassEx(&wcex); 

        _hWnd = CreateWindow(g_AppName, g_AppName, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, NULL, NULL, GetModuleHandle(NULL), NULL); 

        ShowWindow(_hWnd, SW_SHOWDEFAULT); 
        UpdateWindow(_hWnd); 
    } 

    void message_loop() 
    { 
        MSG msg = {0}; 

        while (GetMessage(&msg, NULL, 0, 0)) 
        { 
            if(msg.message == WM_QUIT) 
            { 
                break; 
            } 

            TranslateMessage(&msg); 
            DispatchMessage(&msg); 
        } 
    } 

    static LRESULT WINAPI WindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) 
    { 
        switch(uMsg) 
        { 
        case WM_DESTROY: 
            PostQuitMessage(0); 
            return 0; 
        case WM_POWERBROADCAST: 
            { 
                //power management code here 
            } 

        } 

        return DefWindowProc(hWnd, uMsg, wParam, lParam); 
    } 
}; 

这是一个最小的引导程序:

int APIENTRY _tWinMain(HINSTANCE hInstance,
                     HINSTANCE hPrevInstance,
                     LPTSTR    lpCmdLine,
                     int       nCmdShow)
{

    CMyWindow t(640,480);

    Sleep(10000);

    return 0;
}

【讨论】:

  • WinMain 在哪里?还是您只是在主目录中创建一个 CMyWindow 对象?而且,最重要的是,hInstance 在哪里? (抱歉,我还不是 WinAPI 方面的专家,我只是在阅读 Petzold 的书,我还没有看到任何类似的内容)
  • 这个 sn-p 只负责创建窗口并提供WndProc()。您创建自己的 WinMain,创建 HINSTANCE,然后实例化此类。
  • 您可以通过调用 GetModuleHandle(NULL) 来检索可执行文件的 HINSTANCE。
【解决方案4】:

我决定将其设为单例,因为它是主要的应用程序类,并且在程序中拥有该类的单个实例没有问题。

现在我想再问一个相关的问题:假设我有一个偏好对话框。我将对话框创建为资源,然后给它一个过程,并在过程中创建一个控制器对象。这样做对吗?

【讨论】:

  • 所以你做单例的理由是“没问题”?一个理智的程序员会使用一个单例如果它是有益的,而不仅仅是“它现在和现在没有害处”。至少 99.9% 的情况下,单身人士都是一个糟糕的主意。
  • 是的,它们是,但是在这种情况下,你看,单例是唯一不处理 Set/GetWindowLongPtr 函数的解决方案,所以它是有益的。
猜你喜欢
  • 1970-01-01
  • 2018-11-06
  • 1970-01-01
  • 2020-10-17
  • 1970-01-01
  • 2017-01-18
  • 1970-01-01
  • 2014-06-19
  • 2015-04-15
相关资源
最近更新 更多