Was ist MFC unter Windows? — Grundlagenwissen zur Pflege bestehender Bestände

· · Windows, MFC, Visual C++, C++, Win32, Native App, Desktop-Anwendung, Legacy-Code, Legacy-Bestände

1. Das Wichtigste zuerst

Wer alte Windows-Desktopanwendungen pflegt, stößt gelegentlich auf Namen wie diese.

CWinApp
CWnd
CDialog
CDialogEx
CFrameWnd
CDocument
CView
CString
CFile
CArchive
BEGIN_MESSAGE_MAP
ON_COMMAND
ON_BN_CLICKED
DoDataExchange
UpdateData

Das sind Namen, die in MFC, einem Windows-Anwendungsframework für C++, ständig vorkommen. MFC steht für Microsoft Foundation Classes — eine Bibliothek, die die Win32-API als C++-Klassen leichter handhabbar macht.

In der heutigen Windows-App-Entwicklung gibt es mit WinUI, WPF, Windows Forms, Electron, Qt und diversen Web-Technologien viele Optionen, sodass MFC seltener die erste Wahl für Neuentwicklungen ist. Eine ausgestorbene Technologie ist es deshalb aber nicht. In Fachanwendungen, Messgeräten, Steuerungssoftware, CAD/CAM, internen Werkzeugen und langlebiger Standardsoftware wird bis heute an MFC-Codebasen gearbeitet.

Hier zunächst die Perspektiven, die es sich lohnt im Blick zu behalten, um MFC zu verstehen.

MFC ist eine wichtige Landkarte, um alte Windows-Desktopanwendungen zu lesen
MFC verbirgt die Win32-API nicht, sondern verpackt sie C++-gerecht
Ohne die Konventionen von MFC zu kennen, liest man das Verhalten leicht falsch, mehr noch als es der bloße Codeanblick vermuten lässt
Der Wert liegt weniger im Neueinsatz als in Pflege, Verlängerung der Lebensdauer und schrittweiser Migration bestehender Bestände

Dieser Artikel behandelt den Überblick über MFC, den Anwendungsaufbau, Message Maps, Document/View, Dialoge, DDX/DDV, Ressourcen, den Build sowie die Punkte, auf die bei der Pflege zu achten ist.

Als Leserschaft ist an Personen gedacht, die C++ zwar beherrschen, mit der Win32-API und MFC aber noch keine Erfahrung haben. Als Richtwert für das Vorwissen genügt Folgendes.

Bereich Erwartetes Niveau
C++ Klassen, Vererbung, virtuelle Funktionen, Zeiger und Referenzen lesen können
Win32-API Keine Erfahrung nötig. Es reicht, die Begriffe „Fenster“ und „Nachricht“ schon einmal gehört zu haben
Visual Studio Eine Solution schon einmal geöffnet und gebaut zu haben
COM/OLE Keine Erfahrung nötig. Es genügt, bei Bedarf in Kapitel 27 nachzuschlagen

Am Ende von Kapitel 2 ist „das Wissen, das man zum Lesen von MFC braucht“ aufgeführt — es ist aber nicht nötig, sich das vorab vollständig anzueignen. Es reicht, beim Lesen bei Bedarf dorthin zurückzukehren.

Auch die Lesereihenfolge muss nicht von Kapitel 1 bis Kapitel 41 durchgehend sein. Hier die kürzesten Wege je nach Ziel.

Ziel Zu lesende Kapitel
Nur wissen, was MFC ist und wo es heute steht Kapitel 2–4, 36
Bestehenden MFC-Code lesen können Kapitel 6 → 9 → 10 → 14 → 12/13 → 38
Einen fehlgeschlagenen Build reparieren Kapitel 5, 25, 32, 33
Einen Fehler untersuchen Kapitel 34 → 35 → 8/22
Pflege- oder Migrationsstrategie festlegen Kapitel 30, 31, 37, 39

Am wichtigsten zuerst zu verstehen sind Message Maps (Kapitel 9) und Document/View (Kapitel 14). Sind diese beiden klar, lässt sich lesen, „warum eine Funktion ausgeführt wird, deren Aufrufer man nicht findet“ und „wie Daten und Bildschirm miteinander verbunden sind“. Überspringt man dagegen genau diese beiden, hängen die Erklärungen der übrigen Kapitel in der Luft.

Die in diesem Artikel vorkommenden Code-Ausschnitte sind zudem, nach Kapiteln in Dateien geordnet, als Referenzsammlung auf GitHub veröffentlicht.

windows-mfc-overview - komurasoft-blog-samples (GitHub)

2. Was ist MFC?

MFC ist eine Klassenbibliothek, um native Windows-Desktopanwendungen in C++ zu erstellen.

Verwendet man die Win32-API direkt, schreibt man typischerweise Code wie diesen.

LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    switch (message)
    {
    case WM_PAINT:
        // Zeichnen
        break;
    case WM_DESTROY:
        PostQuitMessage(0);
        break;
    default:
        return DefWindowProc(hWnd, message, wParam, lParam);
    }
    return 0;
}

Die Win32-API ist sehr mächtig, baut aber im Kern auf C-Funktionen, Handles, Nachrichten und Callbacks auf, wodurch große Anwendungen schnell unübersichtlich werden. MFC macht diese als C++-Klassen handhabbar: Ein Fenster wird als CWnd, ein Dialog als CDialog, die Anwendung als Ganzes als CWinApp, ein Rahmenfenster als CFrameWnd und eine Ansicht als CView dargestellt.

class CMainFrame : public CFrameWnd
{
public:
    CMainFrame();

protected:
    afx_msg int OnCreate(LPCREATESTRUCT lpCreateStruct);
    DECLARE_MESSAGE_MAP()
};

MFC ersetzt die Win32-API nicht durch etwas völlig anderes, sondern ist ein dünnes, aber breit angelegtes C++-Framework, das auf dem Denkansatz der Win32-API aufbaut. Um MFC zu lesen, braucht man deshalb nicht nur Wissen über die MFC-Klassen, sondern auch über Themen wie diese.

Windows-Nachrichten
Handles wie HWND
GDI/GDI+
Ressourcendateien
COM/OLE
DLLs und Laufzeitumgebungen
Zeichenkodierung
Threads und Nachrichtenschleifen

„Eine magische Bibliothek, mit der man schreiben kann, ohne Windows zu kennen“ trifft es weniger als „die Mechanik von Windows, geordnet in C++-Typen und ein Framework“ — das ist das eigentliche Wesen von MFC.

3. Ist MFC heute noch nutzbar?

MFC lässt sich weiterhin in Visual Studio verwenden. Ein Missverständnis über seine Stellung sollte man dabei allerdings vermeiden. Es wird zwar weiterhin unterstützt, ist aber kein aktiv um neue Funktionen erweitertes, modernes UI-Framework — auch in Microsofts MFC-Dokumentation findet sich der Hinweis, dass MFC zwar weiterhin unterstützt wird, aber keine neuen Funktionen mehr hinzukommen und die Dokumentation nicht mehr aktualisiert wird.

Damit lässt sich die Stellung von MFC ungefähr so zusammenfassen.

Pflege bestehender MFC-Anwendungen             -> in der Praxis üblich
Funktionserweiterung bestehender MFC-Anwendungen -> denkbar
Aktualisierung der Build-Umgebung einer MFC-Anwendung -> wichtig
Schrittweise Migration von MFC zu einer anderen UI -> denkbar
Einsatz bei einer komplett neuen, allgemeinen GUI-Anwendung -> sorgfältig abwägen

Insbesondere in lange genutzten Fachanwendungen sind UI, Druck, Dateiein-/ausgabe, Gerätesteuerung, proprietäre Protokolle und COM-Integration oft gebündelt in MFC zu finden.

Bei einer solchen Codebasis braucht es vor „MFC loswerden“ zunächst „MFC in einen lesbaren Zustand bringen“.

4. Die Domäne, in der MFC stark war

Der klassische Einsatzbereich von MFC sind native Windows-Desktopanwendungen.

Konkret handelt es sich um Anwendungen wie diese.

Dialogzentrierte Fachwerkzeuge
SDI-Anwendungen, die Dateien öffnen und bearbeiten
MDI-Anwendungen, die mehrere Dokumente verwalten
Steuerungsbildschirme für Messgeräte und Fertigungsanlagen
Native CAD/CAM-Anwendungen
Anwendungen mit intensiver Nutzung von Druck und Vorschau
Anwendungen mit ActiveX- oder OLE-Integration
Anwendungen, die eng mit alten Windows-APIs und COM-Beständen verzahnt sind

Die Stärke von MFC ist, dass es nah an nativen Windows-Bausteinen arbeitet. Fenster, Menüs, Symbolleisten, Statusleisten, Dialoge, Standardsteuerelemente, Druck, Dateidialoge, die Registry und GDI-Zeichnungen lassen sich als C++-Klassen handhaben.

Die Schwäche von MFC ist dagegen, dass sich modernere Themen wie zeitgemäßer UI-Aufbau, Datenbindung, Testbarkeit, asynchrone Verarbeitung, moderne Layouts, hohe DPI-Unterstützung, Mehrsprachigkeit und Barrierefreiheit nicht so natürlich schreiben lassen wie in neueren Frameworks.

Zusammengefasst ergeben sich diese Merkmale.

Nah an nativem Windows
Direkt aus C++ steuerbar
Viele bestehende Bestände
Win32-Wissen erforderlich
Viele alte Konventionen
Testfreundliche Struktur muss selbst geschaffen werden

5. Vorbereitungen, um MFC in Visual Studio zu nutzen

Auch wenn C++ in Visual Studio installiert ist, ist MFC nicht zwingend mit dabei. Denn MFC wird als individuelle Komponente des Visual Studio Installers behandelt. Typischerweise prüft man diese Komponenten.

