Windows mit C++

Erstellen von Desktop-Apps mit Visual C++ 2012

Kenny Kerr

 

Kenny KerrWährend der allgegenwärtigen Diskussion über Windows 8 und die zugehörigen Windows Store-Apps habe ich einige Fragen zum Stellenwert von Desktop-Apps erhalten, und auch dazu, ob Standard C++ noch immer eine gute Wahl für die Zukunft ist. Solche Fragen sind schwer zu beantworten, aber was ich sagen kann ist, dass der Visual C++ 2012-Compiler mehr denn je mit Standard C++ kompatibel ist und meiner Meinung nach das beste Tool darstellt, das zur Erstellung umfangreicher Desktop-Apps für Windows 7, Windows 8 oder sogar Windows XP verfügbar ist.

Eine anschließende Frage, die sich zwangsläufig stellt, ist die nach der besten Vorgehensweise bei der Entwicklung von Desktop-Apps für Windows. In der Kolumne dieses Monats stelle ich deshalb die Grundlagen der Erstellung von Desktop-Apps mit Visual C++ vor. Als ich von Jeff Prosise (bit.ly/WmoRuR) in die Windows-Programmierung eingeführt wurde, waren die Microsoft Foundation Classes (MFC) ein vielversprechender neuer Weg, um Apps zu entwickeln. MFC ist zwar noch verfügbar, aber in die Jahre gekommen, und die Notwendigkeit für moderne und flexible Alternativen hat Programmierer veranlasst, nach neuen Ansätzen zu suchen. Diese Notwendigkeit wurde noch verstärkt durch die Abkehr von USER- und GDI-Ressourcen (msdn.com/library/ms724515) hin zu Direct3D als primäre Grundlage zum Rendern von Inhalten auf dem Bildschirm.

Über Jahre habe ich die Active Template Library (ATL) und ihre Erweiterung, die Windows Template Library (WTL), zur Entwicklung von Apps empfohlen. Inzwischen zeigen auch diese Bibliotheken Anzeichen des Alterns. Seit der Abkehr von USER- und GDI-Ressourcen gibt es kaum noch Anlass, sie zu benutzen. Womit also beginnen? Natürlich mit der Windows-API. Ich werde Ihnen zeigen, dass die Erstellung eines Desktopfensters ohne Bibliothek nicht so schlimm ist, wie es vielleicht auf den ersten Blick aussieht. Und ich werde anschließend erläutern, wie Sie dabei, mit ein wenig Hilfe von ATL und WTL, etwas mehr C++-Feeling erreichen. ATL und WTL sind allerdings erst dann sinnvoll einsetzbar, wenn Sie eine gute Vorstellung davon haben, was alles hinter den Vorlagen und Makros abläuft.

Die Windows-API

Das Problem bei der Verwendung der Windows-API zur Erstellung eines Desktopfensters ist, dass es unzählige Wege zu diesem Ziel gibt. Es gibt aber auch einen direkten Weg, und er beginnt mit der wichtigsten Includedatei für Windows:

 

#include <windows.h>

Sie können dann die Standardeinstiegspunkte für Apps definieren:

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

Falls Sie eine Konsolenanwendung schreiben möchten, können Sie einfach weiterhin die Standard-C++-Funktion für den Haupteinstiegspunkt verwenden, aber ich gehe davon aus, dass Sie nicht möchten, dass sich bei jedem Start Ihrer App ein Konsolenfenster öffnet. Die wWinMain-Funktion ist geschichtsträchtig. Die __stdcall-Aufrufkonvention bringt Klarheit in die verwirrende x86-Architektur, die etliche Aufrufkonventionen bereitstellt. Das spielt allerdings keine Rolle, wenn Sie für x64 oder ARM programmieren, weil der Visual C++-Compiler für diese Architekturen nur eine Aufrufkonvention implementiert.

