Il presente articolo è stato tradotto automaticamente.

Windows con C++

Creazione di app desktop Visual C++ 2012

Kenny Kerr

 

Kenny KerrCon tutto l'hype su Windows 8 e che cosa ora sono conosciuti come Windows Store apps, ho ricevuto alcune domande circa la rilevanza delle applicazioni desktop e se Standard C++ è ancora una scelta valida andando avanti. Queste domande sono a volte difficile da rispondere, ma quello che posso dirvi è che il compilatore Visual C++ 2012 è più impegnato rispetto mai Standard C++ e rimane la toolchain migliore, a mio modesto parere, per la costruzione di grandi applicazioni desktop per Windows se si stai targeting per Windows 7, Windows 8 o addirittura Windows XP.

Una follow-up questione inevitabilmente ricevere è modo migliore per lo sviluppo di un'applicazione desktop di approccio su Windows e da dove cominciare. Beh, in mese questo, sto andando a esplorare i fondamenti della creazione di applicazioni desktop con Visual C++. Quando mi è stato introdotto in primo luogo alle finestre di programmazione da Jeff Prosise (bit.ly/WmoRuR), Microsoft Foundation Classes (MFC) è stato un promettente nuovo modo creare applicazioni. Mentre MFC è ancora disponibile, esso realmente è mostrare la sua età, e la necessità di alternative moderne e flessibili ha spinto i programmatori per la ricerca di nuovi approcci. Questo problema è stato aggravato da uno spostamento dall'utente e GDI (msdn.com/library/ms724515) le risorse e verso Direct3D come fondamento principale da cui contenuto viene eseguito il rendering sullo schermo.

Per anni sono stato promuovere la libreria ATL (Active Template) e la sua estensione, il Windows Template Library (WTL), come grandi scelte per la costruzione di applicazioni. Tuttavia, anche queste librerie sono ora mostrando segni di invecchiamento. Con il passaggio dall'utente e GDI risorse, c'è anche meno motivo di usarli. Così dove cominciare? Con l'API di Windows, naturalmente. Ti mostrerò che creando una finestra desktop senza alcuna libreria a tutti non è in realtà scoraggiante come potrebbe sembrare in un primo momento. Poi ti mostrerò come si può dare un po ' più di un C++ sapore, se lo desiderano, con un piccolo aiuto da ATL e WTL. ATL e WTL ha molto più senso una volta che hai una buona idea di come funziona il tutto dietro le macro e i modelli.

Windows API

Il guaio con l'utilizzo dell'API di Windows per creare una finestra desktop è che ci sono innumerevoli modi si potrebbe andare su scrittura — troppe scelte, davvero. Ancora, c'è un modo semplice per creare una finestra, e si inizia con il master Includi file per Windows:

#include <windows.h>

È quindi possibile definire il punto di ingresso standard per le applicazioni:

int __stdcall wWinMain(HINSTANCE module, HINSTANCE, PWSTR, int)

Se si sta scrivendo un'applicazione console, allora si può solo continuare a utilizzare la funzione di punto di ingresso principale C++ standard, ma darò per scontato che non si desidera una finestra di console spuntando ogni volta che inizia il tuo app. La funzione wWinMain è immersa nella storia. Il stdcall convenzione di chiamata chiarisce questioni sulla confusione x86 architecture, che prevede una manciata di convenzioni di chiamata. Se stai targeting x64 o braccio, quindi non importa perché il compilatore Visual C++ implementa solo una convenzione di chiamata singola su quelle architetture — ma non fa male, neanche.