Desktopentwicklung mit C++
MSVC v143 - VS 2022 C++ x64/x86-Buildtools
Windows SDK
C++ MFC for latest v143 build tools
C++ ATL for latest v143 build tools
Ob die Spectre-Mitigations-Variante von MFC benötigt wird

Werden beim Build MFC-bezogene Dateien nicht gefunden, sollte man nicht nur die Projekteinstellungen, sondern auch prüfen, ob die MFC-Komponente in Visual Studio überhaupt installiert ist.

Die Auswahl im Installer erfolgt in diesen Schritten.

1. Den „Visual Studio Installer“ im Startmenü öffnen
2. Bei der installierten Visual-Studio-Version auf „Ändern“ klicken
3. Im Tab „Workloads“ „Desktopentwicklung mit C++“ ankreuzen
4. Rechts im selben Fenster unter „Installationsdetails“
   „C++ MFC for latest v143 build tools“ ankreuzen
5. Wird die Komponente nicht gefunden, zum Tab „Einzelne Komponenten“ wechseln
   und im Suchfeld „MFC“ eingeben
6. Auf „Ändern“ klicken, um zu installieren

Allein die Auswahl der Workload „Desktopentwicklung mit C++“ installiert MFC unter Umständen nicht mit, daher sollte man in Schritt 4 oder 5 unbedingt prüfen, ob die Komponente selbst angekreuzt ist.

Wird die Installation per Skript oder aus CI heraus vorgenommen, gibt man die Komponenten-ID an.

Anzeigename Komponenten-ID
C++ MFC for latest v143 build tools Microsoft.VisualStudio.Component.VC.ATLMFC
C++ ATL for latest v143 build tools Microsoft.VisualStudio.Component.VC.ATL
Desktopentwicklung mit C++ (Workload der Build Tools) Microsoft.VisualStudio.Workload.VCTools

Ob die Komponente vorhanden ist, lässt sich am Vorhandensein des Ordners atlmfc erkennen, in dem die MFC-Header liegen. Header und Bibliotheken von MFC liegen unterhalb des MSVC-Toolsets.

<Installationsverzeichnis von Visual Studio>\VC\Tools\MSVC\<Toolset-Version>\atlmfc\include\afxwin.h

Mit PowerShell lässt sich danach so suchen.

Get-ChildItem -Path "$env:ProgramFiles\Microsoft Visual Studio\2022" -Recurse -Filter afxwin.h -ErrorAction SilentlyContinue |
    Select-Object -ExpandProperty FullName

Erscheint nichts, ist die MFC-Komponente nicht installiert.

Baut man in diesem Zustand, bleibt der Build beim Einbinden von afxwin.h stehen. Typischerweise erscheint dieser Kompilierfehler.

fatal error C1083: Cannot open include file: 'afxwin.h': No such file or directory

Diesen Fehler als „falsch gesetzten Include-Pfad“ zu deuten und in den Projekteigenschaften danach zu suchen, ist ein beliebter Umweg. Werden afxwin.h oder afxdialogex.h nicht gefunden, sollte man zuerst den Visual Studio Installer verdächtigen.

Dasselbe gilt für CI-Umgebungen und Build-Server: Baut ein Projekt lokal in Visual Studio, aber nicht in der CI, kann das Fehlen der MFC-Komponente oder eine abweichende Toolset-Version die Ursache sein.

6. Grundstruktur einer MFC-Anwendung

Eine MFC-Anwendung hat üblicherweise ungefähr diese Struktur.

Von CWinApp abgeleitete Klasse
  Übernimmt Initialisierung und Beendigung der gesamten Anwendung

Von CFrameWnd / CMDIFrameWnd / CDialog abgeleitete Klasse
  Übernimmt Hauptfenster oder Dialog

Von CView abgeleitete Klasse
  Übernimmt Bildschirmdarstellung und Benutzerinteraktion

Von CDocument abgeleitete Klasse
  Übernimmt Daten und das Speichern von Dateien

Ressourcendatei
  Enthält Menüs, Dialoge, Symbole, Zeichenketten usw.

Message Map
  Verknüpft Windows-Nachrichten und Befehle mit Handler-Funktionen

In einer einfachen MFC-Anwendung taucht beispielsweise eine solche von CWinApp abgeleitete Klasse auf.

class CMyApp : public CWinApp
{
public:
    virtual BOOL InitInstance();
};

CMyApp theApp;

BOOL CMyApp::InitInstance()
{
    CWinApp::InitInstance();

    CMainFrame* pFrame = new CMainFrame;
    m_pMainWnd = pFrame;

    pFrame->Create(nullptr, _T("My MFC Application"));
    pFrame->ShowWindow(SW_SHOW);
    pFrame->UpdateWindow();

    return TRUE;
}

CWinApp ist die Klasse, die die gesamte Anwendung repräsentiert. In einer MFC-Anwendung existiert normalerweise genau ein von CWinApp abgeleitetes Objekt.

Ein globales Objekt wie dieses theApp mag zunächst befremdlich wirken, ist in MFC aber die Standardstruktur.

7. Was macht CWinApp?

CWinApp ist als Einstiegspunkt einer MFC-Anwendung wichtig. In einer gewöhnlichen Win32-Anwendung schreibt man WinMain, die Registrierung der Fensterklasse und die Nachrichtenschleife selbst; in MFC übernimmt das Framework das meiste davon. Entwickler überschreiben vor allem InitInstance, um anwendungsspezifische Initialisierung zu schreiben.

BOOL CMyApp::InitInstance()
{
    CWinApp::InitInstance();

    // Einstellungen laden
    // COM initialisieren
    // Hauptfenster erstellen
    // Dokumentvorlage registrieren

    return TRUE;
}

In InitInstance findet man typischerweise Verarbeitungen wie diese.

Initialisierung der gemeinsamen Steuerelemente
Setzen von Registry-Schlüsseln
Laden der Liste zuletzt verwendeter Dateien
Registrierung der Dokumentvorlage
Erzeugung des Hauptrahmens
Verarbeitung von Kommandozeilenargumenten
Initialisierung von COM/OLE

Bei der Pflege lohnt es sich zunächst, die von CWinApp abgeleitete Klasse zu prüfen — dadurch wird die Startreihenfolge der gesamten Anwendung besser erkennbar.

8. CWnd ist die zentrale Klasse in MFC

Viele der UI-Klassen von MFC basieren auf CWnd, das ein Windows-Fenster repräsentiert. CWnd-Objekt und HWND sind allerdings nicht dasselbe.

HWND
  Vom Windows-Betriebssystem verwalteter Fenster-Handle

CWnd
  C++-Wrapper-Objekt, das HWND leichter handhabbar macht

In MFC hält CWnd intern ein HWND.

HWND hWnd = m_hWnd;

Alternativ lässt es sich so abrufen.

HWND hWnd = GetSafeHwnd();

Bei der Pflege ist wichtig: Auch wenn ein CWnd* existiert, kann das zugehörige HWND bereits zerstört sein.

Ob ein Fenster gültig ist, prüft man daher etwa so.

if (pWnd != nullptr && ::IsWindow(pWnd->GetSafeHwnd()))
{
    pWnd->ShowWindow(SW_SHOW);
}

Bei der Fehlersuche in MFC ist entscheidend zu prüfen, ob die Lebensdauer des C++-Objekts CWnd und die Lebensdauer des tatsächlichen Windows-Fenster-Handles auseinanderlaufen.

9. Was ist die Message Map?

Eines der Konstrukte, in denen sich der Charakter von MFC am deutlichsten zeigt, ist die Message Map.

Windows-Anwendungen empfangen Mausklicks, Tastatureingaben, Neuzeichnungen, Fenstergrößenänderungen, Menüauswahlen usw. als Windows-Nachrichten.

In der Win32-API werden Nachrichten normalerweise in einer switch-Anweisung von WndProc verarbeitet.

MFC verknüpft sie stattdessen über die Message Map mit Handler-Funktionen.

BEGIN_MESSAGE_MAP(CMyDialog, CDialogEx)
    ON_BN_CLICKED(IDC_BUTTON_OK, &CMyDialog::OnClickedButtonOk)
    ON_WM_CLOSE()
END_MESSAGE_MAP()

void CMyDialog::OnClickedButtonOk()
{
    AfxMessageBox(_T("Clicked"));
}

Der Sinn dieses Codes ist:

Wenn die Schaltfläche IDC_BUTTON_OK geklickt wird,
rufe CMyDialog::OnClickedButtonOk auf

Der Ablauf vom Eintreffen einer Nachricht bis zum Aufruf des Handlers lässt sich so darstellen.

janeingefundenbis zuletzt nicht gefundenWindows-NachrichtWM_COMMAND / WM_PAINT usw.Nachrichtenschleifevom MFC-Framework betriebenCWnd::WindowProcvon MFC bereitgestellter gemeinsamer EinstiegspunktGibt es einen passenden Eintragin der Message Map der eigenen Klasse?Ruft die von ON_BN_CLICKED usw. referenzierteHandler-Funktion aufSucht in der Message Map der Basisklassevon CDialogEx über CDialog bis CWndIrgendwo gefunden?DefWindowProcüberlässt es der Windows-Standardverarbeitung

Dass keine switch-Anweisung zu sehen ist, liegt daran, dass das Framework diesen Teil — „von der eigenen Klasse zur Basisklasse durch die Message Map wandern“ — übernimmt. DECLARE_MESSAGE_MAP und BEGIN_MESSAGE_MAP lassen sich am besten als Makros verstehen, die für diese Suche pro Klasse eine Zuordnungstabelle bereitstellen.

Wer MFC nicht gewohnt ist, empfindet es oft als unklar, woher eine Funktion aufgerufen wird. Findet die Suche keinen direkten Aufruf, schaut man sich die Message Map an.

Funktion wird nirgends direkt aufgerufen
wird aber bei einem Ereignis ausgeführt
-> BEGIN_MESSAGE_MAP / ON_...-Makros prüfen