Insbesondere die beiden HINSTANCE-Parameter sind historisch bedingt. In den 16-Bit-Tagen von Windows war das zweite HINSTANCE das Handle für jede vorherige Instanz der App. Es ermöglichte einer App, mit einer früheren Instanz von sich selbst zu kommunizieren oder sogar in diese zurückzuwechseln, wenn der Benutzer sie versehentlich erneut gestartet hatte. Heute ist dieser zweite Parameter immer ein „nullptr“. Sie werden bemerkt haben, dass ich den ersten Parameter „module“ statt „instance“ genannt habe. In 16-Bit-Windows waren Instanzen und Module getrennte Dinge. Alle Apps teilten sich das Modul mit den Codesegmenten, besaßen aber eigene Instanzen für die Datensegmente. Die aktuellen und früheren HINSTANCE-Parameter sollten jetzt mehr Sinn ergeben. 32-Bit-Windows führte getrennte Adressräume ein und damit für jeden Prozess die Notwendigkeit, eine eigene Instanz bzw. ein eigenes Modul zu verwalten, die jetzt identisch waren. Heute ist dies nur die Basisadresse der ausführbaren Datei. Der Visual C++-Linker macht diese Adresse durch eine Pseudovariable verfügbar, auf die Sie mit folgender Deklaration zugreifen können:

extern "C" IMAGE_DOS_HEADER __ImageBase;

Die Adresse von „__ImageBase“ wird der gleiche Wert wie der des HINSTANCE-Parameters sein. Auf diese Weise ruft die C-Laufzeitbibliothek die Adresse des Moduls ab, die Ihrer wWinMain-Funktion zuerst übergeben wird. Das ist ein üblicher Shortcut, wenn Sie diesen wWinMain-Parameter nicht in Ihrer App weitergeben möchten. Beachten Sie aber, dass diese Variable auf das aktuelle Modul verweist (DLL oder eine ausführbare Datei) und somit zum eindeutigen Laden modulspezifischer Ressourcen nützlich ist.

Der nächste Parameter stellt beliebige Befehlszeilenargumente bereit, und der letzte Parameter ist ein Wert, der an die ShowWindow-Funktion für das Hauptfenster der App weitergegeben werden muss, vorausgesetzt, dass Sie ShowWindow zu Beginn aufrufen. Ironischerweise wird er fast immer ignoriert. Grund dafür ist die Art und Weise, in der eine App über „CreateProcess“ und ähnliche Funktionen gestartet wird, um einem Shortcut (oder einer anderen App) die Festlegung zu ermöglichen, ob das Hauptfenster einer App beim Start minimiert, maximiert oder normal angezeigt wird.

Die App muss innerhalb der wWinMain-Funktion eine Fensterklasse registrieren. Die Fensterklasse wird durch eine WNDCLASS-Struktur beschrieben und mit der RegisterClass-Funktion registriert. Diese Registrierung wird in einer Tabelle mit einem Wertepaar, bestehend aus dem Modulzeiger und dem Klassennamen, gespeichert, sodass die CreateWindow-Funktion die Klasseninformationen abrufen kann, wenn es an der Zeit ist, das Fenster zu erstellen:

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));

Um die Beispiele einfach zu halten, verwende ich das allgemeine VERIFY-Makro als Platzhalter, um anzugeben, wo Code eingefügt werden sollte, der etwaige Fehlermeldungen der diversen API-Funktionen behandelt. Ersetzen Sie diese Platzhalter durch Ihre bevorzugten Fehlerbehandlungsroutinen.

Der bisherige Code ist mindestens erforderlich, um ein Standardfenster zu beschreiben. Die WNDCLASS-Struktur wird mit einem leeren Paar geschweifter Klammern initialisiert. Dadurch ist sichergestellt, dass alle Strukturmember mit 0 (Null) oder dem „nullptr“ initialisiert werden. Festgelegt werden müssen nur folgende Member: „hCursor“, um den Mauszeiger (Cursor) zu definieren, der im Fenster angezeigt werden soll, „hInstance“ und „lpszClassName“, um die Fensterklasse für den Prozess zu identifizieren, und „lpfnWndProc“, um auf die Fensterprozedur zu zeigen, die Nachrichten für das Fenster verarbeitet. In diesem Fall verwende ich einen Lambda-Ausdruck, um sozusagen alles zusammenzufassen. Auf die Fensterprozedur komme ich gleich zurück. Zunächst soll im nächsten Schritt das Fenster erstellt werden:

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

