在一个类中创建Win32 C++窗口和过程

14

前置文字/问题

我正在尝试制作一个相当简单的工具来帮助调试变量值。我希望它完全包含在类中。最终产品是我可以在类中使用像ShowThisValue(任何内容)的函数。

我遇到的问题是,如果可能的话,我无法想出将该过程放在类内的方法。以下是短版本及其问题。

-代码已于11/29/13再次更新- -我现在将其放在自己的项目中。

viewvars TEST; // global
TEST.CreateTestWindow(hThisInstance); // in WinMain() right before ShowWindow(hwnd, nFunsterStil);

[viewvars.h] 整个更新的

class viewvars {

private:
    HWND hWindow;            // the window, a pointer to
    LRESULT WindowProc(UINT message, WPARAM wParam, LPARAM lParam);        
    static LRESULT CALLBACK ThisWindowProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam);

public:        
    viewvars(); // blank constructor
    int CreateTestWindow(HINSTANCE hInst);

};

// blank constructor
viewvars::viewvars() {}



// create the window
int viewvars::CreateTestWindow(HINSTANCE hInst) {

// variables
char thisClassName[] = "viewVars";     
MSG msg;       
WNDCLASS wincl;  


// check for class info and modify the info
if (!GetClassInfo(hInst, thisClassName, &wincl)) {
    wincl.style = 0;
    wincl.hInstance = hInst;
    wincl.lpszClassName = thisClassName;
    wincl.lpfnWndProc = &ThisWindowProc;
    wincl.cbClsExtra = 0;
    wincl.cbWndExtra = 0;
    wincl.hIcon = NULL;
    wincl.hCursor = NULL;
    wincl.hbrBackground = (HBRUSH)COLOR_BTNSHADOW;
    wincl.lpszMenuName = NULL;

    if (RegisterClass(&wincl) == 0) {
        MessageBox(NULL,"The window class failed to register.","Error",0); 
        return -1;
    }
}  

// create window
hWindow = CreateWindow(thisClassName, "Test", WS_POPUP | WS_CLIPCHILDREN, 10, 10, 200, 200, NULL, NULL, hInst, this);
if (hWindow == NULL) { 
    MessageBox(NULL,"Problem creating the window.","Error",0); 
    return -1; 
} 

// show window
ShowWindow(hWindow, TRUE);

// message loop
while (GetMessage(&msg, hWindow, 0, 0))
{
    TranslateMessage(&msg);  
    DispatchMessage(&msg); 
}    

// then quit window?
DestroyWindow(hWindow);
hWindow = NULL;

return msg.wParam;
}   


// window proc
LRESULT CALLBACK viewvars::ThisWindowProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) {

MessageBox(NULL,"Has it gone this far?","Bench",0); 

// variable
viewvars *view;

// ????
if (message == WM_NCCREATE) {
    CREATESTRUCT *cs = (CREATESTRUCT*)lParam; 
    view = (viewvars*) cs->lpCreateParams;

    SetLastError(0);
    if (SetWindowLongPtr(hwnd, GWL_USERDATA, (LONG_PTR) view) == 0) {
        if (GetLastError() != 0) {
            MessageBox(NULL,"There has been an error near here.","Error",0); 
            return FALSE;
        }
    }
}
else {
    view = (viewvars*) GetWindowLongPtr(hwnd, GWL_USERDATA);
}


if (view) return view->WindowProc(message, wParam, lParam);

MessageBox(NULL,"If shown, the above statement did not return, and the statement below did.","Error",0); 

return DefWindowProc(hwnd, message, wParam, lParam);
}


LRESULT viewvars::WindowProc(UINT message, WPARAM wParam, LPARAM lParam) {
// you can access non-static members in here...
MessageBox(NULL,"Made it to window proc.","Error",0);

switch (message)
{
    case WM_PAINT: 
        return 0;
    break;
    case WM_DESTROY:
        PostQuitMessage(0);       
        return 0;
    break;
    default:
        MessageBox(NULL,"DefWindowProc Returned.","Error",0);
        return DefWindowProc(hWindow, message, wParam, lParam);
    break;
}
}

这些消息框以以下顺序出现:

  • 已经到了这一步吗?
  • 到达窗口处理程序
  • DefWindowProc返回
  • 已经到了这一步吗? // 重复的?
  • 到达窗口处理程序
  • DefWindowProc返回
  • 创建窗口时出现问题

感谢迄今为止的帮助。您知道问题可能在哪里吗?enter image description here