I due parametri HINSTANCE sono particolarmente avvolta nella storia. Nei giorni 16-bit di Windows, la seconda HINSTANCE era il manico a qualsiasi istanza precedente dell'app. Questo ha permesso un'app per comunicare con qualsiasi precedente istanza della stessa o anche per tornare alla precedente istanza se l'utente aveva accidentalmente iniziato nuovamente. Oggi, questo secondo parametro è sempre un nullptr. Avrete anche notato che ho chiamato il primo parametro "modulo" piuttosto che "istanza". Ancora una volta, a 16-bit di Windows, istanze e moduli erano due cose separate. Tutte le applicazioni vuoi condividere segmenti di codice contenente i modulo ma sarebbero dato istanze univoche contenente i segmenti dati. I parametri HINSTANCE correnti e precedenti ora dovrebbero avere più senso. Windows a 32-bit introdotti spazi di indirizzamento separati e insieme a quello della necessità per ogni processo mappare il propria istanza/modulo, ora la stessa. Oggi, questo è solo l'indirizzo di base del file eseguibile. Il linker Visual C++ in realtà espone questo indirizzo tramite una variabile pseudo, cui è possibile accedere dichiarandola come segue:

extern "C" IMAGE_DOS_HEADER __ImageBase;

L'indirizzo di ImageBase sarà lo stesso valore come parametro HINSTANCE. Questo è infatti il modo in cui la libreria di runtime C (CRT) ottiene l'indirizzo del modulo da passare alla funzione wWinMain in primo luogo. È una scorciatoia comoda se non volete passare questo parametro wWinMain intorno il tuo app. Tenere a mente, però, che questa variabile indica il modulo corrente se è una DLL o un file eseguibile ed è quindi utile per caricare le risorse specifiche del modulo senza ambiguità.

Il parametro successivo fornisce argomenti della riga di comando, e l'ultimo parametro è un valore che deve essere passato alla funzione ShowWindow per la finestra principale dell'applicazione, supponendo che si sta chiamando inizialmente ShowWindow. L'ironia è che quasi sempre verrà ignorata. Questo va indietro al modo in cui viene lanciato un'app via CreateProcess e gli amici per consentire un collegamento — o qualche altro app — per definire se la finestra principale di un'applicazione inizialmente è ridotto a icona, ingrandita o mostrata normalmente.

All'interno della funzione wWinMain, l'app deve registrare una classe della finestra. La classe della finestra è descritto da una struttura WNDCLASS e registrata con la funzione RegisterClass. Questa registrazione è memorizzato in una tabella utilizzando una coppia composta dal modulo puntatore e il nome della classe, permettendo la funzione CreateWindow cercare le informazioni di classe quando è il momento di creare la finestra:

WNDCLASS wc = {};
wc.hCursor = LoadCursor(nullptr, IDC_ARROW);
wc.hInstance = module;
wc.lpszClassName = L"window";
wc.lpfnWndProc = []
 (HWND window, UINT message, WPARAM wparam, 
    LPARAM lparam) -> LRESULT
{
  ...
};
VERIFY(RegisterClass(&wc));

Per mantenere gli esempi brevi, userò solo il macro VERIFY comune come segnaposto per indicare dove sarà necessario aggiungere alcuni per gestire eventuali errori segnalati dalle varie funzioni API di gestione degli errori. Basta considerare queste come segnaposto per il vostro preferito politica di gestione degli errori.

Il codice precedente è il minimo che è necessaria per descrivere una finestra standard. La struttura WNDCLASS è inizializzata con una vuota coppia di parentesi graffe. Questo assicura che i membri della struttura vengono inizializzati a zero o nullptr. Solo i membri che devono essere impostati sono hCursor per indicare che il puntatore del mouse, o cursore, da utilizzare quando il mouse si trova sopra la finestra; hInstance e lpszClassName per identificare la classe di finestra all'interno del processo; e lpfnWndProc per puntare alla routine di finestra che elaborerà i messaggi inviati alla finestra. In questo caso, sto usando un'espressione lambda per tenere tutto in linea, per intenderci. Potrai ottenere indietro alla routine di finestra in un attimo. Il passo successivo è quello di creare la finestra:

VERIFY(CreateWindow(wc.lpszClassName, L"Title",
  WS_OVERLAPPEDWINDOW | WS_VISIBLE,
  CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
  nullptr, nullptr, module, nullptr));