Bei einem MFC-Code-Review ist es wichtig, nicht nur die Handler-Funktion zu betrachten, sondern sie stets zusammen mit der Message Map zu prüfen.

10. Command Routing

In MFC werden auch Bedienungen über Menü und Symbolleiste als Befehle (Commands) behandelt.

Typisch dafür ist ON_COMMAND.

BEGIN_MESSAGE_MAP(CMainFrame, CFrameWnd)
    ON_COMMAND(ID_FILE_OPEN, &CMainFrame::OnFileOpen)
END_MESSAGE_MAP()

void CMainFrame::OnFileOpen()
{
    // Datei öffnen
}

MFC verfügt über einen Mechanismus, der Befehle an das jeweils passende Objekt weiterleitet.

Selbst bei derselben ID_EDIT_COPY kann es je nach Situation unterschiedlich sein, ob die aktuell aktive Ansicht, das Dokument, der Rahmen oder die Anwendung den Befehl verarbeitet.

Aktive Ansicht
Dokument
Rahmenfenster
Anwendung

In dieser Reihenfolge wird der Befehl an das jeweils verarbeitungsfähige Objekt weitergereicht.

Deshalb lässt sich in MFC „welche Funktion beim Drücken eines Menüpunkts aufgerufen wird“ mitunter nicht allein durch eine einfache Textsuche verfolgen.

Bei der Pflege sind unter anderem diese Punkte relevant.

Wie lautet die Befehls-ID?
In welcher Klasse liegt ON_COMMAND?
Wo liegt ON_UPDATE_COMMAND_UI?
Welche Ansicht ist aktuell aktiv?
Wird die Document/View-Struktur verwendet?

11. Was ist ON_UPDATE_COMMAND_UI?

In MFC wird ON_UPDATE_COMMAND_UI verwendet, um Aktivierung/Deaktivierung, Häkchen-Status oder den angezeigten Text von Menüpunkten und Symbolleistenschaltflächen zu aktualisieren.

BEGIN_MESSAGE_MAP(CMainFrame, CFrameWnd)
    ON_COMMAND(ID_EDIT_DELETE, &CMainFrame::OnEditDelete)
    ON_UPDATE_COMMAND_UI(ID_EDIT_DELETE, &CMainFrame::OnUpdateEditDelete)
END_MESSAGE_MAP()

void CMainFrame::OnUpdateEditDelete(CCmdUI* pCmdUI)
{
    pCmdUI->Enable(CanDeleteCurrentItem());
}

Damit lassen sich Menü oder Schaltfläche nur dann aktivieren, wenn ein Löschen möglich ist.

Beobachtet man bei einer MFC-Anwendung, dass eine Schaltfläche unerklärlich ausgegraut ist, ein Menü sich nicht drücken lässt oder sich ein Häkchen-Status ändert, findet man die Ursache oft, indem man nach ON_UPDATE_COMMAND_UI sucht.

12. Dialogbasierte MFC-Anwendungen

Eine der am leichtesten verständlichen Formen von MFC ist die dialogbasierte Anwendung.

Einstellungsbildschirme, einfache Fachwerkzeuge oder Bedienbildschirme für Geräte sind mitunter um einen Dialog herum aufgebaut.

Typischerweise erbt man dabei von CDialog oder CDialogEx.

class CSettingsDialog : public CDialogEx
{
public:
    CSettingsDialog(CWnd* pParent = nullptr);

#ifdef AFX_DESIGN_TIME
    enum { IDD = IDD_SETTINGS_DIALOG };
#endif

protected:
    virtual void DoDataExchange(CDataExchange* pDX);
    virtual BOOL OnInitDialog();

    afx_msg void OnBnClickedOk();
    DECLARE_MESSAGE_MAP()

private:
    CString m_name;
    int m_interval;
};

In dialogbasiertem Code treten häufig diese Elemente auf.

IDD_...        Dialogressourcen-ID
IDC_...        Steuerelement-ID
OnInitDialog   Initialisierungsverarbeitung
DoDataExchange Verknüpfung von Steuerelementen und Mitgliedsvariablen
UpdateData     Synchronisation von Bildschirm und Variablen
ON_BN_CLICKED  Verarbeitung von Schaltflächenklicks

Ein Dialog ist nicht nur eine Optik — Ressource, Mitgliedsvariablen, Message Map und Initialisierungsverarbeitung wirken zusammen.

13. DDX und DDV

Häufig anzutreffen in MFC-Dialogen sind DDX und DDV.

DDX = Dialog Data Exchange
DDV = Dialog Data Validation

DDX ist der Mechanismus, der Steuerelemente im Dialog mit C++-Mitgliedsvariablen verknüpft; DDV validiert deren Eingabewert.

void CSettingsDialog::DoDataExchange(CDataExchange* pDX)
{
    CDialogEx::DoDataExchange(pDX);
    DDX_Text(pDX, IDC_EDIT_NAME, m_name);
    DDX_Text(pDX, IDC_EDIT_INTERVAL, m_interval);
    DDV_MinMaxInt(pDX, m_interval, 1, 3600);
}

Ruft man UpdateData(TRUE) auf, werden die Eingaben des Bildschirms in die Mitgliedsvariablen übernommen.

void CSettingsDialog::OnBnClickedOk()
{
    if (!UpdateData(TRUE))
    {
        return;
    }

    // An dieser Stelle enthalten m_name und m_interval die Bildschirmeingaben
    SaveSettings(m_name, m_interval);

    CDialogEx::OnOK();
}

Ruft man umgekehrt UpdateData(FALSE) auf, werden die Werte der Mitgliedsvariablen auf den Bildschirm übertragen.

BOOL CSettingsDialog::OnInitDialog()
{
    CDialogEx::OnInitDialog();

    m_name = _T("default");
    m_interval = 60;
    UpdateData(FALSE);

    return TRUE;
}

Wenn in einem MFC-Dialog die Eingabewerte nicht stimmen, prüft man unter anderem Folgendes.

Ist in DoDataExchange ein DDX definiert?
Wird UpdateData(TRUE) aufgerufen?
Erfolgt der Aufruf von UpdateData(FALSE) zum richtigen Zeitpunkt?
Wird die Eingabe von DDV zurückgewiesen?
Stimmt die Steuerelement-ID mit der Ressource überein?

14. Die Document/View-Architektur

Eines der großen Merkmale von MFC ist die Document/View-Architektur.

Sie ist eine Struktur, um die von einer Anwendung verarbeiteten Daten von deren Darstellung zu trennen.

CDocument
  Hält die Daten
  Übernimmt das Lesen und Schreiben von Dateien
  Benachrichtigt mehrere Ansichten über Aktualisierungen

CView
  Zeigt die Daten an
  Verarbeitet Benutzerinteraktion
  Verwaltet Zeichnung und Auswahlzustand

Die Beziehung zwischen Rahmen, Ansicht und Dokument lässt sich so darstellen.

Abruf über GetDocumentBenachrichtigung über UpdateAllViewsVon CWinApp abgeleitetStart und Beendigung der gesamten AnwendungDokumentvorlageCSingleDocTemplate / CMultiDocTemplateRahmenfensterCFrameWnd / CMDIChildWndVon CDocument abgeleitetDaten und Dateiein-/ausgabeVon CView abgeleitetZeichnung und Benutzerinteraktion

Wichtig ist: Es ist die Dokumentvorlage, die Rahmen, Dokument und Ansicht miteinander verbindet. Da diese Vorlage innerhalb von InitInstance registriert wird, liest man zuerst InitInstance, wenn man wissen möchte, „welche Ansicht welches Dokument betrachtet“.

Wer von der Ansicht aus auf das Dokument zugreift, verwendet GetDocument; wer Änderungen des Dokuments an alle Ansichten weitergeben möchte, verwendet UpdateAllViews — behält man diesen Richtungsunterschied im Blick, lässt sich ein Fehler, bei dem der Bildschirm nicht aktualisiert wird, leichter aufspüren.

Bei Anwendungen wie Texteditoren, Grafikeditoren, Werkzeugen zur Bearbeitung von Konfigurationsdateien oder CAD-Programmen ergibt die Trennung von Daten und Darstellung beispielsweise Sinn.

class CMyDocument : public CDocument
{
public:
    std::vector<Item> m_items;

    virtual BOOL OnOpenDocument(LPCTSTR lpszPathName);
    virtual BOOL OnSaveDocument(LPCTSTR lpszPathName);
};

class CMyView : public CView
{
protected:
    virtual void OnDraw(CDC* pDC);

    CMyDocument* GetDocument() const;
};

Auf Seiten der Ansicht wird das Dokument abgerufen und gezeichnet.

void CMyView::OnDraw(CDC* pDC)
{
    CMyDocument* pDoc = GetDocument();
    if (pDoc == nullptr)
    {
        return;
    }

    for (const auto& item : pDoc->m_items)
    {
        // Mit pDC zeichnen
    }
}

Der Vorteil von Document/View ist, dass sich dieselben Daten leicht in mehreren Ansichten darstellen lassen.

Dieselben Daten lassen sich beispielsweise so präsentieren.

Tabellenansicht
Diagrammansicht
Detailansicht
Vorschau
Druckansicht

Bei einem einfachen Einstellungsbildschirm oder einem kleinen Werkzeug kann sich Document/View allerdings auch als unnötig schwergewichtige Struktur anfühlen.

Bei der Pflege lohnt es sich, zuerst zu klären, ob die Anwendung Document/View verwendet oder dialogzentriert ist — das erleichtert das Nachvollziehen des Codes.

15. SDI und MDI

In MFC treten in Kombination mit Document/View häufig die Strukturen SDI und MDI auf.

SDI = Single Document Interface
MDI = Multiple Document Interface

SDI ist im Grunde eine Form, bei der ein Rahmen ein Dokument verarbeitet.

Hauptfenster
  Ein Dokument
  Eine oder mehrere Ansichten