将WindowProc方法改为静态方法。 - thang
2
可以使用动态 thunking 技术实现:https://dev59.com/QnXYa4cB1Zd3GeqP_N5H#18326813 - noseratio - open to work
2
不要为每个消息调用MessageBox。这会创建重入性和普遍混乱。此外,有些消息不应该产生收益。产生收益会引起问题。 - Raymond Chen
没错,我只是用那些消息来看它在做什么。一开始它就没起作用,所以我只是想看看它在做什么。就这个东西而言,它相当于逐步调试每个步骤。我可以打开控制台并使用cout而不是MessageBox,得到完全相同的结果。忽略所有消息,Create Window没有起作用... - Evan Carslake
您没有设置wincl.cbSize。 - user253751
5个回答

17

要将非静态类方法用作窗口过程需要动态分配thunk,这是一种高级技术,我不会在这里深入讲解。

另一种方法是将类方法声明为static,然后它就可以作为窗口过程工作。当然,作为static方法,它不能再访问非静态类成员而不使用实例指针。为了获得指针,您可以让该类将其this指针传递给CreateWindow/Ex()lpParam参数,然后窗口过程可以从WM_NCCREATE消息中提取该指针并使用SetWindowLong/Ptr(GWL_USERDATA)将其存储在窗口中。之后,后续的消息可以使用GetWindowLong/Ptr(GWL_USERDATA)检索该指针,从而能够访问该对象的非静态成员。例如:

class viewvars
{
private:
    HWND hWindow;
    LRESULT WindowProc(UINT message, WPARAM wParam, LPARAM lParam);

    static LRESULT CALLBACK ThisWindowProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam);
public:
    int CreateTestWindow(HINSTANCE hInst);
};

int viewvars::CreateTestWindow(HINSTANCE hInst)
{ 
    WNDCLASS wincl;

    if (!GetClassInfo(hInst, thisClassName, &wincl))
    {
        ...
        wincl.hInstance = hInst;
        wincl.lpszClassName = thisClassName;
        wincl.lpfnWndProc = &ThisWindowProc;

        if (RegisterClass(&wincl) == 0)
            return -1;
    }

    hWindow = CreateWindow(..., hInst, this);
    if (hWindow == NULL)
        return -1;

    ...

    MSG msg;
    while (GetMessage(&msg, hWindow, 0, 0))
    {
        TranslateMessage(&msg);  
        DispatchMessage(&msg); 
    }

    DestroyWindow(hWindow);
    hWindow = NULL;

    return msg.wParam;
}

LRESULT CALLBACK viewvars::ThisWindowProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    viewvars *view;

    if (message == WM_NCCREATE)
    {
        CREATESTRUCT *cs = (CREATESTRUCT*) lParam; 
        view = (viewvars*) cs->lpCreateParams;

        SetLastError(0);
        if (SetWindowLongPtr(hwnd, GWL_USERDATA, (LONG_PTR) view) == 0)
        {
            if (GetLastError() != 0)
                return FALSE;
        }
    }
    else
    {
        view = (viewvars*) GetWindowLongPtr(hwnd, GWL_USERDATA);
    }

    if (view)
        return view->WindowProc(message, wParam, lParam);

    return DefWindowProc(hwnd, message, wParam, lParam);
}

LRESULT viewvars::WindowProc(UINT message, WPARAM wParam, LPARAM lParam)
{
    // you can access non-static members in here...

    switch (message)
    {
        case WM_PAINT: 
            return 0;

        case WM_DESTROY:
            PostQuitMessage(0);       
            return 0;

        default:
            return DefWindowProc(hWindow, message, wParam, lParam);
    }
}

谢谢,我认为这会起作用。但是现在我有一个问题,它卡在注册类部分。我的注册类段落与您发布的相同。我需要更改或添加什么吗?我以前没有注册过它。我还有几个问题。所以当它说访问成员信息时,它是否意味着类中的公共/私有变量?那我添加的其他函数呢?谢谢。 - Evan Carslake
你是否填写了所有的WNDCLASS成员,而不仅仅是我展示的几个?RegisterClass()挂起是不寻常的。是的,我指的是所有类成员(变量、方法等),无论是公共的还是私有的,都没有关系(static过程本身就是一个成员,因此它可以访问所有成员)。 - Remy Lebeau
@EvanCarslake:不要在注释中放太多代码。请编辑您的原始问题以显示您的代码。并且请解释您实际遇到的问题,报告了哪些错误,采取了哪些步骤来进行故障排除(您是否通过调试器逐步执行代码?您是否检查过 WM_NCCREATE 是否在失败之前被接收到?等等)。 - Remy Lebeau
谢谢,我已经更新了原始帖子,WM_NCCREATE在失败之前被调用两次... - Evan Carslake
@EvenCarslake:在同一个窗口上不应该多次收到WM_NCCREATE消息。它只会发送一次给每个窗口。 - Remy Lebeau
显示剩余6条评论

11