La funzione CreateWindow prevede parecchi parametri, ma la maggior parte di loro sono soli valori predefiniti. I parametri di primi e secondo-di-scorso, come accennato, insieme rappresentano la chiave che crea la funzione RegisterClass per lasciare CreateWindow trovare le informazioni di classe finestra. Il secondo parametro indica il testo che verrà visualizzato nella barra del titolo della finestra. Il terzo indica lo stile della finestra. La costante WS_OVERLAPPEDWINDOW è uno stile comunemente usato descrivere una normale finestra di primo livello con una barra del titolo con bottoni, bordi ridimensionabili e così via. Combinando questo con la costante WS_VISIBLE incarica CreateWindow per andare avanti e mostrare la finestra. Se si omette WS_VISIBLE, è necessario chiamare la funzione ShowWindow prima finestra farà il suo debutto sul desktop.

I prossimi quattro parametri indicano posizione iniziale e la dimensione della finestra, e la costante CW_USEDEFAULT utilizzata in ogni caso appena dice Windows scegliere valori predefiniti appropriati. I successivi due parametri forniscono l'handle della finestra finestra padre e menu, rispettivamente (e non sono necessari). Il parametro finale fornisce l'opzione di passare un valore per la dimensione del puntatore alla routine di finestra durante la creazione. Se tutto va bene, viene visualizzata una finestra sul desktop e viene restituito un handle di finestra. Se le cose andare a sud, poi nullptr viene invece restituito e può essere chiamata la funzione GetLastError per scoprire perché. Con tutto il parlare i disagi di utilizzando l'API di Windows, si scopre che la creazione di una finestra è in realtà abbastanza semplice e si riduce a questo:

WNDCLASS wc = { ...
};
RegisterClass(&wc);
CreateWindow( ...
);

Una volta che la finestra viene visualizzata, è importante che il vostro app inizia a spedire messaggi appena possibile — altrimenti il vostro app apparirà non risponde. Windows è fondamentalmente un OS basato su messaggi, event-driven. Questo è particolarmente vero per il desktop. Mentre Windows crea e gestisce la coda dei messaggi, è responsabilità del app per annullare l'accodamento e inviare loro, perché i messaggi vengono inviati al thread di una finestra, piuttosto che direttamente alla finestra. Questo fornisce una notevole flessibilità, ma un ciclo semplice messaggio non deve essere complicato, come illustrato di seguito:

MSG message;
BOOL result;
while (result = GetMessage(&message, 0, 0, 0))
{
  if (-1 != result)
  {
    DispatchMessage(&message);
  }
}

Forse non sorprendentemente, questo ciclo di messaggi apparentemente semplice è spesso implementato in modo errato. Questo deriva dal fatto che la funzione GetMessage è prototipo per restituire un valore Boolean, ma in realtà, questo è davvero solo un int. GetMessage dequeues o recupera, un messaggio dalla coda di messaggi del thread chiamante. Questo può essere per qualsiasi finestra o no a tutti, ma nel nostro caso, il thread è solo pompaggio messaggi per una singola finestra. Se il messaggio WM_QUIT è annullato, poi GetMessage restituirà zero, che indica che la finestra è scomparso e viene eseguita il messaggi dei elaborazione e che dovrebbe terminare l'applicazione. Se qualcosa va storto, poi GetMessage potrebbe restituire -1 e si può ancora chiamare GetLastError per ottenere ulteriori informazioni. In caso contrario, qualsiasi valore diverso da zero di ritorno da GetMessage indica che un messaggio è stato rimosso dalla coda ed è pronto per essere spedito alla finestra. Naturalmente, questo è lo scopo della funzione DispatchMessage. Naturalmente, ci sono molte varianti per il ciclo di messaggi, e avendo la possibilità di costruire il tuo offre molte scelte per come si comporterà il vostro app, cosa ingresso che accetterà e come verrà tradotto. A parte il puntatore MSG, i restanti parametri di GetMessage utilizzabile facoltativamente filtrare i messaggi.