MDI ist eine Form, bei der ein übergeordnetes Fenster mehrere Kindfenster enthält, von denen jedes ein Dokument verarbeitet.

MDI-Elternrahmen
  MDI-Kindrahmen 1 -> Dokument 1
  MDI-Kindrahmen 2 -> Dokument 2
  MDI-Kindrahmen 3 -> Dokument 3

In älteren Windows-Anwendungen wurde häufig MDI verwendet.

Bei der Pflege lässt sich die Struktur schon am Klassennamen erahnen.

CFrameWnd       Rahmen des SDI-Typs
CMDIFrameWnd    MDI-Elternrahmen
CMDIChildWnd    MDI-Kindrahmen
CSingleDocTemplate Dokumentvorlage für SDI
CMultiDocTemplate  Dokumentvorlage für MDI

In von MFC-Assistenten erstellten Anwendungen findet sich innerhalb von InitInstance häufig die Registrierung von CSingleDocTemplate oder CMultiDocTemplate.

16. Ressourcendateien verstehen

In MFC-Anwendungen ist die .rc-Datei von großer Bedeutung.

.rc ist die Ressourcendatei von Windows.

Darin sind unter anderem definiert:

Dialogvorlagen
Menüs
Tastenkombinationen (Accelerator Keys)
Symbole
Bitmaps
Zeichenkettentabellen
Versionsinformationen
Symbolleisten

Zudem sind in resource.h die Ressourcen-IDs definiert.

#define IDD_SETTINGS_DIALOG  101
#define IDC_EDIT_NAME        1001
#define IDC_EDIT_INTERVAL    1002
#define ID_FILE_OPEN         32771

In MFC-Code werden diese IDs verwendet, um Ressource und C++-Code miteinander zu verknüpfen.

DDX_Text(pDX, IDC_EDIT_NAME, m_name);
ON_COMMAND(ID_FILE_OPEN, &CMainFrame::OnFileOpen)

Ein bei der Pflege häufiges Problem ist die Inkonsistenz von Ressourcen-IDs.

Die ID in resource.h hat sich geändert
Beim Merge eines anderen Branches ist eine ID kollidiert
Die Steuerelement-ID im Dialog stimmt nicht mit der ID in DDX überein
Eine eigentlich gelöschte Menü-ID ist noch vorhanden
Eine ID in der Zeichenkettentabelle ist doppelt vergeben

Um das Verhalten einer MFC-Anwendung zu untersuchen, muss man nicht nur den C++-Code, sondern gleichzeitig auch .rc und resource.h betrachten.

17. Class Wizard und handgeschriebener Code

MFC hat eine tief mit dem Class Wizard von Visual Studio verwobene Geschichte.

Mit dem Class Wizard lassen sich Nachrichten-Handler, DDX-Variablen und das Überschreiben virtueller Funktionen automatisch generieren.

Deshalb enthält MFC-Code oft noch vom Werkzeug generierte Formen.

//{{AFX_DATA(CSettingsDialog)
//}}AFX_DATA

//{{AFX_MSG(CSettingsDialog)
//}}AFX_MSG

In neueren Visual-Studio-Versionen hat sich zwar die Darstellung und die erzeugte Form geändert, in älteren Codebasen finden sich aber weiterhin solche Kommentarmarker.

Bei der Pflege ist wichtig, die Grenze zwischen generiertem und handgeschriebenem Code nicht achtlos zu zerstören.

Message Map nicht löschen
DDX-Zuordnung nicht zerstören
Ressourcen-IDs nicht unbedacht ändern
Alte, auf den Class Wizard bezogene Kommentare nicht grundlos entfernen

In MFC reicht es nicht aus, dass der Code als C++ kompiliert. Auch die vom Ressourceneditor und vom Class Wizard in Visual Studio erwartete Form muss bis zu einem gewissen Grad erhalten bleiben.

18. CString und Zeichenketten

Eine in MFC häufig auftretende Zeichenkettenklasse ist CString.

CString name = _T("Komura");
CString message;
message.Format(_T("Hello, %s"), name.GetString());

CString ist eine variable Zeichenkettenklasse, die in MFC-/ATL-Code häufig verwendet wird. Im modernen C++ kommen oft std::string oder std::wstring zum Einsatz, doch in MFC wird CString wegen der guten Kompatibilität mit APIs und Steuerelementen häufig eingesetzt.

Bei der Pflege muss man sich der Zeichenkodierung bewusst sein.

CString      entspricht je nach Projekteinstellung CStringA oder CStringW
CStringA     ANSI-/MBCS-basiert
CStringW     Unicode-/UTF-16-basiert
LPCTSTR      TCHAR-basierter Zeichenkettenzeiger
LPCSTR       char-basiert
LPCWSTR      wchar_t-basiert
std::string  in der Regel char-basiert
std::wstring wchar_t-basiert

In aktuellen Windows-Anwendungen ist es grundsätzlich sicherer, von Unicode auszugehen, in alten MFC-Anwendungen kann aber noch auf MBCS ausgelegter Code vorhanden sein.

CString text = _T("日本語");
std::wstring ws(text.GetString());

Fehler bei der Zeichenkettenkonvertierung treten bei der Pflege von MFC-Anwendungen häufig auf.

Besonders zu beachten sind Fälle wie diese.

Eine auf Shift_JIS ausgelegte Datei wird gelesen
Umstellung auf einen Unicode-Build
Eine externe DLL erfordert char*
COM erfordert BSTR
Unbedachte Konvertierung nach std::string führt zu Zeichensalat

Sieht man CString, sollte man nicht einfach denken „das ist nur eine alte Zeichenkettenklasse“, sondern es zusammen mit der Zeichensatzeinstellung des Projekts, externen APIs und dem Dateiformat prüfen.

19. CFile und CArchive

MFC bietet auch Klassen für Dateioperationen und Serialisierung. Typisch sind CFile und CArchive.

CFile file;
if (file.Open(path, CFile::modeRead))
{
    CArchive ar(&file, CArchive::load);
    // Aus ar lesen
}

CArchive wird häufig im Serialisierungsmechanismus von MFC verwendet.

In von CDocument abgeleiteten Klassen wird mitunter Serialize überschrieben, um Lesen und Speichern in derselben Funktion zu implementieren.

void CMyDocument::Serialize(CArchive& ar)
{
    if (ar.IsStoring())
    {
        ar << m_title;
        ar << static_cast<int>(m_items.size());
        for (const auto& item : m_items)
        {
            ar << item.Name;
            ar << item.Value;
        }
    }
    else
    {
        int count = 0;
        ar >> m_title;
        ar >> count;
        m_items.clear();
        for (int i = 0; i < count; ++i)
        {
            Item item;
            ar >> item.Name;
            ar >> item.Value;
            m_items.push_back(item);
        }
    }
}

Die Serialisierung von MFC ist praktisch, erfordert im Langzeitbetrieb aber Vorsicht.

Kompatibilität mit alten Dateiformaten
Verwaltung von Versionsnummern
Wiederherstellung bei fehlgeschlagenem Lesen
Ausnahmebehandlung
Zeichenkodierung
Byte-Reihenfolge (Endianness)
Werden Strukturen unverändert gespeichert?

In MFC-Anwendungen, die seit Langem ein eigenes Binärformat verwenden, ist Serialize mitunter faktisch zur Dateispezifikation geworden.

In diesem Fall sollte man vor jeder Codeänderung unbedingt Testdaten anlegen, mit denen sich bestehende Dateien einlesen lassen.

20. GDI-Zeichnung und CDC

Bei der Bildschirmzeichnung von MFC kommt häufig die Klasse CDC zum Einsatz, die den Device Context von Windows handhabt. In CView::OnDraw wird als Argument ein CDC* übergeben.

void CMyView::OnDraw(CDC* pDC)
{
    pDC->TextOut(10, 10, _T("Hello MFC"));
    pDC->Rectangle(10, 40, 200, 120);
}

Bei der Verwendung von Stiften und Pinseln ist auf Auswahl und Wiederherstellung zu achten.

void CMyView::OnDraw(CDC* pDC)
{
    CPen pen(PS_SOLID, 1, RGB(0, 0, 0));
    CPen* pOldPen = pDC->SelectObject(&pen);

    pDC->MoveTo(10, 10);
    pDC->LineTo(100, 100);

    pDC->SelectObject(pOldPen);
}

Im Umfeld von GDI-Objekten werden diese Fehler zum Problem.

Nach SelectObject wird das ursprüngliche Objekt nicht wiederhergestellt
Es werden viele GDI-Objekte erzeugt, aber nicht freigegeben
Die Rollen von OnPaint und OnDraw werden vermischt
Es wird nicht doppelt gepuffert, wodurch es flackert
Bei hoher DPI bricht auf feste Pixel ausgelegtes Zeichnen zusammen

Bei Zeichnungsfehlern in MFC muss man nicht nur die C++-Logik, sondern auch GDI-Ressourcen von Windows, den Zeitpunkt der Neuzeichnung, die DPI und die Schriftgröße prüfen.

21. Modale und modenlose Dialoge

In MFC ist auch bei der Art, wie ein Dialog angezeigt wird, Vorsicht geboten.

Ein modaler Dialog wird mit DoModal angezeigt.

CSettingsDialog dlg(this);
if (dlg.DoModal() == IDOK)
{
    // Verarbeitung bei OK
}

In diesem Fall wartet der Aufrufer, bis der Dialog geschlossen wird.

Ein modenloser Dialog gibt dagegen die Kontrolle nach der Erzeugung sofort an den Aufrufer zurück.

m_pToolDialog = new CToolDialog(this);
m_pToolDialog->Create(IDD_TOOL_DIALOG, this);
m_pToolDialog->ShowWindow(SW_SHOW);

Bei modenlosen Dialogen ist die Lebensdauerverwaltung wichtig.