主消息循环不应该在你的类中,特别是不应该在一个"CreateTestWindow"函数中,因为直到你的线程接收到使GetMessage返回0的WM_QUIT消息之前,你将无法从该函数中返回。

这是您的viewvars类的简单实现。关键点:

  • 窗口过程是静态成员。
  • 通过使用GWLP_USERDATA来链接窗口过程和对象。请参见SetWindowLongPtr
  • 如果窗口仍然存在,则类DTOR会破坏窗口。 WM_DESTROY消息将HWND成员设置为0。
  • 向类添加OnMsgXXX方法很简单:声明/定义它们,然后只需使用存储在GWLP_USERDATA中的'this'指针从WindowProc调用它们即可。

编辑:

  • 根据陈先生的建议,在WM_NCCREATE中提前将HWND绑定到对象,以便允许在窗口创建期间将消息处理程序作为方法进行绑定

我更改了创建样式,以显示窗口并能够移动它。

// VIEWVARS.H
class viewvars {

public:
    static viewvars* CreateTestWindow( HINSTANCE hInstance );
    viewvars() : m_hWnd( 0 ) {}
    ~viewvars();

private:
    static LRESULT CALLBACK WindowProc( HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam );
    static const char * m_pszClassName;
    HWND m_hWnd;

};

// VIEWVARS.CPP
#include "viewvars.h"

const char * viewvars::m_pszClassName = "viewvars";

viewvars * viewvars::CreateTestWindow( HINSTANCE hInst ) {

    WNDCLASS wincl;
    if (!GetClassInfo(hInst, m_pszClassName, &wincl)) {
        wincl.style = 0;
        wincl.hInstance = hInst;
        wincl.lpszClassName = m_pszClassName;
        wincl.lpfnWndProc = WindowProc;
        wincl.cbClsExtra = 0;
        wincl.cbWndExtra = 0;
        wincl.hIcon = NULL;
        wincl.hCursor = NULL;
        wincl.hbrBackground = (HBRUSH)(COLOR_BTNFACE+1);
        wincl.lpszMenuName = NULL;
        if (RegisterClass(&wincl) == 0) {
            MessageBox(NULL,"The window class failed to register.","Error",0);
            return 0;
        }
    }

    viewvars * pviewvars = new viewvars;
    HWND hWnd = CreateWindow( m_pszClassName, "Test", WS_VISIBLE | WS_OVERLAPPED, 50, 50, 200, 200, NULL, NULL, hInst, pviewvars );
    if ( hWnd == NULL ) {
        delete pviewvars;
        MessageBox(NULL,"Problem creating the window.","Error",0); 
        return 0; 
    }

    return pviewvars;

}

 LRESULT CALLBACK viewvars::WindowProc( HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam ) {

    switch ( uMsg ) {

        case WM_NCCREATE: {
            CREATESTRUCT * pcs = (CREATESTRUCT*)lParam;
            viewvars * pviewvars = (viewvars*)pcs->lpCreateParams;
            pviewvars->m_hWnd = hwnd;
            SetWindowLongPtr( hwnd, GWLP_USERDATA, (LONG)pcs->lpCreateParams );
            return TRUE;
        }

        case WM_DESTROY: {
            viewvars * pviewvars = (viewvars *)GetWindowLongPtr( hwnd, GWLP_USERDATA );
            if ( pviewvars ) pviewvars->m_hWnd = 0;
            break;
        }

        default:
            return DefWindowProc( hwnd, uMsg, wParam, lParam );

    }

    return 0;

}

viewvars::~viewvars() {
    if ( m_hWnd ) DestroyWindow( m_hWnd );
}

最后,这是一个“主”示例,但请注意,这里没有结束过程的方式。这应该由常规代码(另一个窗口)来处理。

// MAIN.CPP
#include <Windows.h>
#include "viewvars.h"

int APIENTRY WinMain(HINSTANCE hInstance,
                     HINSTANCE hPrevInstance,
                     LPTSTR    lpCmdLine,
                     int       nCmdShow)
{
    viewvars * pviewvars = viewvars::CreateTestWindow( hInstance );
    if ( pviewvars == 0 ) return 0;

    BOOL bRet;
    MSG msg;
    while( (bRet = GetMessage( &msg, 0, 0, 0 )) != 0)
    { 
        if (bRet == -1)
        {
            // handle the error and possibly exit
        }
        else
        {
            TranslateMessage(&msg); 
            DispatchMessage(&msg); 
        }
    }

    delete pviewvars;

    return 0;

}