La routine di finestra inizierà a ricevere messaggi prima che la funzione CreateWindow restituisce anche, quindi era meglio essere pronti e in attesa. Ma che cosa assomigliare? Una finestra richiede una tabella o una mappa dei messaggi. Questo potrebbe essere letteralmente una catena di istruzioni if-else o un'istruzione switch grande all'interno della routine della finestra. Questo, tuttavia, rapidamente diventare ingombrante, e molto sforzo è stato speso in diverse librerie e Framework per cercare di gestire questo in qualche modo. In realtà, essa non deve essere nulla di fantasia, e in molti casi è sufficiente una semplice tabella statica. In primo luogo, aiuta a conoscere che cosa è costituito da una finestra di messaggio. Soprattutto, c'è una costante — ad esempio WM_PAINT o WM_SIZE — che identifica in modo univoco il messaggio. Due argomenti, per intenderci, sono forniti per ogni messaggio, e questi sono chiamati WPARAM e LPARAM, rispettivamente. A seconda del messaggio, questi potrebbe non fornire tutte le informazioni. Infine, Windows prevede la gestione di determinati messaggi per restituire un valore, e questo è chiamato il LRESULT. La maggior parte dei messaggi che il vostro app gestisce, tuttavia, non restituiscono un valore e devono invece restituire zero.

Data questa definizione, possiamo costruire una tabella semplice per la gestione dei messaggi utilizzando questi tipi come blocchetti di costruzione:

typedef LRESULT (* message_callback)(HWND, WPARAM, LPARAM);
struct message_handler
{
  UINT message;
  message_callback handler;
};

Come minimo, quindi possiamo creare una tabella statica dei gestori di messaggi, come mostrato Figura 1.

Figura 1 tabella statica dei gestori di messaggi

static message_handler s_handlers[] =
{
  {
    WM_PAINT, [] (HWND window, WPARAM, LPARAM) -> LRESULT
    {
      PAINTSTRUCT ps;
      VERIFY(BeginPaint(window, &ps));
      // Dress up some pixels here!
EndPaint(window, &ps);
      return 0;
    }
  },
  {
    WM_DESTROY, [] (HWND, WPARAM, LPARAM) -> LRESULT
    {
      PostQuitMessage(0);
      return 0;
    }
  }
};

Quando la finestra ha bisogno di pittura arriva il messaggio WM_PAINT. Questo succede molto meno spesso di quanto abbia fatto nelle versioni precedenti di Windows grazie ai progressi nel rendering e la composizione del desktop. Le funzioni BeginPaint ed EndPaint sono reliquie di GDI , ma sono ancora necessari anche se si sta disegnando con un motore di rendering completamente diverso. Questo è perché dicono Windows che hai finito pittura convalidando la finestra disegno di superficie. Senza queste chiamate, Windows non sarebbe considerare il messaggio WM_PAINT risposto e la finestra vuoi ricevere messaggi un flusso costante di WM_PAINT inutilmente.

Il messaggio WM_DESTROY arriva dopo che la finestra è scomparso, ti dira che la finestra viene distrutta. Questo è di solito un indicatore che dovrebbe terminare l'applicazione, ma la funzione GetMessage all'interno il ciclo di messaggi è ancora in attesa del messaggio WM_QUIT. Questo messaggio di accodamento è il lavoro della funzione PostQuitMessage, come descritto. L'unico parametro accetta un valore che viene passato lungo via WPARAM di WM_QUIT, come un modo per restituire codici di uscita diversa quando si chiude l'applicazione.

L'ultimo pezzo del puzzle è di attuare la procedura di finestra reale. Ho omesso il corpo del lambda che ho usato per preparare la struttura WNDCLASS in precedenza, ma visto quello che ora so, non dovrebbe essere difficile da capire che cosa potrebbe sembrare:

wc.lpfnWndProc =
  [] (HWND window, UINT message,
      WPARAM wparam, LPARAM lparam) -> LRESULT
{
  for (auto h = s_handlers; h != s_handlers +
    _countof(s_handlers); ++h)
  {
    if (message == h->message)
    {
      return h->handler(window, wparam, lparam);
    }
  }
  return DefWindowProc(window, message, wparam, lparam);
};

Il per ciclo sembra per un gestore corrispondente. Fortunatamente, Windows fornisce predefinito della gestione dei messaggi che si sceglie di non elaborare voi stessi. Questo è il compito della funzione DefWindowProc.

E questo è tutto, se hai ottenuto questo lontano, che hai creato con successo una finestra del desktop utilizzando le API di Windows!

Il modo di ATL

Il problema con queste funzioni API di Windows è che essi sono stati progettati a lungo prima di C++ divenne il singolo di successo che è oggi e così non erano progettata per accogliere comodamente una visione del mondo object-oriented. Ancora, con abbastanza codifica intelligente, questa API C-stile può essere trasformata in qualcosa di un po ' più adatto per il programmatore C++ medio. ATL fornisce una libreria di modelli di classe e le macro che appena che, quindi se avete bisogno di gestire più di una manciata di classi di finestre o ancora contare su risorse utente e GDI per l'attuazione della vostra finestra, non c'è davvero alcun motivo per non utilizzano ATL. La finestra della sezione precedente può essere espressa con ATL, come mostrato Figura 2.

Figura 2 esprimendo una finestra in ATL

class Window : public CWindowImpl<Window, CWindow,
  CWinTraits<WS_OVERLAPPEDWINDOW | WS_VISIBLE>>
{
  BEGIN_MSG_MAP(Window)
    MESSAGE_HANDLER(WM_PAINT, PaintHandler)
    MESSAGE_HANDLER(WM_DESTROY, DestroyHandler)
  END_MSG_MAP()
  LRESULT PaintHandler(UINT, WPARAM, LPARAM, BOOL &)
  {
    PAINTSTRUCT ps;
    VERIFY(BeginPaint(&ps));
    // Dress up some pixels here!
EndPaint(&ps);
    return 0;
  }
  LRESULT DestroyHandler(UINT, WPARAM, LPARAM, BOOL &)
  {
    PostQuitMessage(0);
    return 0;
  }
};

La classe CWindowImpl fornisce il necessario routing dei messaggi. CWindow è una classe base che fornisce un grande molti wrapper della funzione membro, principalmente quindi non è necessario fornire in modo esplicito l'handle della finestra su ogni chiamata di funzione. Si può vedere in azione con la funzione BeginPaint ed EndPaint chiama in questo esempio. Il modello di CWinTraits fornisce la finestra costanti di stile che verranno utilizzati durante la creazione.

Le macro harken torna a MFC e lavorano con CWindowImpl per abbinare i messaggi in arrivo alle funzioni membro appropriato per la movimentazione. Ogni gestore è fornito con la costante del messaggio come primo argomento. Questo può essere utile se avete bisogno di gestire una varietà di messaggi con una funzione membro singolo. Il parametro finale impostazione predefinita è TRUE e consente al gestore di decidere in fase di esecuzione, se effettivamente vuole elaborare il messaggio o lasciare che Windows — o anche qualche altro gestore, prendersi cura di essa. Queste macro, insieme a CWindowImpl, sono molto potenti e consentono di gestire i messaggi di riflessi, messaggio catena mappe insieme e così via.

Per creare la finestra, è necessario utilizzare la funzione di membro di creare la finestra eredita da CWindowImpl, e questo a sua volta chiamerà il buone vecchio RegisterClass e CreateWindow funzioni sul vostro conto:

Window window;
VERIFY(window.Create(nullptr, 0, L"Title"));