Die CreateWindow-Funktion erwartet etliche Parameter, von denen die meisten Standardwerte sind. Der erste und der vorletzte Parameter bilden, wie bereits erwähnt, gemeinsam den Schlüssel, den die RegisterClass-Funktion erstellt, damit „CreateWindow“ die Fensterklasseninformationen finden kann. Der zweite Parameter gibt den Text an, der in der Titelleiste des Fensters angezeigt wird. Der dritte bestimmt den Fensterstil. Mit der Konstanten WS_OVERLAPPEDWINDOW wird üblicherweise ein reguläres Fenster der obersten Ebene festgelegt, das eine Titelleiste mit Schaltflächen besitzt, in der Größe veränderbar ist, usw. In Kombination mit der Konstanten WS_VISIBLE wird „CreateWindow“ angewiesen, das Fenster anzuzeigen. Wenn Sie WS_VISIBLE weglassen, müssen Sie die ShowWindow-Funktion aufrufen, damit das Fenster auf dem Desktop sichtbar wird.

Die nächsten vier Parameter geben Ausgangsposition und Anfangsgröße für das Fenster an, wobei die jeweils verwendete Konstante CW_USEDEFAULT besagt, dass Windows geeignete Voreinstellungen wählen soll. Die nächsten beiden Parameter (nicht erforderlich) stellen das Handle für das dem Fenster übergeordnete Fenster bzw. Menü bereit. Der letzte Parameter bietet die Möglichkeit, einen Zeigerwert an die Fensterprozedur während der Erstellung zu übergeben. Wenn alles gut geht, erscheint ein Fenster auf dem Desktop, und es wird ein Fensterhandle zurückgegeben. Wenn nicht, wird stattdessen der „nullptr“ zurückgegeben, und Sie können die GetLastError-Funktion aufgerufen, um den Grund abzufragen. Trotz all des Geredes über die Schwierigkeiten bei der Verwendung der Windows-API stellt sich also heraus, dass die Erstellung eines Fensters eigentlich ganz einfach ist und der Vorgang sich wie folgt zusammenfassen lässt:

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

Es ist wichtig, dass Ihre App so schnell wie möglich mit der Nachrichtenverarbeitung beginnt, sobald das Fenster sichtbar ist, da es sonst den Anschein hat, als würde sie nicht reagieren. Windows ist grundsätzlich ein ereignisgesteuertes, auf Nachrichtenaustausch basierendes Betriebssystem. Dies gilt insbesondere für den Desktop. Windows erstellt und verwaltet zwar die Nachrichtenwarteschlange, aber es liegt in der Verantwortung der App, die Nachrichten abzurufen und weiterzuleiten, weil Nachrichten an den Thread eines Fensters und nicht direkt an das Fenster selbst gesendet werden. Dies bietet ein hohes Maß an Flexibilität, und eine einfache Nachrichtenschleife muss nicht kompliziert sein:

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