现在它有点更有意义了,那是一个可行的例子。谢谢。 - Evan Carslake
感谢您的付出。祝您在“viewvars”剩余的工作顺利! - manuell
5
请注意,由于在 CreateWindow 返回之前您尚未设置 GWLP_USERDATA,这意味着您无法自定义作为窗口创建一部分发送的消息的行为。也许这并不重要,但我提出来是因为它将来可能会对您产生影响。 - Raymond Chen
@RaymondChen 谢谢。确实。已更新。 - manuell
1
在WM_NCCREATE消息中,在return之前添加SetWindowPos(hwnd,0,0,0,0,0,SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER);以防止窗口首次出现时的背景闪烁。 - NickSoft

3

很遗憾,将实例方法用作WndProc的C样式回调函数不起作用。至少没有直接的方式。

之所以不能这样做是因为实例方法需要传递this指针(指向一个实例),而调用WndProc的代码不能正确设置它。Win32 API最初是为C设计的,因此这是您必须使用一些解决方法的领域之一。

解决此问题的一种方法是创建一个静态方法作为窗口过程并将消息分派到您的类实例。类实例必须被注册(读取并添加到静态集合中),以便静态方法知道将WndProc消息分派到哪个实例。实例将在构造函数中向静态调度程序注册自己,并在析构函数中删除自己。

当然,所有的注册、注销和分派开销只有在您的WndProc处理程序需要调用其他实例成员函数或访问成员变量时才是必要的。否则,您可以将其设置为静态,并且完成了。


您不需要静态集合来保存类实例指针。当类创建其窗口时,它可以将其“this”指针传递给“CreateWindow/Ex()”函数的“lpParam”参数,然后窗口过程可以从“WM_NCCREATE”消息中提取该指针,并使用“SetWindowLong/Ptr(GWL_USERDATA)”将其存储在窗口中。之后,后续消息可以使用“GetWindowLong/Ptr(GWL_USERDATA)”检索该指针,并根据需要调用非静态方法。 - Remy Lebeau
@RemyLebeau - 很好的观点!我已经很久没有处理这个问题了,所以我主要是根据旧的记忆来处理的。 - Mike Dinescu

2

您的窗口程序在CreateWindow期间被调用。您将hWindow传递给DefWindowProc,但是hWindow只有在CreateWindow返回后才设置 - 因此您向DefWindowProc传递了一个垃圾窗口句柄。

我没有找到一种好的方法来解决这个问题。您可以通过更改WindowProc来在窗口过程中设置hWindow:

LRESULT WindowProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)

(添加hwnd参数),将调用更改为:

return view->WindowProc(hwnd, message, wParam, lParam);

今日免费次数已满, 请开通会员/明日再来
hWindow = NULL;
hWindow = CreateWindow(..., hInst, this);
if (hWindow == NULL)
    return -1;

第一个任务是确保hWindow已经初始化;第二个任务是在调用窗口过程后,如果CreateWindow失败,则需要添加以下内容:

在WindowProc开头添加:

if(!this->hWindow)
    this->hWindow = hwnd;

你说得对,那似乎是问题所在。我仔细查看了一遍,但没有找到可以设置hWindow的地方。你有任何想法在哪里可以设置它吗?谢谢。 - Evan Carslake
1
我也没有看到一个好的解决方案。也许可以将HWND作为第一个参数传递给WindowProc函数。(一旦hWindow被初始化,这将是多余的)。 - user253751
我尝试了一下,但由于静态原因出现了错误。我开始觉得如果这样做是可能的,那也只是勉强可能。我还看了一个(10年前的)Win32 C包装类,结果发现有无尽的内存泄漏(正如用户所描述的)。代码与我的半相似。嗯... - Evan Carslake
1
我更新了我的答案,并附上了所需的代码更改以实现我的解决方案。 - user253751
谢谢,我已经半成功了。一开始窗口会弹出来,但会进入某种无限循环,整个窗口和关闭按钮都会闪烁。无论如何,现在我遇到了执行函数的问题。在主文件中,没有任何东西通过CreateWindow()/ ShowWindow()传递给主窗口。`hwnd = CreateWindowEx (0, szClassName, "Windows App", WS_OVERLAPPEDWINDOW, ..., HWND_DESKTOP, NULL, hThisInstance, NULL);` `ShowWindow (hwnd, nFunsterStil); ` `TEST.CreateTestWindow(hThisInstance); `主窗口显示,但从未到达TEST。有什么想法吗?谢谢。 - Evan Carslake
我认为创建两个窗口可能是最后的问题。我添加了一张图片来准确展示我想要做的事情。我只能显示一个窗口(要么这个,要么那个),当我关闭它时,程序就会结束。 - Evan Carslake

-1

通过调试器逐步执行代码。当您到达该行时

    MessageBox(NULL,"DefWindowProc Returned.","Error",0);
    return DefWindowProc(hWindow, message, wParam, lParam);

你会看到一些错误: hWindow 是垃圾值。你正在使用未初始化的变量。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接