Wann wird ein mit new erzeugter Dialog per delete freigegeben?
Wird das übergeordnete Fenster nicht vorher zerstört?
Verwendet der Dialog PostNcDestroy?
Wird er nicht doppelt erzeugt?
Bleibt nach dem Schließen kein Zeiger übrig?

Bei der Absturzuntersuchung in MFC ist ein Lebensdauerproblem bei modenlosen Dialogen mitunter die Ursache.

22. Die Lebensdauer von C++-Objekten und Windows-Handles

Sehr wichtig in MFC ist der Unterschied in der Lebensdauer zwischen C++-Objekten und Windows-Handles. CWnd ist beispielsweise ein C++-Objekt, das eigentliche Fenster verwaltet Windows dagegen als HWND, und diese beiden werden nicht immer gleichzeitig erzeugt und gleichzeitig zerstört.

Ein CWnd-Objekt existiert, aber das HWND existiert noch nicht
Das HWND wurde zerstört, aber das CWnd-Objekt bleibt bestehen
Es wird ein temporärer CWnd-Wrapper erzeugt
Der Handle wird per Attach/Detach ausgetauscht

Vorsicht ist beispielsweise bei Code wie diesem geboten.

CWnd* pWnd = GetDlgItem(IDC_SOME_CONTROL);
// pWnd in einem Member speichern und später verwenden

Hält man einen von GetDlgItem erhaltenen Zeiger über einen langen Zeitraum, besteht die Gefahr, nach der Zerstörung des Fensters darauf zuzugreifen.

Bei Bedarf ist es sicherer, entweder jedes Mal GetDlgItem aufzurufen oder das Steuerelement über eine per DDX verwaltete Mitgliedsvariable anzusprechen.

DDX_Control(pDX, IDC_LIST_ITEMS, m_listItems);

In MFC ist ein Zeiger nicht automatisch sicher, nur weil er nicht null ist.

if (m_pDialog != nullptr && ::IsWindow(m_pDialog->GetSafeHwnd()))
{
    m_pDialog->SetWindowText(_T("Running"));
}

Dieses Gespür ist bei der Pflege von MFC sehr wichtig.

23. Threads und UI-Aktualisierung

Die Windows-UI muss grundsätzlich von dem UI-Thread aus bedient werden, in dem sie erzeugt wurde — das gilt auch für MFC-Anwendungen. Bedient ein Worker-Thread ein UI-Steuerelement direkt, führt das zu instabilem Verhalten oder Abstürzen.

Ein Beispiel, das man vermeiden sollte.

UINT WorkerThreadProc(LPVOID pParam)
{
    CMyDialog* pDlg = static_cast<CMyDialog*>(pParam);

    // Die UI nicht direkt aus einem Worker-Thread bedienen
    pDlg->SetDlgItemText(IDC_STATUS, _T("Done"));

    return 0;
}

Üblicherweise benachrichtigt man den UI-Thread stattdessen etwa mit PostMessage.

constexpr UINT WM_APP_WORK_DONE = WM_APP + 1;

UINT WorkerThreadProc(LPVOID pParam)
{
    HWND hWnd = static_cast<HWND>(pParam);

    // Aufwendige Verarbeitung

    ::PostMessage(hWnd, WM_APP_WORK_DONE, 0, 0);
    return 0;
}

Auf UI-Seite wird das über die Message Map empfangen.

BEGIN_MESSAGE_MAP(CMyDialog, CDialogEx)
    ON_MESSAGE(WM_APP_WORK_DONE, &CMyDialog::OnWorkDone)
END_MESSAGE_MAP()

LRESULT CMyDialog::OnWorkDone(WPARAM, LPARAM)
{
    SetDlgItemText(IDC_STATUS, _T("Done"));
    return 0;
}

Rund um Threads in MFC sollte man Folgendes prüfen.

Wird die UI nicht direkt aus einem Worker-Thread bedient?
Wird PostMessage nicht nach der Zerstörung des Fensters aufgerufen?
Wird der UI-Thread beim Warten auf Threadende nicht blockiert?
Ist die Sperrung gemeinsam genutzter Daten angemessen?
Werden Rückgabewert und Lebensdauer von AfxBeginThread nicht missverstanden?

24. MFC-DLLs und Modulzustand

Erstellt man mit MFC eine DLL, kommt der Begriff des Modulzustands (Module State) ins Spiel.

In Szenarien, in denen eine MFC-DLL Ressourcen lädt, einen Dialog anzeigt oder eine Erweiterungs-DLL erstellt, stellt sich die Frage, in welchem Modul nach Ressourcen gesucht wird.

Am Einstiegspunkt von Funktionen einer MFC-DLL findet sich mitunter ein Makro wie dieses.

AFX_MANAGE_STATE(AfxGetStaticModuleState());

Es sorgt dafür, dass MFC den korrekten Modulzustand verwenden kann.

Vergisst man dieses Makro, führt das zu Störungen wie diesen.

Dialogressourcen in der DLL werden nicht gefunden
Zeichenkettenressourcen werden aus einem anderen Modul gelesen
Symbole oder Menüs werden nicht gefunden
Es funktioniert nur im Debug-Build, bricht aber im Release

Bei der Pflege einer MFC-DLL ist es wichtig, das Verhältnis zwischen EXE, gewöhnlicher DLL, Erweiterungs-DLL und Ressourcen-DLL zu klären.

Besondere Vorsicht ist geboten, wenn eine MFC-DLL aus einer Nicht-MFC-Anwendung heraus aufgerufen wird oder eine Plug-in-Struktur vorliegt.

25. MFC statisch verlinken oder als gemeinsame DLL nutzen

In MFC-Anwendungen gibt es in den Projekteinstellungen die Option „Use of MFC“.

Die beiden typischen Optionen sind:

Use MFC in a Shared DLL
Use MFC in a Static Library

Bei Verwendung der gemeinsamen DLL benötigt die Ausführungsumgebung die passende MFC- beziehungsweise Visual-C++-Laufzeitumgebung.

Bei statischer Verlinkung wirkt das Auslieferungspaket zwar mitunter einfacher, doch muss man die Größe der ausführbaren Datei, Updates, das Einspielen von Sicherheitskorrekturen sowie Lizenz- und Weiterverteilungsbedingungen bedenken.

Keine der beiden Varianten ist immer die richtige Wahl.

Als Entscheidungskriterien dienen unter anderem diese Punkte.

Lässt sich beim Zielsystem das Visual C++ Redistributable installieren?
Soll die Anwendung möglichst als einzelne exe-Datei vorliegen?
Wie werden Sicherheitsupdates eingespielt?
Teilen sich mehrere Anwendungen dieselbe Laufzeitumgebung?
Lässt sich ein Installer bereitstellen?
Welche Windows-Version ist Zielplattform?

Bei der Pflege ist es zunächst wichtig, die aktuelle Einstellung zu prüfen.

Configuration Properties
  General
    Use of MFC

Zusätzlich sollte man auch die Einstellung Runtime Library betrachten.

/MD   Multi-threaded DLL
/MDd  Multi-threaded Debug DLL
/MT   Multi-threaded
/MTd  Multi-threaded Debug

Sind die Verlinkungseinstellungen von MFC und CRT uneinheitlich gemischt, können an den Bibliotheksgrenzen Probleme bei Speicherreservierung/-freigabe auftreten.

26. Unicode, MBCS und TCHAR

In altem MFC-Code tauchen TCHAR, LPCTSTR und das Makro _T() ständig auf.

CString title = _T("設定");
SetWindowText(title);

Diese Schreibweise dient dazu, sowohl Unicode- als auch MBCS-Builds zu unterstützen.

Unicode-Build
  TCHAR    -> wchar_t
  LPCTSTR  -> const wchar_t*
  _T("...") -> L"..."

MBCS-Build
  TCHAR    -> char
  LPCTSTR  -> const char*
  _T("...") -> "..."

Heute ist der Unicode-Build üblich, in alten Anwendungen kann aber noch auf MBCS ausgelegte Verarbeitung vorhanden sein.

Besonders bei externen Dateien, Kommunikationsprotokollen, alten DLLs, Datenbankverbindungen und serieller Kommunikation muss man die Annahme zur Zeichenkodierung prüfen.

Wichtig ist, die Umstellung auf Unicode nicht als einfache Ersetzungsarbeit zu betrachten.

Ist die Größe eines char-Arrays in Byte oder in Zeichen angegeben?
Wird strlen verwendet?
Wird sizeof(buffer) als Zeichenanzahl verwendet?
Erwartet die externe API UTF-16 oder Shift_JIS?
Darf sich das Dateispeicherformat ändern?

Beim Ausbessern der Zeichenkettenverarbeitung in MFC sollte man nicht nur die Bildschirmanzeige prüfen, sondern auch Dateikompatibilität und externe Anbindungen bis zum Ende durchdenken.

27. MFC und COM/OLE/ActiveX

MFC wurde auch in Anwendungen mit enger Beziehung zu COM, OLE und ActiveX eingesetzt.

In alten Fachanwendungen können noch Elemente wie diese vorhanden sein.

OLE Automation
ActiveX-Steuerelement
COM-Server
COM-Client
IDispatch
BSTR
VARIANT
COleDispatchDriver
COleVariant

In der Startverarbeitung von MFC-Anwendungen taucht mitunter Code wie dieser auf.

if (!AfxOleInit())
{
    AfxMessageBox(_T("OLE initialization failed"));
    return FALSE;
}

Wird COM/OLE verwendet, kann ein scheinbares MFC-Problem tatsächlich an der COM-Initialisierung, dem Threading-Modell, der Referenzzählung, den Registrierungsinformationen oder Unterschieden zwischen 32-Bit und 64-Bit liegen.

Besondere Vorsicht ist bei 32-Bit-ActiveX- und COM-Komponenten geboten.