Diese scheinbar einfache Nachrichtenschleife wird oft falsch implementiert. Dies resultiert aus der Tatsache, dass der Prototyp der GetMessage-Funktion einen BOOL-Wert zurückgibt, der ja aber tatsächlich nur ein Ganzzahlwert ist. „GetMessage“ entnimmt eine Nachricht der Warteschlange des aufrufenden Threads. Diese kann für jedes Fenster oder kein Fenster bestimmt sein, aber in unserem Fall liefert der Thread nur Nachrichten für ein einzelnes Fenster. Wenn die WM_QUIT-Nachricht aus der Warteschlange entfernt wird, gibt „GetMessage“ den Wert 0 (Null) zurück, was darauf hinweist, dass das Fenster verschwunden ist, also keine Nachrichten mehr verarbeitet, und dass die App beendet werden soll. Wenn ein Fehler auftritt, wird „GetMessage“ -1 zurückgeben, und Sie können „GetLastError“ erneut aufrufen, um weitere Informationen zu erhalten. Jeder andere Rückgabewert von „GetMessage“, der nicht 0 (Null) ist, besagt, dass eine Nachricht aus der Warteschlange entfernt wurde und die Nachricht bereit ist, an das Fenster geschickt zu werden. Dies ist die Aufgabe der DispatchMessage-Funktion. Es gibt natürlich viele Varianten einer Nachrichtenschleife, sodass Sie die Möglichkeit haben, durch entsprechende Programmierung selbst festzulegen, wie sich Ihre App verhalten soll, welche Eingaben sie akzeptiert, und wie diese umgesetzt werden. Abgesehen vom MSG-Zeiger können die Parameter für „GetMessage“ verwendet werden, um gegebenenfalls Nachrichten zu filtern.

Die Fensterprozedur beginnt mit dem Empfangen von Nachrichten, bevor die CreateWindow-Funktion zurückkehrt, sodass es besser ist, bereit zu sein und zu warten. Aber was bedeutet das? Ein Fenster benötigt eine Nachrichtenliste oder -tabelle. Diese könnte aus einer Folge von if-else-Anweisungen oder einer großen switch-Anweisung innerhalb der Fensterprozedur bestehen. So etwas wird jedoch schnell unhandlich, und es wurde deshalb in verschiedenen Bibliotheken und Frameworks mit sehr viel Aufwand versucht, entsprechende Tabellen irgendwie zu verwalten. Tatsächlich ist dieser Aufwand nicht erforderlich, denn eine einfache statische Tabelle wird in vielen Fällen ausreichen. Dazu ist es hilfreich zu wissen, woraus eine Fensternachricht besteht. Der wichtigste Teil ist eine Konstante wie WM_PAINT oder WM_SIZE zur eindeutigen Identifizierung der Nachricht. Für jede Nachricht sind sozusagen zwei Argumente vorgesehen, und diese werden als WPARAM bzw. LPARAM bezeichnet. Je nach Nachricht kann es ein, dass sie keine Informationen enthalten. Schließlich erwartet Windows nach Verarbeitung bestimmter Nachrichten die Rückgabe eines Werts, der LRESULT genannt wird. Die meisten Meldungen, die Ihre App verarbeitet, werden nur den Wert 0 (Null) zurückgeben.

Dank dieser Informationen können wir für die Nachrichtenverarbeitung eine einfache Tabelle mit folgenden Typen als Bausteinen erstellen:

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

Als Minimum können wir dann eine statische Tabelle mit Nachrichtenhandlern aufbauen, wie in Abbildung 1 dargestellt.

Abbildung 1 Eine statische Tabelle mit Nachrichtenhandlern

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;     }   } };

Die Nachricht WM_PAINT trifft ein, sobald das Fenster neu aufgebaut werden muss. Dies geschieht viel seltener als in früheren Versionen von Windows, dank der Fortschritte beim Rendern und beim Desktopaufbau. Die Funktionen „BeginPaint“ und „EndPaint“ sind GDI-Relikte, werden aber noch benötigt, selbst wenn Sie ein ganz anderes Renderingmodul verwenden. Der Grund ist, dass sie Windows durch Validierung der Zeichenoberfläche des Fensters das Ende des Rendervorgangs mitteilen. Ohne diese Aufrufe würde Windows die WM_PAINT-Nachricht als unbeantwortet betrachten, und Ihr Fenster würde einen stetigen Strom unnötiger WM_PAINT-Nachrichten erhalten.

