Gestione dello stato di un'applicazione
Una routine di finestra è solo una funzione che viene richiamata per ogni messaggio, quindi è intrinsecamente senza stato. È quindi necessario tenere traccia dello stato dell'applicazione da una chiamata di funzione alla successiva.
L'approccio più semplice consiste semplicemente nell'inserire tutto nelle variabili globali. Questa operazione funziona abbastanza bene per i programmi di piccole dimensioni e molti esempi di SDK usano questo approccio. In un grande programma, tuttavia, porta a una proliferazione di variabili globali. Inoltre, potresti avere diverse finestre, ognuna con la propria procedura di finestra. Tenere traccia di quale finestra deve accedere alle variabili che diventano confusioni e soggette a errori.
La funzione CreateWindowEx consente di passare qualsiasi struttura di dati a una finestra. Quando questa funzione viene chiamata, invia i due messaggi seguenti alla routine della finestra:
Questi messaggi vengono inviati nell'ordine elencato. Questi non sono gli unici due messaggi inviati durante CreateWindowEx, ma è possibile ignorare gli altri per questa discussione.
Il messaggio WM_NCCREATE e WM_CREATE vengono inviati prima che la finestra diventi visibile. Ciò consente di inizializzare l'interfaccia utente, ad esempio per determinare il layout iniziale della finestra.
L'ultimo parametro di CreateWindowEx è un puntatore di tipo void*. È possibile passare qualsiasi valore del puntatore desiderato in questo parametro. Quando la procedura della finestra gestisce il messaggio WM_NCCREATE o WM_CREATE , può estrarre questo valore dai dati del messaggio.
Verrà illustrato come usare questo parametro per passare i dati dell'applicazione alla finestra. Definire prima di tutto una classe o una struttura che contiene informazioni sullo stato.
// Define a structure to hold some state information.
struct StateInfo {
// ... (struct members not shown)
};
Quando si chiama CreateWindowEx, passare un puntatore a questa struttura nel parametro void* finale.
StateInfo *pState = new (std::nothrow) StateInfo;
if (pState == NULL)
{
return 0;
}
// Initialize the structure members (not shown).
HWND hwnd = CreateWindowEx(
0, // Optional window styles.
CLASS_NAME, // Window class
L"Learn to Program Windows", // Window text
WS_OVERLAPPEDWINDOW, // Window style
// Size and position
CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
NULL, // Parent window
NULL, // Menu
hInstance, // Instance handle
pState // Additional application data
);
Quando si ricevono i messaggi WM_NCCREATE e WM_CREATE , il parametro lParam di ogni messaggio è un puntatore a una struttura CREATESTRUCT . La struttura CREATESTRUCT , a sua volta, contiene il puntatore passato in CreateWindowEx.
Ecco come estrarre il puntatore alla struttura dei dati. Prima di tutto, ottenere la struttura CREATESTRUCT eseguendo il cast del parametro lParam .
CREATESTRUCT *pCreate = reinterpret_cast<CREATESTRUCT*>(lParam);
Il membro lpCreateParams della struttura CREATESTRUCT è il puntatore void originale specificato in CreateWindowEx. Ottenere un puntatore alla propria struttura di dati eseguendo il cast di lpCreateParams.
pState = reinterpret_cast<StateInfo*>(pCreate->lpCreateParams);
Chiamare quindi la funzione SetWindowLongPtr e passare il puntatore alla struttura dei dati.
SetWindowLongPtr(hwnd, GWLP_USERDATA, (LONG_PTR)pState);
Lo scopo di questa ultima chiamata di funzione consiste nell'archiviare il puntatore StateInfo nei dati dell'istanza per la finestra. Dopo aver eseguito questa operazione, è sempre possibile recuperare il puntatore dalla finestra chiamando GetWindowLongPtr:
LONG_PTR ptr = GetWindowLongPtr(hwnd, GWLP_USERDATA);
StateInfo *pState = reinterpret_cast<StateInfo*>(ptr);
Ogni finestra ha i propri dati di istanza, quindi è possibile creare più finestre e assegnare a ogni finestra la propria istanza della struttura dei dati. Questo approccio è particolarmente utile se si definisce una classe di finestre e si crea più finestre di tale classe, ad esempio se si crea una classe di controllo personalizzata. È pratico eseguire il wrapping della chiamata GetWindowLongPtr in una piccola funzione helper.
inline StateInfo* GetAppState(HWND hwnd)
{
LONG_PTR ptr = GetWindowLongPtr(hwnd, GWLP_USERDATA);
StateInfo *pState = reinterpret_cast<StateInfo*>(ptr);
return pState;
}
È ora possibile scrivere la routine della finestra come indicato di seguito.
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
StateInfo *pState;
if (uMsg == WM_CREATE)
{
CREATESTRUCT *pCreate = reinterpret_cast<CREATESTRUCT*>(lParam);
pState = reinterpret_cast<StateInfo*>(pCreate->lpCreateParams);
SetWindowLongPtr(hwnd, GWLP_USERDATA, (LONG_PTR)pState);
}
else
{
pState = GetAppState(hwnd);
}
switch (uMsg)
{
// Remainder of the window procedure not shown ...
}
return TRUE;
}
Approccio Object-Oriented
È possibile estendere ulteriormente questo approccio. È già stata definita una struttura di dati per contenere informazioni sullo stato sulla finestra. È consigliabile fornire questa struttura di dati con funzioni membro (metodi) che operano sui dati. Ciò comporta naturalmente una progettazione in cui la struttura (o la classe) è responsabile di tutte le operazioni nella finestra. La procedura della finestra diventerà quindi parte della classe.
In altre parole, vorremmo andare da questo:
// pseudocode
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
StateInfo *pState;
/* Get pState from the HWND. */
switch (uMsg)
{
case WM_SIZE:
HandleResize(pState, ...);
break;
case WM_PAINT:
HandlePaint(pState, ...);
break;
// And so forth.
}
}
A questo scopo:
// pseudocode
LRESULT MyWindow::WindowProc(UINT uMsg, WPARAM wParam, LPARAM lParam)
{
switch (uMsg)
{
case WM_SIZE:
this->HandleResize(...);
break;
case WM_PAINT:
this->HandlePaint(...);
break;
}
}
L'unico problema è come collegare il MyWindow::WindowProc
metodo. La funzione RegisterClass prevede che la procedura della finestra sia un puntatore alla funzione. Non è possibile passare un puntatore a una funzione membro (non statica) in questo contesto. Tuttavia, è possibile passare un puntatore a una funzione membro statica e quindi delegare alla funzione membro. Ecco un modello di classe che mostra questo approccio:
template <class DERIVED_TYPE>
class BaseWindow
{
public:
static LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
DERIVED_TYPE *pThis = NULL;
if (uMsg == WM_NCCREATE)
{
CREATESTRUCT* pCreate = (CREATESTRUCT*)lParam;
pThis = (DERIVED_TYPE*)pCreate->lpCreateParams;
SetWindowLongPtr(hwnd, GWLP_USERDATA, (LONG_PTR)pThis);
pThis->m_hwnd = hwnd;
}
else
{
pThis = (DERIVED_TYPE*)GetWindowLongPtr(hwnd, GWLP_USERDATA);
}
if (pThis)
{
return pThis->HandleMessage(uMsg, wParam, lParam);
}
else
{
return DefWindowProc(hwnd, uMsg, wParam, lParam);
}
}
BaseWindow() : m_hwnd(NULL) { }
BOOL Create(
PCWSTR lpWindowName,
DWORD dwStyle,
DWORD dwExStyle = 0,
int x = CW_USEDEFAULT,
int y = CW_USEDEFAULT,
int nWidth = CW_USEDEFAULT,
int nHeight = CW_USEDEFAULT,
HWND hWndParent = 0,
HMENU hMenu = 0
)
{
WNDCLASS wc = {0};
wc.lpfnWndProc = DERIVED_TYPE::WindowProc;
wc.hInstance = GetModuleHandle(NULL);
wc.lpszClassName = ClassName();
RegisterClass(&wc);
m_hwnd = CreateWindowEx(
dwExStyle, ClassName(), lpWindowName, dwStyle, x, y,
nWidth, nHeight, hWndParent, hMenu, GetModuleHandle(NULL), this
);
return (m_hwnd ? TRUE : FALSE);
}
HWND Window() const { return m_hwnd; }
protected:
virtual PCWSTR ClassName() const = 0;
virtual LRESULT HandleMessage(UINT uMsg, WPARAM wParam, LPARAM lParam) = 0;
HWND m_hwnd;
};
La BaseWindow
classe è una classe base astratta, da cui derivano classi di finestre specifiche. Ecco ad esempio la dichiarazione di una classe semplice derivata da BaseWindow
:
class MainWindow : public BaseWindow<MainWindow>
{
public:
PCWSTR ClassName() const { return L"Sample Window Class"; }
LRESULT HandleMessage(UINT uMsg, WPARAM wParam, LPARAM lParam);
};
Per creare la finestra, chiamare BaseWindow::Create
:
int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE, PWSTR pCmdLine, int nCmdShow)
{
MainWindow win;
if (!win.Create(L"Learn to Program Windows", WS_OVERLAPPEDWINDOW))
{
return 0;
}
ShowWindow(win.Window(), nCmdShow);
// Run the message loop.
MSG msg = { };
while (GetMessage(&msg, NULL, 0, 0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
return 0;
}
Il metodo pure-virtuale BaseWindow::HandleMessage
viene usato per implementare la procedura di finestra. Ad esempio, l'implementazione seguente equivale alla procedura della finestra visualizzata all'inizio del modulo 1.
LRESULT MainWindow::HandleMessage(UINT uMsg, WPARAM wParam, LPARAM lParam)
{
switch (uMsg)
{
case WM_DESTROY:
PostQuitMessage(0);
return 0;
case WM_PAINT:
{
PAINTSTRUCT ps;
HDC hdc = BeginPaint(m_hwnd, &ps);
FillRect(hdc, &ps.rcPaint, (HBRUSH) (COLOR_WINDOW+1));
EndPaint(m_hwnd, &ps);
}
return 0;
default:
return DefWindowProc(m_hwnd, uMsg, wParam, lParam);
}
return TRUE;
}
Si noti che l'handle della finestra viene archiviato in una variabile membro (m_hwnd), quindi non è necessario passarlo come parametro a HandleMessage
.
Molti dei framework di programmazione Windows esistenti, ad esempio Microsoft Foundation Classes (MFC) e Active Template Library (ATL), usano approcci che sono fondamentalmente simili a quelli illustrati qui. Naturalmente, un framework completamente generalizzato come MFC è più complesso di questo esempio relativamente semplicistico.
Prossima
Modulo 2: Uso di COM nel programma Windows