Eine 32-Bit-MFC-Anwendung verwendet 32-Bit-COM
Eine 64-Bit-MFC-Anwendung verwendet 64-Bit-COM
Die COM-Registrierung für 32-Bit und 64-Bit ist getrennt
Manche alten ActiveX-Steuerelemente unterstützen kein 64-Bit

Beim Portieren einer MFC-Anwendung auf x64 muss man nicht nur den UI-Code, sondern unbedingt auch COM/OLE-Abhängigkeiten prüfen.

28. Hohe DPI-Werte und modernes Windows

Betreibt man eine alte MFC-Anwendung auf einem aktuellen Windows, kann die Darstellung bei hoher DPI zusammenbrechen.

Beispiele für solche Probleme.

Text wird abgeschnitten
Schaltflächen sind zu klein
Auf feste Pixel ausgelegtes Zeichnen verschiebt sich
Bei mehreren Monitoren mit unterschiedlicher Skalierung bricht die Darstellung
Alte Bitmaps wirken unscharf
Das Dialoglayout wirkt gedrängt

Windows-Desktopanwendungen müssen den DPI-Awareness-Modus explizit angeben.

Auch bei MFC-Anwendungen müssen Manifest, Ressourcen, Zeichnungscode, Schriftarten und Layout geprüft werden.

Besonders Code, der feste Koordinaten direkt einträgt, wird bei hoher DPI leicht zum Problem.

pDC->TextOut(10, 10, _T("Status"));
pDC->Rectangle(10, 40, 200, 80);

Auf feste Pixel ausgelegte Koordinaten verschieben sich in der Darstellung, sobald sich die DPI ändert.

Die Prüfpunkte bei der Pflege sind diese.

DPI-Einstellung im Anwendungsmanifest
Schriftart der Dialogressourcen
Zeichnen mit festen Pixeln
Auflösung der Bildressourcen
Verhalten bei mehreren Monitoren
Darstellung unter Windows 10 / Windows 11

Die Anpassung von MFC an hohe DPI ist nicht immer allein mit einer Änderung der Projekteinstellungen erledigt. Bei alter UI sind oft eine tatsächliche Sichtprüfung des Bildschirms und Layoutkorrekturen nötig.

29. Ausnahme- und Fehlerbehandlung

MFC verfügt über eigene Ausnahmeklassen und eine eigene Fehlerbehandlungskonvention.

In altem Code trifft man auf Makros wie diese.

TRY
{
    // Verarbeitung
}
CATCH(CFileException, e)
{
    e->ReportError();
}
END_CATCH

In manchem Code sind sie mit dem modernen C++ try/catch vermischt.

try
{
    DoSomething();
}
catch (const std::exception& ex)
{
    // Protokollausgabe
}

Bei der Pflege von MFC sind folgende Punkte zu beachten.

Sind MFC-Ausnahmen und C++-Standardausnahmen vermischt?
Wird die Lebensdauer des Ausnahmeobjekts missverstanden?
Sind die alten Makros THROW/CATCH verstanden?
Sind Rückgabewertfehler und Ausnahmen vermischt?
Bleibt bei bloßem AfxMessageBox kein Protokolleintrag zurück?

In Fachanwendungen ist es wichtig, nicht nur eine Fehlermeldung auf dem Bildschirm anzuzeigen, sondern auch Protokoll, Bedienverlauf, Eingabewerte und den Status externer Verbindungen festzuhalten.

In alten MFC-Anwendungen endet ein Fehler mitunter allein bei AfxMessageBox.

AfxMessageBox(_T("Speichern fehlgeschlagen."));

Für eine bessere Pflegbarkeit sollte man UI-Anzeige und Protokollierung trennen.

LogError(_T("Save failed"), path);
AfxMessageBox(_T("Speichern fehlgeschlagen. Bitte prüfen Sie das Protokoll."));

30. Wie sich MFC und modernes C++ zusammenbringen lassen

Nur weil man eine MFC-Anwendung pflegt, muss nicht alles nach der alten C++-Schreibweise gestaltet werden.

Die UI-Schicht kann die Konventionen von MFC respektieren, während sich Domänenlogik und Berechnungen in modernem C++ organisieren lassen.

Eine mögliche Aufteilung sieht so aus.

MFC-Schicht
  CDialog
  CView
  CDocument
  CString
  Message Map
  Ressourcenbedienung

Nicht-MFC-Schicht
  std::string / std::wstring
  std::vector
  std::optional
  std::variant
  std::filesystem
  Testbare Klassen
  Geschäftslogik

Eine schlechte Form ist es, wenn die gesamte Verarbeitung in der Dialogklasse zusammengepfercht wird.

void CMainDialog::OnBnClickedExecute()
{
    // Eingabe abrufen
    // Datei lesen
    // Kommunikation
    // Berechnung
    // Datenbank aktualisieren
    // Bildschirm aktualisieren
    // Protokoll schreiben
    // Ausnahmebehandlung
}

Solcher Code lässt sich nur schwer ändern, schwer testen und auch die Fehlersuche wird schwierig.

Zur Verbesserung holt man die Logik aus der MFC-Klasse heraus.

void CMainDialog::OnBnClickedExecute()
{
    if (!UpdateData(TRUE))
    {
        return;
    }

    ExecuteRequest request;
    request.Name = ToStdWString(m_name);
    request.Interval = m_interval;

    ExecuteResult result = m_service.Execute(request);

    m_status = ToCString(result.Message);
    UpdateData(FALSE);
}

So lässt sich m_service.Execute ohne MFC testen.

Die wirksamste Verbesserung bei der Pflege bestehender MFC-Bestände ist es, die Logik schrittweise aus der UI-Klasse herauszulösen.

31. MFC-Code testfreundlich gestalten

MFC-Anwendungen sind in ihrer ursprünglichen Form oft schwer per Unit-Test zu prüfen.

Der Grund liegt darin, dass UI, Win32, Dateien, Kommunikation, Datenbank und globaler Zustand leicht eng gekoppelt werden.

Der Denkansatz, um Testbarkeit zu erreichen, ist dieser.

CDialog oder CView nicht direkt testen wollen
Zunächst die Nicht-UI-Logik herauslösen
MFC-Typen an der Grenze konvertieren
Datei- und Kommunikationszugriffe über Schnittstellen abstrahieren
Bildschirm-Event-Handler dünn halten

Man verschiebt die Verarbeitung beispielsweise in eine reine C++-Klasse.

class PriceCalculator
{
public:
    int CalculateTotal(const std::vector<int>& prices) const
    {
        int total = 0;
        for (int price : prices)
        {
            total += price;
        }
        return total;
    }
};

Die MFC-Seite übernimmt nur Eingabe und Ausgabe.

void CPriceDialog::OnBnClickedCalculate()
{
    if (!UpdateData(TRUE))
    {
        return;
    }

    std::vector<int> prices = ParsePrices(ToStdWString(m_input));
    int total = m_calculator.CalculateTotal(prices);

    m_result.Format(_T("%d"), total);
    UpdateData(FALSE);
}

Mit dieser Struktur lassen sich PriceCalculator und ParsePrices mit gewöhnlichen C++-Testframeworks testen.

Es ist nicht nötig, die gesamte MFC-Anwendung auf einen Schlag neu zu bauen — schon das Herauslösen testbarer Verarbeitung aus den Event-Handlern zeigt Wirkung.

32. Die Build-Umgebung festlegen

Bei der Pflege von MFC-Anwendungen ist es wichtig, die Build-Umgebung festzulegen.

In alten Codebasen kann sich das Build-Ergebnis durch Unterschiede wie diese ändern.

Version von Visual Studio
Version des MSVC-Toolsets
Version des Windows SDK
Vorhandensein der MFC-/ATL-Komponente
x86 / x64 / ARM64
Debug / Release
Unicode / MBCS
Statische Verlinkung von MFC / gemeinsame DLL
Einstellung der Laufzeitbibliothek
Vorkompilierte Header

In MFC-Anwendungen sammeln sich in stdafx.h oder pch.h mitunter viele Abhängigkeiten.

#include "framework.h"
#include "MyApp.h"

Ein Build kann kaputtgehen, weil nur bei einer einzelnen Datei die Compilereinstellung abweicht oder die Einstellung des vorkompilierten Headers nicht passt.

Bei Pflegeprojekten erspart es später viel Mühe, diese Informationen in der README festzuhalten.

Benötigte Visual-Studio-Version
Benötigte Workloads und individuelle Komponenten
Benötigtes Windows SDK
Zielplattform
Verlinkungsart von MFC
Build-Schritte
Ausführung der CI
Erstellung des Auslieferungspakets

Der erste Schritt bei der Pflege von MFC ist, über „auf meinem PC lässt es sich bauen“ hinauszukommen.

33. MFC in der CI bauen

Auch MFC-Anwendungen lassen sich in der CI bauen.

Allerdings muss die MFC-Komponente in der CI-Umgebung installiert sein.

Verwendet man die Visual Studio Build Tools, muss man die Komponenten-ID von MFC/ATL zur Installation angeben. Die anzugebenden IDs entsprechen der Tabelle aus Kapitel 5.

Zu prüfende Punkte sind diese.

Ist MFC in den Build Tools enthalten?
Stimmt das Zieltoolset mit dem Projekt überein?
Ist das Windows SDK installiert?
Werden sowohl x86- als auch x64-Build geprüft?
Funktioniert der Ressourcencompiler?
Gibt es eine Signaturverarbeitung?
Ist auch die Erstellung des Installers Teil der CI?

Bei MFC-Anwendungen ist es nicht einfach, sogar UI-Tests vollständig zu automatisieren, aber mindestens diese Automatisierungen sind sinnvoll.

Debug-/Release-Build
x86-/x64-Build
Statische Analyse
Unit-Tests
Erstellung des Installers
Speichern der Hashes der Artefakte
Prüfung abhängiger DLLs

Im Langzeitbetrieb hat es bereits großen Wert, den buildfähigen Zustand zu erhalten.

34. Wo man bei der Fehlersuche in einer MFC-Anwendung nachsieht