Die Nachricht WM_DESTROY trifft ein, nachdem das Fenster verschwunden ist, sodass Sie wissen, dass es zerstört wurde. Dies ist normalerweise ein Indikator dafür, dass die App beendet werden soll, aber die GetMessage-Funktion innerhalb der Nachrichtenschleife wartet noch immer auf die WM_QUIT-Nachricht. Das Übermitteln dieser Nachricht ist die Aufgabe der PostQuitMessage-Funktion. Ihr einziger Parameter enthält einen Wert, der von WM_QUIT in WPARAM übergeben wird und eine Möglichkeit darstellt, verschiedene Abschlusscodes beim Beendigen der App zurückzugeben.

Als letztes Stück des Puzzles muss die endgültige Fensterprozedur implementiert werden. Ich habe den inneren Teil des Lambda-Ausdrucks ausgelassen, mit dem ich die WNDCLASS-Struktur zuvor vorbereitet habe, aber angesichts dessen, was Sie jetzt bereits wissen, sollte es nicht schwer sein, den Aufbau zu erkennen:

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); };

In der for-Schleife wird ein entsprechender Handler erwartet. Glücklicherweise stellt Windows eine Standardbehandlung für alle Nachrichten zur Verfügung, die Sie nicht selbst verarbeiten möchten. Diese Aufgabe übernimmt die DefWindowProc-Funktion.

Und das war schon alles – wenn Sie bis hierher gekommen sind, haben Sie ein Desktopfenster mithilfe der Windows-API erstellt!

Eine Portion ATL

Das Problem mit diesen Funktionen der Windows-API ist, dass sie lange vor der Einführung von C++ entwickelt wurden und somit nicht den Prinzipien einer objektorientierten Sicht auf die Welt entsprechen. Dennoch konnte diese API im C-Stil durch kluge Codierung so umgewandelt werden, dass sie den Bedürfnissen eines heutigen C++-Programmierers entspricht. Das Ergebnis ist ATL, eine Bibliothek mit Klassenvorlagen und Makros, die es Ihnen ermöglichen, mehr als nur einige Fensterklassen zu verwalten oder weiterhin USER- und GDI-Ressourcen zur Implementierung von Fenstern zu verwenden. Die Abbildung 2 zeigt, wie das Fenster aus dem vorherigen Abschnitt mit ATL erstellt werden kann.

Abbildung 2 Erstellen eines Fensters mit 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;   } };

Die CWindowImpl-Klasse ermöglicht das notwendige Routing von Nachrichten. „CWindow“ ist eine Basisklasse, die sehr viele Wrapper für Memberfunktionen enthält, sodass Sie das Fensterhandle nicht explizit für jeden Funktionsaufruf bereitstellen müssen. Sie können das im Beispiel anhand der Funktionsaufrufe „BeginPaint“ und „EndPaint“ erkennen. Die Vorlage „CWinTraits“ stellt die Konstanten für den Fensterstil bereit, die bei der Erstellung verwendet werden.

Die Makros beruhen auf MFC und arbeiten mit „CWindowImpl“ zusammen, um eingehende Nachrichten den entsprechenden Memberfunktionen zur Bearbeitung zuzuordnen. Jedem Handler wird die Nachrichtenkonstante als erstes Argument übergeben. Dies ist nützlich, wenn Sie eine Vielzahl von Nachrichten mit einer einzigen Memberfunktion verarbeiten müssen. Der letzte Parameter hat den Standardwert TRUE und lässt den Handler zur Laufzeit entscheiden, ob er die Nachricht verarbeiten will oder ob Windows – oder sogar ein anderer Handler – sich darum kümmern soll. Diese Makros sind, zusammen mit „CWindowImpl“, sehr mächtig und ermöglichen Ihnen die Verarbeitung reflektierter Meldungen, die Verkettung von Nachrichtentabellen usw.

