Funkce WndProc nebo DlgProc v třídě

Z nejrůznějších důvodů je někdy potřeba napsat třídu v C++ tak, aby obsahovala funkci pro obsluhu zpráv Windows - WndProc() nebo DlgProc(). Tímto případem může být např. subclassování okna bez "asistence" MFC, nebo nutnost napsat třídu, představující okno, aniž by vůbec knihovna MFC byla použita. Tento článek pomůže všem, kteří s tímto úkolem mají problémy.

Co je to WndProc ?

Jenom ve stručnosti připomenu, že funkce WndProc() (popř. DlgProc()) slouží k obsluze zpráv zasílaných systémem Windows každému oknu. Celá architektura oken ve Windows je postavená na tomto posílání zpráv. Programování uživatelského prostředí ve Windows je tedy událostně řízeným programováním. Jestliže nastane nějaká událost, která může mít vliv na chování okna (např. stisk tlačítka myši, stlačení klávesy, změna mapování disků, změna rozlišení obrazovky), systém pošle oknu, kterého se to týká, příslušnou událost. Pod pojmem "pošle událost oknu" se rozumí zavolání funkce, která je nastavena pro obsluhu událostí. Této funkci budeme říkat WndProc().

Jak Windows určí, která funkce obsluhuje zprávy okna ?

Funkce WndProc() je většinou specifická pro všechna okna dané třídy. Při registraci třídy je ukazatel na funkci jedním z členů struktury WNDCLASSEX, která je předána jako parametr funkci RegisterClassEx():

LRESULT CALLBACK MyWndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    return DefWindowProc(hWnd, uMsg, wParam, lParam);
}

int WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int)
{
    WNDCLASSEX wcex;
    ...
    wcex.lpfnWndProc = MyWndProc;
    ...
    RegisterClassEx(&wcex);
    ...

}

Každá aplikace, která používá vlastní nedialogové okno, musí registrovat vlastní třídu okna. Pokud programujete pouze v MFC, nenechte se zmýlit tím, že nic takového ve vašem programu nevidíte. Registraci třídy okna za vás automaticky udělá knihovna MFC.

Při subclassování je možné nastavit vlastní proceduru okna. Protože subclassování se provádí funkcí SetWindowLong() (SetWindowLongPtr), je subclassování specifické pro okno, nikoliv pro všechna okna dané třídy. Pokud tedy subclassujete edit box, nebudou subclassované všechny instance editboxu ve vaší aplikaci.

MyWndProc uvnitř třídy

Dejme tomu, že chceme napsat třídu, zapouzdřující okno a chceme se vyhnout knihovně MFC. Tato třída by měla obsahovat aspoň funkci Create (která vytvoří okno) a funkci WndProc, která se bude starat o obsluhu zpráv. Tedy asi takto:

class TMyWindow
{
  public:
    TMyWindow();
    ~TMyWindow();

    int  RegisterWindow(HINSTANCE hInst);
    HWND Create(HWND hParent = NULL);
    int  RunMessageLoop();

  protected:
    LONG CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam);

    // A další

};

Napíšeme třídu, a zkompilujeme program. Ale ouha - překladač hlásí tuto chybu:

error C2440: '=' : cannot convert from 
   long (__stdcall TMyWindow::*)(struct HWND__ *,unsigned int,unsigned int,long)
to
   long (__stdcall *)(struct HWND__ *,unsigned int,unsigned int,long)

Stručně řečeno, překladači vadí, že naše funkce WndProc() je členskou proměnnou třídy. Proč ale ?

Vysvětlením je způsob volání členských funkcí třídy. Každá funkce třídy je volána s "neviditelným" parametrem this, který je ukazatelem na aktuální instanci třídy. Pokud v programu napíšeme volání:

pClass->Function(Param1);
překladač to přeloží jako
TClass::Function(pClass, Param1);

Proto není možné běžnou členskou funkci nastavit jako WndProc() nějakému oknu. Systém Windows jednoduše při volání předá funkci čtyři parametry. Členská funkce jich v případě naší WndProc() dostane pět. Co s tím ?

Naštěstí členská funkce může obsahovat funkce, které nemají parametr this. Tyto funkce je nutné ve třídě nadeklarovat jako static:

class TMyWindow
{
  protected:
    static LONG CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
};

Takováto funkce již překladači nevadí. Protože její volací konvence, počet parametrů a návratová hodnota souhlasí s požadovanou deklarací funkce WndProc(), překladač si přestane stěžovat.

Uvedené řešení v sobě obsahuje další problém. Protože uvnitř funkce WndProc() neexistuje hodnota this, není možné přistupovat v k členským proměnným třídy a ani volat členské funkce. Při pokusu o volání funkce z WndProc() hlásí překladač chybu:

LONG CALLBACK TMyWindow::WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    if(uMsg == WM_MOUSEMOVE)
        OnMouseMove(hWnd, wParam, lParam);

    return DefWindowProc(hWnd, uMsg, wParam, lParam);
}
error C2352: 'TMyWindow::OnMouseMove' : illegal call of non-static member function

K tomu, abychom tuto funkci mohli zavolat, prostě potřebujeme ukazatel na instanci třídy. Tento ukazatel je možné uložit např. do proměnných okna pomocí SetWindowLongPtr:

LRESULT CALLBACK TMyWindow::WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    TMyWindow * pMyWindow = (TMyWindow *)GetWindowLongPtr(hWnd, GWLP_USERDATA);

    switch(uMsg)
    {
        case WM_CREATE:
            SetWindowLongPtr(hWnd, GWLP_USERDATA, (LONG_PTR)((LPCREATESTRUCT)lParam)->lpCreateParams);
            break;

        ...
    }
}

Pokud chcete použít dialog, je potřeba použít jinou inicializaci

INT_PTR CALLBACK TMyDialog::DialogProc(HWND hDlg, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    TMyDialog * pMyDialog = (TMyWindow *)GetWindowLongPtr(hDlg, GWLP_USER);

    switch(uMsg)
    {
        case WM_INITDIALOG:
            SetWindowLongPtr(hDlg, GWLP_USER, (LONG_PTR)lParam);
            break;

        ...
    }
}
HWND TMyWindow::Create(HWND hParent)
{
    return CreateWindow(szClassName, "MyWindow", WS_OVERLAPPEDWINDOW,
                 CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
                 hParent, NULL, m_hInst, this);
}

Samozřejmě je možné implementovat i jiný způsob, předání ukazatele this do WndProc() MFC např. používá mapu, ve které jsou dvojice hWnd a pWnd. V této mapě se pak hledá ukazatel na třídu, která "patří" danému oknu.

Ukázkový projekt Wndproc (10 KB)