Bei der Fehlersuche in einer MFC-Anwendung ist es effizient, in dieser Reihenfolge vorzugehen.

1. Welcher Bildschirm ist betroffen?
2. Ist es ein Dialog, eine View oder ein Frame?
3. Welche Ressourcen-ID gehört zur Bedienung?
4. Zu welcher Funktion führt die Message Map?
5. Ist die Richtung von UpdateData korrekt?
6. Bei Document/View: Wie ist der Zustand des Document?
7. Fließt es beim Command Routing nicht in eine andere Klasse ab?
8. Wird die UI aus einem Worker-Thread bedient?
9. Werden Ausnahmen oder Fehler allein von AfxMessageBox verschluckt?
10. Gibt es release-build-spezifische Probleme mit nicht initialisierten Werten oder Lebensdauer?

Bei einem Fehler wie „nach dem Drücken der Schaltfläche passiert nichts“ prüft man beispielsweise Folgendes.

Ist die IDC der Schaltfläche korrekt?
Existiert ON_BN_CLICKED?
Ist die Signatur des Handlers korrekt?
Handelt es sich nicht um eine andere Dialogressource?
Ist die Schaltfläche deaktiviert?
Ist UpdateData mitten in der Verarbeitung fehlgeschlagen?
Wird eine Ausnahme verschluckt?

Bei „das Menü lässt sich nicht drücken“ prüft man dies.

Ist es über ON_UPDATE_COMMAND_UI deaktiviert?
Ist die Befehls-ID doppelt vergeben?
Entspricht die aktive Ansicht der Erwartung?
In Frame/View/Document/App — wo liegt der Handler?

Da bei MFC das sichtbare Ereignis und die eigentliche Verarbeitung über Makros und Routing verknüpft sind, hilft es, bis man sich daran gewöhnt hat, den Aufrufpfad als Diagramm darzustellen. Das Diagramm aus Kapitel 9 zeigt die Grundform dieses Aufrufpfads.

Möchte man anhand der Symptome eine erste Vermutung anstellen, empfiehlt sich zunächst ein Blick auf die Tabelle in Kapitel 35. Sie ordnet häufige Fallstricke den jeweiligen Prüforten zu.

35. Häufige Fallstricke

Hier eine Zusammenstellung häufiger Fallstricke bei der Pflege von MFC.

Die Message Map wird nicht geprüft, es wird nur nach dem Funktionsaufruf gesucht
Man geht davon aus, dass ein CWnd* gültig ist, sobald es nicht null ist
Die Lebensdauer von HWND und die des C++-Objekts werden verwechselt
Die Richtung von UpdateData(TRUE/FALSE) wird verwechselt
Man bemerkt nicht, dass etwas über ON_UPDATE_COMMAND_UI deaktiviert wurde
ID-Kollisionen in resource.h werden nicht bemerkt
Die UI wird direkt aus einem Worker-Thread bedient
Bei der Konvertierung zwischen CString und std::string entsteht Zeichensalat
Auf MBCS ausgelegter Code wird durch die Umstellung auf Unicode zerstört
AFX_MANAGE_STATE wird in einer MFC-DLL vergessen
Auf x86 ausgelegtes COM/ActiveX wird bei der x64-Portierung zerstört
Auswahl/Freigabe von GDI-Objekten wird falsch gehandhabt
Bei hoher DPI bricht ein auf feste Koordinaten ausgelegtes Layout zusammen

Zu jedem dieser Punkte wird festgehalten, „mit welchem Symptom er auftritt“ und „wo man nachprüft“. Der Einstieg in die Fehlersuche entspricht dem Vorgehen aus Kapitel 34.

Fallstrick Typisches Symptom Prüfort
Die Message Map wird nicht geprüft, es wird nur nach dem Funktionsaufruf gesucht Wird ausgeführt, obwohl kein Aufrufer zu finden ist BEGIN_MESSAGE_MAP durchsuchen. Kapitel 9, Kapitel 34 Schritt 4
Man geht davon aus, dass ein CWnd* gültig ist, sobald er nicht null ist Gelegentlicher Absturz, Absturz bei Bedienung eines geschlossenen Bildschirms ::IsWindow(pWnd->GetSafeHwnd()) einsetzen. Kapitel 8
Die Lebensdauer von HWND und die des C++-Objekts werden verwechselt Zugriffsverletzung nach Schließen des Dialogs Wird der Rückgabewert von GetDlgItem gehalten? Kapitel 22
Die Richtung von UpdateData wird verwechselt Eingabewerte werden nicht übernommen, Anfangswerte erscheinen nicht im Bildschirm Aufrufstelle von UpdateData(TRUE) und UpdateData(FALSE). Kapitel 13, Kapitel 34 Schritt 5
Man bemerkt nicht, dass etwas über ON_UPDATE_COMMAND_UI deaktiviert wurde Schaltfläche oder Menü ist ausgegraut und nicht bedienbar Handler von ON_UPDATE_COMMAND_UI. Kapitel 11, Kapitel 34 „Menü lässt sich nicht drücken“
ID-Kollisionen in resource.h werden nicht bemerkt Ein anderer Dialog oder Menüpunkt reagiert resource.h und .rc anhand der ID abgleichen. Kapitel 16
Die UI wird direkt aus einem Worker-Thread bedient Schwer reproduzierbares, instabiles Abstürzen, gelegentliches Hängen Wird in der an AfxBeginThread übergebenen Funktion die UI bedient? Kapitel 23
Bei der Konvertierung zwischen CString und std::string entsteht Zeichensalat Nur japanischer Text ist beschädigt, das Ende wird abgeschnitten Zeichensatzeinstellung des Projekts und Konvertierungsstelle. Kapitel 18
Auf MBCS ausgelegter Code wird durch die Umstellung auf Unicode zerstört Verschiebung der Pufferlänge, bestehende Dateien lassen sich nicht mehr lesen Wird sizeof als Zeichenanzahl verwendet? Kapitel 26
AFX_MANAGE_STATE wird in einer MFC-DLL vergessen Dialoge oder Zeichenkettenressourcen in der DLL werden nicht gefunden Anfang der öffentlichen Funktionen der DLL. Kapitel 24
Auf x86 ausgelegtes COM/ActiveX wird bei der x64-Portierung zerstört Nur der 64-Bit-Build schlägt beim Start oder bei der Bildschirmanzeige fehl Ist die COM-Registrierung nur auf der 32-Bit-Seite vorhanden? Kapitel 27
Auswahl und Freigabe von GDI-Objekten wird falsch gehandhabt Bei langer Laufzeit bricht die Zeichnung zusammen Steigt die Spalte „GDI-Objekte“ im Task-Manager fortlaufend? Kapitel 20
Bei hoher DPI bricht ein auf feste Koordinaten ausgelegtes Layout zusammen Nur bei 150 % Skalierung wird Text abgeschnitten DPI-Einstellung im Manifest und Zeichnung mit festen Koordinaten. Kapitel 28

Ein Fehler in MFC lässt sich nicht immer allein durch die C++-Syntax verstehen — man muss auch Windows-Nachrichten, Ressourcen, Handles, Module und Laufzeiteinstellungen einbeziehen.

36. Sollte man MFC für eine Neuentwicklung wählen?

Ob man MFC für eine komplett neue Entwicklung wählt, sollte man sorgfältig abwägen.

Gründe, MFC zu wählen, gibt es in Fällen wie diesen.

Enge Zusammenarbeit mit bestehendem MFC-Code ist erforderlich
Bestehende MFC-Bausteine oder Bildschirme sollen wiederverwendet werden
Sehr enge Steuerung nahe an Win32/GDI/COM ist erforderlich
Im Unternehmen ist ausreichend MFC-Pflegekompetenz vorhanden
Das Ziel ist ausschließlich der Windows-Desktop
Im Rahmen einer langfristigen Migrationsplanung muss zunächst mit MFC erweitert werden

Umgekehrt sollte man in folgenden Fällen andere Optionen prüfen.

Eine moderne UI soll entstehen
Flexibles Layout oder Animation ist erforderlich
Web- oder Cloud-Anbindung steht im Mittelpunkt
Testbarkeit hat hohe Priorität
Es soll eine Technologie gewählt werden, die jungen Entwicklern den Einstieg erleichtert
Plattformübergreifende Unterstützung ist erforderlich
Barrierefreiheit oder hohe DPI sollen von Anfang an im Vordergrund stehen

MFC ist weder eine Technologie, „die es sich jetzt nicht mehr zu lernen lohnt“, noch eine, „die man wählt, weil sie neu ist“ — es ist eine Technologie, um sich mit bestehenden nativen Windows-Beständen auseinanderzusetzen.

37. Denkansatz für die Migration weg von MFC

Möchte man eine MFC-Anwendung auf eine andere Technologie migrieren, scheitert der Versuch, sofort eine vollständige Neugestaltung anzustreben, leicht.

Zunächst zerlegt man die Anwendung gedanklich so.

UI
Geschäftslogik
Dateiformat
Kommunikationsverarbeitung
Datenbankverarbeitung
Gerätesteuerung
Druck
COM/OLE-Anbindung
Konfigurationsverwaltung
Protokollierung

Davon ist die UI am stärksten von MFC abhängig.

Geschäftslogik und Dateiverarbeitung lassen sich dagegen möglicherweise herauslösen.

Der realistische Ablauf einer Migration sieht so aus.

1. Die Build-Umgebung reproduzieren
2. Das bestehende Verhalten mit Testdaten festschreiben
3. Die Logik aus den UI-Event-Handlern herauslösen
4. Sie in eine Nicht-MFC-C++-Bibliothek verschieben
5. Automatisierte Tests hinzufügen
6. Die externe Spezifikation dokumentieren
7. Schrittweise, beginnend mit den notwendigen Bildschirmen, ersetzen