Um das Fenster zu erstellen, müssen Sie die Create-Memberfunktion verwenden, die Ihr Fenster von „CWindowImpl“ erbt, und diese wiederum ruft die guten alten Funktionen „RegisterClass“ und „CreateWindow“ für Sie auf:

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

An dieser Stelle muss der Thread wiederum schnell damit beginnen, Nachrichten zu verarbeiten, wozu die Windows-API-Nachrichtenschleife aus dem vorherigen Abschnitt genügt. ATL kommt eigentlich erst infrage, wenn Sie mehrere Fenster auf einem einzigen Thread verwalten müssen, denn mit nur einem einzigen Fenster der obersten Ebene gibt es kaum einen Unterschied zum Windows-API-Ansatz aus dem vorherigen Abschnitt.

WTL: Eine zusätzliche Portion ATL

Während ATL in erster Linie dazu gedacht ist, die Entwicklung von COM-Servern zu vereinfachen, und nur ein einfaches (und doch äußerst effektives) Behandlungsmodell für Fenster bereitstellt, besteht WTL aus einer Reihe weiterer Klassenvorlagen und Makros, die speziell zur Erstellung komplexer Fenster auf Grundlage von USER- und GDI-Ressourcen entwickelt wurden. WTL steht nun auf SourceForge (wtl.sourceforge.net) zur Verfügung, ist aber für neue Apps, die moderne Renderingmodule verwenden, nicht unbedingt von Vorteil. Einige Funktionen sind aber nützlich. Aus der WTL-Headerdatei „atlapp.h“ können Sie die Nachrichtenschleifenimplementierung verwenden, um die Version zu ersetzen, die ich zuvor skizziert habe.

CMessageLoop loop; loop.Run();

WTL lässt sich einfach in Ihre App integrieren und ist einfach zu benutzen und äußerst nützlich, wenn Sie besondere Anforderungen an Nachrichtenfilterung und Routing stellen. WTL enthält zudem „atlcrack.h“ mit Makros, die das generische Makro MESSAGE_HANDLER aus ATL ersetzen. Diese sind nicht nur komfortabler, sondern vereinfachen auch die Behandlung einer neuen Nachricht, da sie diese sozusagen analysieren und Ihnen damit die jeweils richtige Interpretation von WPARAM und LPARAM ermöglichen. Ein gutes Beispiel ist WM_SIZE, das den neuen Clientbereich für das Fenster im LOWORD und HIWORD des LPARAM bereitstellt. Mit ATL könnte das wie folgt aussehen:

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; }

Mithilfe von WTL ist es etwas einfacher:

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 ... }

 

Beachten Sie das neue Makro MSG_WM_SIZE, welches das generische Makro MESSAGE_HANDLER aus der originalen Nachrichtentabelle ersetzt. Die Memberfunktion zur Behandlung der Nachricht ist ebenfalls einfacher. Wie Sie sehen, sind weder unnötige Parameter noch ein Rückgabewert vorhanden. Der erste Parameter ist einfach der WPARAM, den Sie analysieren können, wenn Sie wissen möchten, wodurch die Größenänderung verursacht wurde.

Das Angenehme an ATL und WTL ist, dass beide in Form einer Anzahl von Headerdateien vorliegen, die Sie nach Bedarf übernehmen können. Sie verwenden, was Sie benötigen, und ignorieren den Rest. Allerdings können Sie, wie gezeigt, auch ohne diese Bibliotheken auskommen und eine App nur mithilfe der Windows-API programmieren. Beim nächsten Mal zeige ich Ihnen einen modernen Ansatz zum Rendern der Pixel im Fenster Ihrer App.

Kenny Kerr ist ein Programmierer aus Kanada, Autor bei Pluralsight und ein Microsoft MVP. Er veröffentlicht Blogs unter kennykerr.ca, und Sie können ihm auf Twitter unter twitter.com/kennykerr folgen.

Unser Dank gilt dem folgenden technischen Experten für die Durchsicht dieses Artikels: Worachai Chaoweeraprasit