A questo punto, il filo deve ancora iniziare rapidamente invio messaggi, e il ciclo di messaggi Windows API dalla sezione precedente sarà sufficiente. L'approccio ATL certamente è utile se è necessario gestire finestre multiple su un singolo thread, ma con un'unica finestra di primo livello, è lo stesso come l'approccio Windows API dalla sezione precedente.

WTL: Una Dose Extra di ATL

Mentre ATL è stato progettato principalmente per semplificare lo sviluppo del server COM e fornisce solo un semplice, ma estremamente efficace — finestra Gestione modello, WTL è costituito da una sfilza di modelli di classe aggiuntiva e macro progettate specificamente per supportare la creazione di windows più complessi basati su risorse utente e GDI . WTL è ora disponibile su SourceForge (wtl.sourceforge. NET), ma per un nuovo app utilizzando un motore di rendering moderno, non fornisce una grande quantità di valore. Ancora, ci sono una manciata di aiutanti utili. Dall'intestazione WTL atlapp.h, è possibile utilizzare l'implementazione del ciclo di messaggio per sostituire la versione arrotolato a mano che descritta in precedenza:

CMessageLoop loop;
loop.Run();

Anche se è facile cadere nella tua app e uso, WTL confezioni un sacco di potenza, se si hanno sofisticate esigenze routing e filtraggio dei messaggi. WTL fornisce anche atlcrack.h con macro progettate per sostituire la macro MESSAGE_HANDLER generica fornita da ATL Queste sono solo comodità, ma rendono più facile da ottenere installato e funzionante con un nuovo messaggio, perché prendersi cura di cracking aperto il messaggio, per intenderci ed evitare qualsiasi congettura nel capire come interpretare WPARAM e LPARAM. Un buon esempio è WM_SIZE, che racchiude la nuova area client della finestra come le parole di basso e alto ordine del suo LPARAM. Con ATL, questo potrebbe apparire come segue:

BEGIN_MSG_MAP(Window)
  ...
MESSAGE_HANDLER(WM_SIZE, SizeHandler)
END_MSG_MAP()
LRESULT SizeHandler(UINT, WPARAM, 
  LPARAM lparam, BOOL &)
{
  auto width = LOWORD(lparam);
  auto height = HIWORD(lparam);
  // Handle the new size here ...
return 0;
}

Con l'aiuto di WTL, questo è un po' più semplice:

BEGIN_MSG_MAP(Window)
  ...
MSG_WM_SIZE(SizeHandler)
END_MSG_MAP()
void SizeHandler(UINT, SIZE size)
{
  auto width = size.cx;
  auto height = size.cy;
  // Handle the new size here ...
}

Si noti la nuova macro MSG_WM_SIZE che ha sostituito la macro MESSAGE_HANDLER generica nella mappa del messaggio originale. La funzione membro gestendo il messaggio è anche più semplice. Come si può vedere, non ci sono parametri inutili o un valore restituito. Il primo parametro è appena WPARAM, che è possibile controllare se avete bisogno di sapere cosa ha causato il cambiamento nelle dimensioni.

La bellezza di ATL e WTL è che essi stanno appena forniti come un insieme di file di intestazione che è possibile includere a vostra discrezione. Si utilizza quello che serve e ignorare il resto. Tuttavia, come vi ho mostrato qui, si può ottenere abbastanza lontano senza fare affidamento su una qualsiasi di queste librerie e basta scrivere il vostro app utilizzando l'API di Windows. Unitevi a me la prossima volta, quando vi mostrerò un approccio moderno per il rendering in realtà i pixel nella finestra dell'applicazione.

Kenny Kerr è un programmatore di computer basato in Canada, un autore per Pluralsight e MVP Microsoft. Blog di lui a kennykerr.ca ed è possibile seguirlo su Twitter a twitter.com/kennykerr.

Grazie all'esperto tecnica seguente per la revisione di questo articolo: Worachai Chaoweeraprasit