Erfolgversprechender ist es, sich nicht das Ziel „MFC loswerden“ selbst zu setzen, sondern „die in MFC eingeschlossene wichtige Logik nach außen holen“.

38. Der Einstieg beim Lesen von MFC-Code

Liest man ein bestehendes MFC-Projekt zum ersten Mal, beginnt man am besten mit diesen Dateien.

*.vcxproj
  Toolset, MFC-Einstellung, Zeichensatz, Laufzeiteinstellung ansehen

resource.h
  Ressourcen-IDs ansehen

*.rc
  Dialoge, Menüs, Zeichenketten, Symbole ansehen

*App.cpp / *App.h
  Von CWinApp abgeleitete Klasse und InitInstance ansehen

MainFrm.cpp / MainFrm.h
  Hauptrahmen sowie Menü/Symbolleiste ansehen

*Doc.cpp / *Doc.h
  Bei Document/View: Datenstruktur und Speicherverarbeitung ansehen

*View.cpp / *View.h
  Zeichnung und Benutzerinteraktion ansehen

*Dlg.cpp / *Dlg.h
  Dialog, DDX, Schaltflächenverarbeitung ansehen

Als Nächstes häufig verwendete Suchbegriffe.

BEGIN_MESSAGE_MAP
ON_COMMAND
ON_UPDATE_COMMAND_UI
ON_BN_CLICKED
DoDataExchange
UpdateData
OnInitDialog
OnDraw
Serialize
AfxMessageBox
AfxBeginThread
AFX_MANAGE_STATE

Sucht man danach, wird der Ablauf der Anwendung besser erkennbar.

39. Gestaltungsgrundsätze für die Pflege von MFC

Für die langfristige Pflege bestehender MFC-Bestände sind Grundsätze wie diese wirksam.

UI-Event-Handler dünn halten
CString und CWnd nicht zu stark in die Nicht-UI-Schicht durchsickern lassen
Geschäftslogik in gewöhnliche C++-Klassen verschieben
Kompatibilitätstests für das Dateiformat erstellen
Unterschiede zwischen x86/x64 klar benennen
Änderungen an Ressourcen-IDs im Review prüfen
Den Modulzustand von MFC-DLLs kontrollieren
Protokollierung ausbauen
Den Build über CI festlegen
Regelmäßig die Bildschirmdarstellung bei hoher DPI und unter Windows 11 prüfen

Besonders wichtig ist, CDialog oder CView nicht mit zu viel Verarbeitung zu überladen.

Die Bildschirmklassen von MFC sollten sich auf Eingabe, Anzeige und Ereignisweiterleitung konzentrieren.

Werte vom Bildschirm abrufen
An den Dienst übergeben
Ergebnis im Bildschirm widerspiegeln

Hält man sich in diesem Rahmen, lässt sich selbst MFC recht gut pflegen.

40. Praktische Checkliste

Eine Checkliste für den Umgang mit MFC-Anwendungen.

Ist die Version von Visual Studio eindeutig festgelegt?
Sind die MFC-/ATL-Komponenten installiert?
Ist die Zielplattform x86/x64 eindeutig?
Ist die Einstellung Unicode/MBCS bekannt?
Ist MFC statisch verlinkt oder als gemeinsame DLL eingebunden?
Welches Visual C++ Redistributable wird benötigt?
Werden resource.h und .rc in Reviews einbezogen?
Wird der Ereignispfad anhand der Message Map nachvollzogen?
Ist die Richtung von UpdateData korrekt?
Wird die Document/View-Struktur verwendet?
Ist die Lebensdauerverwaltung modenloser Dialoge sicher?
Wird die UI nicht direkt aus einem Worker-Thread bedient?
Gibt es Stellen in einer MFC-DLL, an denen AFX_MANAGE_STATE nötig wäre?
Wurde die Darstellung bei hoher DPI geprüft?
Sind COM/ActiveX-Abhängigkeiten für 32-Bit/64-Bit vorbereitet?
Bleibt ein Protokoll erhalten?
Lässt sich die Nicht-UI-Logik testen?

MFC wirkt zunächst ungewohnt, doch sobald klar ist, wo man nachsehen muss, lässt es sich recht regelmäßig lesen.

41. Zusammenfassung

MFC ist ein traditionsreiches Framework, um native Windows-Desktopanwendungen in C++ zu erstellen. Es ist nicht mehr der Mainstream für Neuentwicklungen, bleibt aber für die Pflege bestehender Bestände, die Aktualisierung der Build-Umgebung, Funktionserweiterungen und schrittweise Migration nach wie vor eine wichtige Technologie.

Besonders wichtig für das Verständnis von MFC sind diese Punkte.

MFC macht die Win32-API mit C++ leichter handhabbar
CWinApp verwaltet die gesamte Anwendung
Die Lebensdauer von CWnd und HWND ist nicht dieselbe
Die Message Map verknüpft Ereignis und Funktion
Document/View trennt Daten und Darstellung
DDX/DDV dienen der Synchronisation und Prüfung von Dialogwerten
Ressourcendatei und resource.h sind sehr wichtig
CString und TCHAR versteht man am besten zusammen mit der Zeichensatzeinstellung
Bei MFC-DLLs ist auf den Modulzustand zu achten
Es lohnt sich, alte MFC-Anwendungen unter den Gesichtspunkten hohe DPI, x64, CI und Testbarkeit zu überarbeiten

Beim Umgang mit MFC sollte man nicht vorschnell urteilen „alt bedeutet schlecht“, sondern zunächst die Struktur verstehen. In bestehenden MFC-Anwendungen steckt oft jahrelanges Fachwissen, kundenspezifische Vorgaben, Geräteanbindung und Dateikompatibilität. Um diesen Wert zu bewahren und die Anwendung dabei Stück für Stück pflegbarer zu machen, ist es realistisch, die Konventionen von MFC zu verstehen, Nicht-UI-Logik herauszulösen und Build sowie Tests in Ordnung zu bringen.

Wagt man es, das Ganze in einem Satz zusammenzufassen, ergibt sich Folgendes.

Die Grundlage, um die Mechanik nativer Windows-Anwendungen mit C++-Klassen und einem Framework zu handhaben.

Mit dieser Sichtweise wird MFC nicht zu einer bloß alten Technologie, sondern zu einem Schlüssel, um bestehende Windows-Bestände sicher zu erschließen.

Quellen

Aktuelle Artikel mit denselben Schlagwörtern führen zu verwandten Themen weiter.

Diese Seiten ordnen den Artikel in einen größeren Leistungs- und Entscheidungskontext ein.

Dieser Artikel ist direkt mit den folgenden Leistungen verbunden.

Häufige Fragen

Fragen, die in Beratungen zu diesem Artikelthema häufig gestellt werden.

Was ist MFC?
MFC steht für Microsoft Foundation Classes und ist ein Windows-Anwendungsframework, das die Win32-API als C++-Klassen leichter handhabbar macht. Ein Fenster wird durch CWnd, ein Dialog durch CDialog und die Anwendung als Ganzes durch CWinApp dargestellt. MFC ist keine „magische Bibliothek, mit der man schreiben kann, ohne Windows zu kennen“, sondern ordnet die Mechanik von Windows in C++-Typen und ein Framework ein — deshalb braucht man auch Win32-Wissen wie Windows-Nachrichten und Handles.
Kann man MFC heute noch verwenden? Wird es noch unterstützt?
MFC lässt sich nach wie vor in Visual Studio verwenden und wird weiterhin unterstützt. In Microsofts Dokumentation findet sich allerdings der Hinweis, dass keine neuen Funktionen mehr hinzugefügt und die Dokumentation nicht mehr aktualisiert wird. In der Praxis ist es daher üblich, MFC für die Pflege und Erweiterung bestehender MFC-Anwendungen, für die Aktualisierung der Build-Umgebung oder für die schrittweise Migration zu einer anderen UI einzusetzen, während der Einsatz in einer komplett neuen, allgemeinen GUI-Anwendung sorgfältig abgewogen werden sollte. Dafür muss die einzelne Komponente „C++ MFC for latest build tools“ über den Visual Studio Installer installiert werden.
Was ist die Message Map in MFC?
Das ist der Mechanismus von MFC, der Windows-Nachrichten und Befehle mit Handler-Funktionen verknüpft. Zwischen BEGIN_MESSAGE_MAP und END_MESSAGE_MAP wird mit Makros wie ON_BN_CLICKED oder ON_COMMAND festgelegt: „Wird diese Schaltfläche geklickt, rufe diese Funktion auf.“ Findet die Suche nach einer Funktion keinen direkten Aufruf, obwohl sie beim entsprechenden Ereignis ausgeführt wird, sollte man die Message Map prüfen. Bei einem MFC-Code-Review ist es wichtig, nicht nur die Handler-Funktion, sondern immer auch die zugehörige Message Map zu betrachten.
Wie migriert man von MFC zu einer anderen Technologie?
Der Versuch, sofort eine vollständige Neugestaltung anzustreben, scheitert leicht. Der realistische Weg besteht darin, die Build-Umgebung zu reproduzieren, das bestehende Verhalten mit Testdaten festzuschreiben, die Logik aus den UI-Event-Handlern herauszulösen und in eine Nicht-MFC-C++-Bibliothek zu verschieben, automatisierte Tests hinzuzufügen, die externe Spezifikation zu dokumentieren und dann, beginnend mit den notwendigen Bildschirmen, schrittweise zu ersetzen. Erfolgversprechender ist es, sich nicht das Ziel „MFC loswerden“ selbst zu setzen, sondern „die in MFC eingeschlossene wichtige Logik nach außen holen“.

Autorenprofil

Profilseite des Artikelautors.

Go Komura

Geschäftsführer von KomuraSoft LLC

Spezialisiert auf Windows-Softwareentwicklung, technische Beratung und Fehleranalyse, insbesondere bei bestehenden Systemen und schwer reproduzierbaren Störungen.

Zurück zum Blog