C++ und Microsoft Foundation Classes (MFC) mit MS VS Community 2015
Kapitel 7 - Doc/View-Modell und SDI

Dr. Erhard Henkes  (Stand: 27.07.2015)

Zurück zum Inhaltsverzeichnis

Zurück zum vorherigen Kapitel
 
 

Kapitel 7 – Doc/View-Modell und SDI


7.1 Dokumente und Ansichten

Während der Begriff Ansichten (View) einen Hinweis auf die grafische/bildhafte Darstellung von Daten gibt, ist die Bezeichnung Dokumente (Doc) nicht selbsterklärend. "Doc" enthält die Daten (Zahlen, Texte, ...),
während "View" diese Daten bildlich darstellt. "Doc" stellt auch einen Mechanismus zur Verfügung, der für das Lesen und Schreiben der Daten von und nach Dateien sorgt, die sogenannte "Serialisierung".
Dafür bietet "View" die Möglichkeit, die Ansicht der Daten nicht nur auf den Bildschirm, sondern auch z.B. auf einen Drucker auszugeben. "View" stellt darüber hinaus den Kontakt mit den Benutzereingaben her (Maus-, Tastatureingaben, ...). 

Für den Rahmen um "Doc" und "View" sorgt eine Rahmenfensterklasse, z.B. CFrameWnd. "Doc" beruht auf der Klasse CDocument und "View" auf der von CWnd abgeleiteten Klasse CView.
Der Wirkmechanismus der Dokumentvorlagen ist in der abstrakten MFC-Klasse CDocTemplate enthalten. Diese Klasse wird nicht direkt benutzt, sondern die davon abgeleiteten Klassen CSingleDocTemplate für SDI
und CMultiDocTemplate für MDI. Zusätzlich benötigt man eine von CWinApp abgeleitete Anwendungsklasse.

Nachfolgend ist die Klassenhierarchie (von links nach rechts) für eine SDI-Anwendung dargestellt:
 
MFC-Klassen
Anwendung
CObject CCmdTarget  CDocument CMyDoc
CDocTemplate  CSingleDocTemplate 
CWinThread CWinApp CMyApp
CWnd CFrameWnd CMyWnd
CView CMyView

Abb. 7.1: Doc/View-Klassenhierarchie einer SDI-Anwendung

Wie Sie sehen, ist die von CObject abgeleitete Klasse CCmdTarget die gemeinsame Basisklasse der Doc/View-Architektur. Was bedeutet SDI? Diese Abkürzung steht für Single-Document Interface. Das Gegenteil ist MDI, das für Multiple-Document Interface steht. Seit der Einführung von Windows 95 werden Anwendungen allgemein als SDI betrieben, weil Windows seit dieser Zeit einen Dokument-zentrierten Blickwinkel verkörpert. Man legt z.B. für jedes Word-Dokument eine eigene Instanz an und stellt nicht mehrere Dokumente in einem Programm dar. Die heutigen Rechner erlauben diesen Luxus.  

Damit aber genug der grauen Theorie. Wir werden an einem ersten Beispiel die Trennung von Daten (Doc) und Ansicht (View) ausprobieren. Wichtig ist, dass Sie es beherrschen, zwischen dem MFC-Rahmenprogramm, das der Assistent erstellt, und Ihrem eigenen Programmcode zu trennen.

Wir starten wie gewohnt mit Datei - Neu - Projekt - MFC-Anwendung. Wählen Sie als Projektname "Test_", damit Sie die Namensanhänge, die der Assistent den zu erstellenden Klassen verpasst, gut erkennen können.

Im Schritt "Anwendungstyp" entscheiden Sie für "Einfaches Dokument" und lassen das Häkchen bei "Unterstützung für die Dokument-/Ansicht-Architektur" gesetzt.

Den visuellen Stil ändern wir ab auf Windows 7.

Abb. 7.2: Schritt "Anwendungstyp" bei der Erstellung des Doc-/View-Programms


Nun müssen wir eine Dateierweiterung wählen für unseren Dokumenten-Typ: Ändern Sie unter "Eigenschaften für Dokumentvorlagen" die Eingaben wie folgt ab:

Abb. 7.3: Unterfenster bei Schritt "Eigenschaften für Dokumentvorlagen"


Wichtig ist der Eintrag bei Dateierweiterung. Hier habe ich "sav" eingetragen.
Sie können jedoch auch andere Bezeichnungen, die auf das Speichern von Daten oder auf den Datentyp hinweisen, benutzen.

Die anderen Fenster übernehmen wir unverändert. Bei "Generierte Klassen" zeigt Ihnen der MFC-Assistent folgende Übersicht:

Abb. 7.4: Die vom Assistent erstellten Klassen
 

Nun sehen Sie den Sinn des Unterstrichs nach Test. Das ist aber Geschmacksache. Sie erkennen die vier Grundpfeiler, auf denen die Anwendung steht:

Ansicht (View),
Daten (Doc),
Rahmenfenster (MainFrame),
Anwendung (App).

Es ist wichtig, dass Sie sich diese Unterteilung gut einprägen, damit Sie beim Programmieren die Übersicht behalten. Nach Kompilieren und Ausführen sehen Sie das Rahmenfenster mit Systemmenü, Menüzeile, Symbolleiste und Statusleiste und natürlich die "View" unseres Programms.

Abb. 7.5: Die kompilierte und ausgeführte Anwendung


Schauen Sie sich die Menüs genauer an. Unter "Datei" finden Sie die wesentlichen Möglichkeiten in Bezug auf unsere sav-Dateien:

Neu, Öffnen, Speichern, Speichern unter, Drucken, Seitenansicht, Druckeinrichtung, Zuletzt verwendete Datei, Beenden

Der Menüpunkt "Bearbeiten" ist z.Z. nicht interessant, bietet aber die üblichen Standards wie Rückgängig, Ausschneiden, Kopieren, Einfügen.

Unter Ansicht können Sie die Erscheinungsform der Anwendung bezüglich Symbolleiste, Statuszeile und allgemeiner Erscheinung abändern.

Unter Hilfe  - "?" finden Sie die bekannte About-Dialogbox (Infofeld).

Damit steht das Fundament. Jetzt liegt es an uns, eigene Ideen in die Tat umzusetzen.


Doc / View

Nehmen wir ein Beispiel zum Üben. Als Kind fand ich folgende Technik, einen Würfel zu zeichnen, eindrucksvoll:
Man zeichnet ein erstes Quadrat, dahinter schräg versetzt ein zweites Quadrat, und verbindet die Eckpunkte der beiden Quadrate mit vier diagonalen Linien.

Damit erhält man einen perspektivisch dargestellten Würfel. 

Diese Idee setzen wir nun in folgendem Programm um:

Zunächst müssen wir überlegen, welche Daten hierbei anfallen, die dann für das konkrete Beispiel in den von uns so benannten sav-Dateien gespeichert werden.

Unsere Quadrate und der Würfel haben gleiche Seitenlängen. Die beiden Quadrate, die wir zeichnen wollen, haben beide einen linken oberen Eckpunkt, der jeweils eine x- und eine y-Koordinate besitzt. Damit müssen wir folgende Daten in unsere Dokumentklasse einfügen:

double Laenge
CPoint ErstesQuadrat
CPoint ZweitesQuadrat

Die nächste Frage ist:
Wie deklarieren wir diese Daten-Variablen, und wo werden sie initialisiert?

Die Deklaration erfolgt in der Dokumentenklasse CTest_Doc, also in der Header-Datei Test_Doc.h:
 
// Test_Doc.h: Schnittstelle der Klasse CTest_Doc
//


#pragma once


class CTest_Doc : public CDocument
{
protected: // Nur aus Serialisierung erstellen
    CTest_Doc();
    DECLARE_DYNCREATE(CTest_Doc)

// Attribute
public:
    CPoint ZweitesQuadrat;
    CPoint ErstesQuadrat;
    int Laenge;


// Vorgänge
public:

// Überschreibungen
public:
    virtual BOOL OnNewDocument();
    virtual void Serialize(CArchive& ar);
#ifdef SHARED_HANDLERS
    virtual void InitializeSearchContent();
    virtual void OnDrawThumbnail(CDC& dc, LPRECT lprcBounds);
#endif // SHARED_HANDLERS

// Implementierung
public:
    virtual ~CTest_Doc();

...
};

Wir wechseln nun zu Test_Doc.cpp und  initialisieren diese Member-Variablen der Dokumentenklasse an zwei Stellen:

1) Im Konstruktor der Dokumentenklasse (One-Time-Konstruktion),
2) In der Member-Funktion "OnNewDocument()" der Dokumentenklasse (Reinitialisierung):
 
 
// CTest_Doc-Erstellung/Zerstörung

CTest_Doc::CTest_Doc()
{
    Laenge = 200;
    ErstesQuadrat.x = 100;
    ErstesQuadrat.y = 200;
    ZweitesQuadrat.x = 200;
    ZweitesQuadrat.y = 100;
}

CTest_Doc::~CTest_Doc()
{
}

BOOL CTest_Doc::OnNewDocument()
{
    if (!CDocument::OnNewDocument())
        return FALSE;

    // TODO: Hier Code zur Reinitialisierung einfügen
    // (SDI-Dokumente verwenden dieses Dokument)
    Laenge = 200;
    ErstesQuadrat.x = 100;
    ErstesQuadrat.y = 200;
    ZweitesQuadrat.x = 200;
    ZweitesQuadrat.y = 100;

    return TRUE;
}

Wenn Sie jetzt kompilieren / ausführen, sehen Sie noch keine Auswirkungen. Bisher haben wir nur Datenvariablen definiert und diese an den richtigen Stellen platziert. Wir haben jetzt noch folgende Schritte vor uns:

Daten anzeigen - Daten speichern / laden - Daten im Programm verändern

Dazu bewegen wir uns geistig von der Dokumentenklasse zur Ansichtsklasse, also zur Datei Test_View.cpp, um die Anzeige der Daten zu realisieren.

Die grafische Anzeige erfolgt in der Member-Funktion CTest_View::OnDraw(), die uns bisher folgendes bietet:
 
void CTest_View::OnDraw(CDC* /*pDC*/)
{
    CTest_Doc* pDoc = GetDocument();
    ASSERT_VALID(pDoc);
    if (!pDoc)
        return;

    // TODO: Code zum Zeichnen der systemeigenen Daten hinzufügen
}

Wichtig sind die beiden Zeiger pDC (noch auskommentiert) und pDoc. Der erste zeigt auf einen Device Context, der beim Neuzeichnen des Fensters eingesetzt wird, und der zweite zeigt auf die Dokumentenklasse CTest_Doc, um die Lücke zwischen View und Doc zu überwinden. Insbesondere die Verwendung des Zeigers pDoc macht den Programmcode etwas schwerfällig, aber dies ist der Preis für die Doc-/View-Architektur. Wir zeichnen zunächst das erste Quadrat, anschließend das zweite Quadrat und zum Schluß die vier Verbindungslinien. Im Programmcode müssen wir für alle Zugriffe auf Member-Variablen der Dokumentenklasse den Zeiger pDoc einsetzen. Unser "Zeichengerät" ist pDC.
 
/////////////////////////////////////////////////////////////////////////////
// CTest_View Zeichnen

void CTest_View::OnDraw(CDC* pDC)
{
  CTest_Doc* pDoc = GetDocument();
  ASSERT_VALID(pDoc);

  //Erstes Quadrat zeichnen:
  pDC->MoveTo(pDoc->ErstesQuadrat);
  pDC->LineTo(pDoc->ErstesQuadrat.x + pDoc->Laenge, pDoc->ErstesQuadrat.y);
  pDC->LineTo(pDoc->ErstesQuadrat.x + pDoc->Laenge, pDoc->ErstesQuadrat.y + pDoc->Laenge);
  pDC->LineTo(pDoc->ErstesQuadrat.x,  pDoc->ErstesQuadrat.y + pDoc->Laenge);
  pDC->LineTo(pDoc->ErstesQuadrat.x,  pDoc->ErstesQuadrat.y);

  //Zweites Quadrat zeichnen:
  pDC->MoveTo(pDoc->ZweitesQuadrat);
  pDC->LineTo(pDoc->ZweitesQuadrat.x + pDoc->Laenge, pDoc->ZweitesQuadrat.y);
  pDC->LineTo(pDoc->ZweitesQuadrat.x + pDoc->Laenge, pDoc->ZweitesQuadrat.y + pDoc->Laenge);
  pDC->LineTo(pDoc->ZweitesQuadrat.x,  pDoc->ZweitesQuadrat.y + pDoc->Laenge);
  pDC->LineTo(pDoc->ZweitesQuadrat.x,  pDoc->ZweitesQuadrat.y);

  //Verbindungslinien zeichnen:
  pDC->MoveTo(pDoc->ErstesQuadrat);
  pDC->LineTo(pDoc->ZweitesQuadrat);

  pDC->MoveTo(pDoc->ErstesQuadrat.x  + pDoc->Laenge, pDoc->ErstesQuadrat.y);
  pDC->LineTo(pDoc->ZweitesQuadrat.x + pDoc->Laenge, pDoc->ZweitesQuadrat.y);

  pDC->MoveTo(pDoc->ErstesQuadrat.x  + pDoc->Laenge, pDoc->ErstesQuadrat.y  + pDoc->Laenge);
  pDC->LineTo(pDoc->ZweitesQuadrat.x + pDoc->Laenge, pDoc->ZweitesQuadrat.y + pDoc->Laenge);

  pDC->MoveTo(pDoc->ErstesQuadrat.x,  pDoc->ErstesQuadrat.y  + pDoc->Laenge);
  pDC->LineTo(pDoc->ZweitesQuadrat.x, pDoc->ZweitesQuadrat.y + pDoc->Laenge);
}

Wenn wir das Programm nun kompilieren und ausführen, erhalten wir das gewünschte Bild:

Abb. 7.6: Als View erhalten wir den perspektivischen Würfel

Halten wir hier zunächst inne. Die entscheidende Trennung von Dokument und Ansicht ist bereits erfolgt. Wir haben 5 Daten (Länge; X1, Y1; X2, Y2) an zwei Stellen (Konstruktor und OnNewDocument() ) in der Dokumentenklasse erzeugt und durch den Einsatz der Funktionen MoveTo() und LineTo() in der Member-Funktion OnDraw() der Ansichtsklasse obenstehende "View" erzeugt. Neu war die Verwendung des Zeigers auf unsere Dokumentenklasse CTest_Doc* pDoc. Wir haben die Attribute in der Doc-Klasse als public deklariert, damit wir einfacher zugreifen können. Definiert man diese als private, so müsste man noch public GET/SET-Funktionen für diese gekapselten Variablen schaffen. Hierauf habe ich hier verzichtet, möchte es aber im Sinne der OOP erwähnen. 

Aus den gleichen Daten könnten wir z.B. auch mit TextOutW() eine Tabelle erzeugen. Das meint man, wenn man sagt, dass ein Dokument mehrere Ansichten (Views) haben kann. Sie kennen dies z.B. auch von MS Excel, wenn man Daten z.B. als Punkt-, Linien-, Balken- oder Tortengrafik darstellen kann. Die Ausgangstabelle ist das Dokument, während die Grafiken die "Views" sind.


Serialisierung

Nun kommt der nächste Schritt, das Schreiben und Lesen von Dokumentobjekten, die sogenannte "Serialisierung":

Sie haben sich sicher schon gefragt, wozu die Trennung in Doc und View eigentlich gut ist. Ein Vorteil ist die vom Assistenten bereit gestellte Funktion zur Serialisierung. Damit ist das Speichern und Laden unserer Daten im Zusammenhang mit unseren sav-Dateien gemeint.

Schauen wir uns zunächst die Funktion im "Rohzustand" an: 
 
// CTest_Doc-Serialisierung

void CTest_Doc::Serialize(CArchive& ar)
{
    if (ar.IsStoring())
    {
        // TODO: Hier Code zum Speichern einfügen
    }
    else
    {
        // TODO: Hier Code zum Laden einfügen
    }
}

Gehen wir sofort praktisch weiter. Der if-Zweig ist zuständig für das Speichern und der else-Zweig für das Laden.
Wir übergeben hier jeweils unsere fünf Daten (Länge, ErstesQuadrat.X, ErstesQuadrat.Y, ZweitesQuadrat.X, ZweitesQuadrat.Y):
 
 
///////////////
// CTest_Doc Serialisierung
void CTest_Doc::Serialize(CArchive& ar)
{
    if (ar.IsStoring())
    {
       ar << Laenge;
       ar << ErstesQuadrat;
       ar << ZweitesQuadrat;
    }
    else
    {
       ar >> Laenge;
       ar >> ErstesQuadrat;
       ar >> ZweitesQuadrat;
    }
}

Nach dem Ausführen des Programms werden wir nun unsere Würfel-Daten speichern, sagen wir unter dem Dateinamen "Wuerfel1.sav".

Abb. 7.7: Das Ergebnis der Serialisierung, die Datei "Wuerfel1.sav"

Öffnen wir die Datei mit dem Notizblock (Notepad) und dem Hex-Editor, dann finden wir folgendes:

Abb. 7.8: Die Datei "Wuerfel.sav" im Text-Editor

Abb. 7.9: Die Datei "Wuerfel.sav" im Hex-Editor

Zum Vergleich unsere Daten:

Laenge           = 200; // Hex: C8
ErstesQuadrat.x  = 100; // Hex: 64
ErstesQuadrat.y  = 200; // Hex: C8
ZweitesQuadrat.x = 200; // Hex: C8
ZweitesQuadrat.y = 100; // Hex: 64

Sie sehen, was hier geschieht. Unsere Daten werden "in Serie" in einer Datei abgelegt. Daher müssen wir logischerweise immer in der gleichen Variablen-Reihenfolge lesen wie schreiben. Das ist doch einfach, oder nicht? In der Klasse CArchive existieren die überladenen Operatoren << und >>, mit deren Hilfe man selbst zusammengesetzte Daten wie CPoint seriell laden bzw. speichern kann (dies erinnert Sie sicher an die C++-Streams cin und cout).

Zum Abschluss basteln wir uns noch eine Routine, mit deren Hilfe man das erste Quadrat verschieben kann. Hierzu benützen wir einfach die Pfeiltasten.

Fügen Sie mittels Klassen-Assistent eine Funktion für die Nachricht WM_KEYDOWN ein:

1) Strg+Shift+X, der Klassen-Assistent erscheint
2) Auswahl der Klasse CTest_View
3) Unter Meldungen WM_KEYDOWN auswählen und doppelt anklicken (fügt Funktion hinzu)
4) Auf "Code bearbeiten" drücken

Sie finden sich nun hier wieder in der neuen Funktion CTest_View::OnKeyDown(...):
 
void CTest_View::OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags)
{
    // TODO: Fügen Sie hier Ihren Meldungsbehandlungscode ein, und/oder benutzen Sie den Standard.

    CView::OnKeyDown(nChar, nRepCnt, nFlags);
}

Wir verwenden den Parameter UINT nChar, um eine switch-Verzweigung mit entsprechenden breaks zu durchlaufen.
Pro Tastendruck schieben wir das erste Quadrat um zehn Pixel in die entsprechende Richtung:
 
void CTest_View::OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags)
{
    CTest_Doc* pDoc = GetDocument();
    ASSERT_VALID(pDoc);

    switch (nChar)
    {
      case 37: (pDoc->ErstesQuadrat.x) -= 10; break;
      case 38: (pDoc->ErstesQuadrat.y) -= 10; break;
      case 39: (pDoc->ErstesQuadrat.x) += 10; break;
      case 40: (pDoc->ErstesQuadrat.y) += 10; break;
    }

    pDoc->SetModifiedFlag();
    Invalidate();

    CView::OnKeyDown(nChar, nRepCnt, nFlags);
}

Sie sehen, dass wir uns wieder des Zeigers auf die Dokumentklasse pDoc bedienen müssen. In der Member-Funktion OnDraw der Ansichtsklasse hatte dies bereits der Assistent für uns erledigt:

void CTest_View::OnDraw(CDC* pDC)
{
CTest_Doc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
...

Hier haben wir einfach von ihm gelernt und ebenfalls den Zeiger pDoc besorgt. Nachdem wir Daten der Dokumentklasse verändern, teilen wir dies auch dieser Klasse mit.
Dies erfolgt mittels pDoc->SetModifiedFlag(). Die Aktualisierung der Ansichtsklasse, also das Neuzeichnen mit den geänderten Daten, erfolgt mittels Invalidate().
Das Setzen des "ModifiedFlags" führt dazu, dass die Anwendung uns bei Veränderungen
vor dem Beenden oder Neuladen nach dem Speichern der aktuellen Daten befragt.

Abb. 7.10: Der Würfel bewegt sich und damit auch zwei der fünf Daten
 

Experimentieren Sie nun mit den verschiedenen Werten, speichern und öffnen Sie die Dateien. Schauen Sie sich die Werte mit einem Text- oder Hex-Editor an, damit Sie sehen, dass die Serialisierung auch bestens funktioniert. Vielleicht fällt Ihnen eine andere Steuerung ein, oder Sie bewegen auch das zweite Quadrat. Wichtig ist, dass Sie zunächst das Grundprinzip der Trennung von Daten und Ansicht und die Serialisierung (Daten seriell speichern und lesen) gut verstehen.

Falls Ihnen die Kapselung der Würfeldaten nicht ausreichend ist, bauen Sie diese zur Übung um auf private und ergänzen Sie die entsprechenden public Get-/Set-Methoden, damit Sie Zugang zu den Werten von View auf Doc haben.

Sie haben z.Z. drei Bereiche:

1) Daten im Programmspeicher (Attribute der Doc-Klasse)

2) Daten in einem permanenten Archiv (Dateien Wuerfel1.sav, Wuerfel2.sav, ...)

3) Grafisch oder textlich veranschaulichte Daten auf dem Bildschirm (oder später Drucker)
 


7.2 Single Document Interface (SDI)

Nachdem Sie nun das Doc/View-Modell mit der Trennung von Daten und Ansicht in Aktion gesehen haben, wenden wir uns nun detaillierter dem Rahmenfenster, den Ansichten und der Anwendung zu. Wir bleiben bei dem sogenannten Single Document Interface (SDI), da das Multiple Document Interface (MDI) heute nicht mehr empfohlen wird.
 

7.2.1 Erstellung der SDI-Anwendung, Rahmenfenster und Systemmenü

Erstellen Sie mit dem Anwendungsassistent eine neue SDI-Anwendung mit Namen "SDI001", übernehmen Sie alle Standardeinstellungen. Der einzige Schritt, in dem Sie aktiv eingreifen müssen, ist der Schritt "Anwendungstyp". Dort wählen Sie nicht "Mehrfaches Dokument", sondern "Einfaches Dokument" aus. Genau genommen ist es merkwürdig, dass MDI voreingestellt ist, denn MS rät Entwicklern inzwischen, von MDI Abstand zu nehmen, d.h. man soll jedes Dokument in einer eigenen Anwendung starten, denn für die heutigen Rechner mit ihren hervorragenden Multitasking-Fähigkeiten ist das kein technisches Problem mehr.

Als visuellen Stil würde ich Windows 7 wählen. Dateierweiterung: dat

Beachten Sie die Schritte "Benutzeroberflächenfunktionen" und "Erweiterte Funktionen" des Assistenten:


Abb. 7.11: Benutzeroebrflächenfunktionen wie Systemmenü, Symbolleisten und Statusleiste

Abb. 7.12: Erweiterte Funktionen wie Drucken/Druckvorschau, ActiveX-Steuerelemente und erweiterte Framebereiche

Dort sind verschiedene Zutaten bereits eingestellt: Symbolleiste, Statusleiste, Drucken plus Druckvorschau, ActiveX-Steuerelemente und erweiterte Framebereiche.

Lassen Sie diese Punkte ausgewählt, weil wir uns damit noch beschäftigen wollen. Bei Benutzeroebrflächenfunktionen hätten wir auch ein geteiltes Fenster wählen können.


Schauen Sie sich nun die Funktion CMainFrame::PreCreateWindow(...), die Sie am schnellsten via Klassenansicht finden, wie folgt aus:
 
BOOL CMainFrame::PreCreateWindow(CREATESTRUCT& cs)
{
    if( !CFrameWndEx::PreCreateWindow(cs) )
        return FALSE;
    // TODO: Ändern Sie hier die Fensterklasse oder die Darstellung, indem Sie CREATESTRUCT cs modifizieren.

    return TRUE;
}

Hier wird also nur die Funktion der Elternklasse CFrameWndEx::PreCreateWindow(...) ausgeführt.

Wenn Sie die Minimieren- und Maximieren-Schaltfläche abwählen wollen, was eigentlich keinen Sinn macht, dann müssten Sie diese Funktion wie folgt abändern:
 
BOOL CMainFrame::PreCreateWindow(CREATESTRUCT& cs)
{
    if( !CFrameWndEx::PreCreateWindow(cs) )
        return FALSE;

    cs.style = WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_THICKFRAME | FWS_ADDTOTITLE; 
    return TRUE;
}

Der normalerweise vorgegebene Window-Style ist WS_OVERLAPPEDWINDOW:
 
WS_OVERLAPPEDWINDOW  kombiniert folgende Window-Styles: 

WS_OVERLAPPED, 
WS_CAPTION, 
WS_SYSMENU, 
WS_THICKFRAME, 
WS_MINIMIZEBOX, 
WS_MAXIMIZEBOX.

Sie sehen, dass bei Abwahl von WS_MIMIMIZEBOX und WS_MAXIMIZEBOX der verbleibende Rest dann cs.style zugewiesen wird.
WS_SYSMENU entspricht "Systemmenü" und WS_THICKFRAME steht für "Breiter Rahmen".

In dieser Funktion können Sie also auch später noch Korrekturen vornehmen. Der Assistent trifft hier nur eine erste Wahl.

Neu ist für uns FWS_ADDTOTITLE. Hier haben wir ein Beispiel eines Frame-Window Style (FWS). Es gibt noch weitere:
 
Frame-Window Style (FWS) Bedeutung
FWS_ADDTOTITLE  Der Document-Titel wird dem Namen der Anwendung hinzugefügt. 
FWS_PREFIXTITLE  Funktioniert zusammen mit FWS_ADDTOTITLE.
Document-Titel steht vor dem Namen der Anwendung. 
Dies ist die Standardeinstellung des MFC-Assistenten.
FWS_SNAPTOBARS Steuert die Größe des Rahmenfensters, das eine Steuerleiste ("control bar") als frei bewegliches ("floating") Fenster  beherbergt. Das Rahmenfenster wird der Größe der Steuerleiste angepaßt.

ohne FWS-Style: nur SDI001


 

mit FWS_ADDTOTITLE: SDI001 - Unbenannt


 

FWS_ADDTOTITLE | FWS_PREFIXTITLE:


 

Abb. 7.12 - 7.14: Verschiedene Styles für den Titel


Was soll eigentlich dieses merkwürdige "Unbenannt" im Titel? Bei Winword oder Excel heißt dies z.B. "Dokument1" bzw. "Mappe1". Wir wollen hier auch einen eigenen Begriff vorgeben. Wie kann man diesen String verändern? Es handelt sich um den Namen für das Dokument. Also müssen wir auch dort ansetzen. Der richtige Platz ist in der Doc-Klasse CSDI001Doc bei der Funktion OnNewDocument():
 
BOOL CSDI001Doc::OnNewDocument()
{
    if (!CDocument::OnNewDocument())
        return FALSE;

    // TODO: Hier Code zur Reinitialisierung einfügen
    // (SDI-Dokumente verwenden dieses Dokument)
    
    SetTitle(L"Neuer Titel");

    return TRUE;
}

Die Titelleiste hat nun den von uns vorgegebenen String vor dem "- SDI001" eingefügt. Das "Unbenannt" sind wir also los.
Hierzu haben wir die MFC-Funktion void CDocument::SetTitle( LPCTSTR lpszTitle ) verwendet.

 

Abb. 7.15: Ersetzung des "Unbenannt" duch einen eigenen Titel

7.2.2 Elemente des Rahmenfensters - Grundlagen

Schauen wir uns die optische Erscheinung unserer Anwendung genauer an und suchen den Bezug im Sourcecode.

Sie können das Rahmenfenster von oben nach unten grob in fünf Zonen einteilen:

1. Systemmenü (auch Fenstermenü genannt)
2. Menü
3. Symbolleiste  (Tool Bar)
4. Client-Bereich
5. Statusleiste (Status Bar) 

Z usätzlich haben wir noch die Möglichkeit folgende Elemente einzublenden: Klassenansicht, Dateiansicht, Ausgabefenster und Eigenschaftsfenster.


Nun suchen wir in unserer Anwendung nach diesen Elementen:

Beginnen wir mit der Klassendefinition unserer von CFrameWndEx abgeleiteten Klasse CMainFrame.
Dort entdecken wir folgende Member-Variablen:

protected:  // Eingebettete Member der Steuerleiste
    CMFCMenuBar       m_wndMenuBar;
    CMFCToolBar       m_wndToolBar;
    CMFCStatusBar     m_wndStatusBar;
    CMFCToolBarImages m_UserImages;
    CFileView         m_wndFileView;
    CClassView        m_wndClassView;
    COutputWnd        m_wndOutput;
    CPropertiesWnd    m_wndProperties;

Schauen wir uns diese Klasse noch einmal von allem für uns Überflüssigen bereinigt an:
 
 
class CMainFrame : public CFrameWndEx
{
protected: 
  CMainFrame();
  DECLARE_DYNCREATE(CMainFrame)

public:
  virtual BOOL PreCreateWindow(CREATESTRUCT& cs);
  virtual BOOL LoadFrame(UINT nIDResource, DWORD dwDefaultStyle = WS_OVERLAPPEDWINDOW | FWS_ADDTOTITLE, CWnd* pParentWnd = NULL, CCreateContext* pContext = NULL);

  virtual ~CMainFrame();

protected: 
  CMFCMenuBar       m_wndMenuBar;
  CMFCToolBar       m_wndToolBar;
  CMFCStatusBar     m_wndStatusBar;
  CMFCToolBarImages m_UserImages;
  CFileView         m_wndFileView;
  CClassView        m_wndClassView;
  COutputWnd        m_wndOutput;
  CPropertiesWnd    m_wndProperties;

  afx_msg int OnCreate(LPCREATESTRUCT lpCreateStruct);
  afx_msg void OnViewCustomize();
  afx_msg LRESULT OnToolbarCreateNew(WPARAM wp, LPARAM lp);
  afx_msg void OnApplicationLook(UINT id);
  afx_msg void OnUpdateApplicationLook(CCmdUI* pCmdUI);
  afx_msg void OnSettingChange(UINT uFlags, LPCTSTR lpszSection);
  DECLARE_MESSAGE_MAP()

  BOOL CreateDockingWindows();
  void SetDockingWindowIcons(BOOL bHiColorIcons);
};

Jetzt erkennen wir die Member dieser Rahmenfensterklasse: Da ist zunächst der Konstruktor CMainFrame() und der Destruktor ~CMainFrame(). Die eine Funktion sorgt für die Erzeugung unseres Objektes "Rahmenfenster", und die andere zerstört es. Das sind C++-Standardelemente einer Klasse. Was gibt es Spezifisches?

Wir finden wir weitere wichtige Funktionen:

virtual BOOL PreCreateWindow(CREATESTRUCT& cs);
int OnCreate(LPCREATESTRUCT lpCreateStruct);

Beginnen wir mit PreCreateWindow(...). Zunächst gibt es dort einen Rückgabewert vom Typ BOOL. Ist dieser Wert FALSE, so ist die Erstellung des Rahmenfensters gescheitert. Das wäre es dann gewesen. Klappt die Erstellung, so ist der Wert TRUE. Das wird in der Implementierung auch abgefragt:
 
BOOL CMainFrame::PreCreateWindow(CREATESTRUCT& cs)
{
  if( !CFrameWnd::PreCreateWindow(cs) )  return FALSE;

  cs.style = WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_THICKFRAME | FWS_ADDTOTITLE | FWS_PREFIXTITLE; // unser style

  return TRUE;
}

Wir gehen davon aus, das es klappt. Warum auch nicht?

Interessanter ist die Strukur cs vom Typ CREATESTRUCT. Also markieren und F1, und schon wird online bei MSDN nachgeschaut:

typedef struct tagCREATESTRUCT 

LPVOID lpCreateParams; 
HANDLE hInstance; 
HMENU hMenu; 
HWND hwndParent; 
int cy; 
int cx; 
int y; 
int x; 
LONG style; 
LPCSTR lpszName; 
LPCSTR lpszClass; 
DWORD dwExStyle;
} CREATESTRUCT;

Wenn Sie sich schon mit der Erstellung von Fenstern mittels WinAPI beschäftigt haben, sind Ihnen diese Parameter geläufig.
Die Fenstergeometrie und damit der Client-Bereich wird z.B. mittels der Ecke links oben ( x, y ), Breite ( cx ) und Höhe ( cy ) festgelegt.
Das Handle hMenu steht für das Menü in unserem Rahmenfenster.

Dort finden sich also die Ansatzpunkte für Geometrie und Menü unseres Fensters.

Nun zur Implentierung der Funktion OnCreate(...):
 
int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
    if (CFrameWndEx::OnCreate(lpCreateStruct) == -1) return -1;

    BOOL bNameValid;

    if (!m_wndMenuBar.Create(this))
    {
        TRACE0("Fehler beim Erstellen der Menüleiste.\n");
        return -1;      // Fehler beim Erstellen
    }

    m_wndMenuBar.SetPaneStyle(m_wndMenuBar.GetPaneStyle() | CBRS_SIZE_DYNAMIC | CBRS_TOOLTIPS | CBRS_FLYBY);

    // Verhindern, dass die Menüleiste beim Aktivieren den Fokus erhält
    CMFCPopupMenu::SetForceMenuFocus(FALSE);

    if (!m_wndToolBar.CreateEx(this, TBSTYLE_FLAT, WS_CHILD | WS_VISIBLE | CBRS_TOP | CBRS_GRIPPER | CBRS_TOOLTIPS | CBRS_FLYBY | CBRS_SIZE_DYNAMIC) ||
        !m_wndToolBar.LoadToolBar(theApp.m_bHiColorIcons ? IDR_MAINFRAME_256 : IDR_MAINFRAME))
    {
        TRACE0("Fehler beim Erstellen der Symbolleiste.\n");
        return -1;      // Fehler beim Erstellen
    }

    CString strToolBarName;
    bNameValid = strToolBarName.LoadString(IDS_TOOLBAR_STANDARD);
    ASSERT(bNameValid);
    m_wndToolBar.SetWindowText(strToolBarName);

    CString strCustomize;
    bNameValid = strCustomize.LoadString(IDS_TOOLBAR_CUSTOMIZE);
    ASSERT(bNameValid);
    m_wndToolBar.EnableCustomizeButton(TRUE, ID_VIEW_CUSTOMIZE, strCustomize);

    // Benutzerdefinierte Symbolleistenvorgänge zulassen:
    InitUserToolbars(NULL, uiFirstUserToolBarId, uiLastUserToolBarId);

    if (!m_wndStatusBar.Create(this))
    {
        TRACE0("Fehler beim Erstellen der Statusleiste.\n");
        return -1;      // Fehler beim Erstellen
    }
    m_wndStatusBar.SetIndicators(indicators, sizeof(indicators)/sizeof(UINT));

    // TODO: Löschen Sie diese fünf Zeilen, wenn Sie nicht möchten, dass die Symbolleiste und die Menüleiste andockbar sind
    m_wndMenuBar.EnableDocking(CBRS_ALIGN_ANY);
    m_wndToolBar.EnableDocking(CBRS_ALIGN_ANY);
    EnableDocking(CBRS_ALIGN_ANY);
    DockPane(&m_wndMenuBar);
    DockPane(&m_wndToolBar);

    // Andockfensterverhalten wie in Visual Studio 2005 aktivieren
    CDockingManager::SetDockingMode(DT_SMART);
    // Automatisches Ausblenden von Andockfenstern wie in Visual Studio 2005 aktivieren
    EnableAutoHidePanes(CBRS_ALIGN_ANY);

    // Menüelementbild laden (nicht auf Standardsymbolleisten platziert):
    CMFCToolBar::AddToolBarForImageCollection(IDR_MENU_IMAGES, theApp.m_bHiColorIcons ? IDB_MENU_IMAGES_24 : 0);

    // Andockfenster erstellen
    if (!CreateDockingWindows())
    {
        TRACE0("Fehler beim Erstellen der Andockfenster.\n");
        return -1;
    }

    m_wndFileView.EnableDocking(CBRS_ALIGN_ANY);
    m_wndClassView.EnableDocking(CBRS_ALIGN_ANY);
    DockPane(&m_wndFileView);
    CDockablePane* pTabbedBar = NULL;
    m_wndClassView.AttachToTabWnd(&m_wndFileView, DM_SHOW, TRUE, &pTabbedBar);
    m_wndOutput.EnableDocking(CBRS_ALIGN_ANY);
    DockPane(&m_wndOutput);
    m_wndProperties.EnableDocking(CBRS_ALIGN_ANY);
    DockPane(&m_wndProperties);

    // Visuellen Manager und Stil auf Basis eines persistenten Werts festlegen
    OnApplicationLook(theApp.m_nAppLook);

    // Umpositionieren des Menüs für Symbolleisten und Andockfenster aktivieren
    EnablePaneMenu(TRUE, ID_VIEW_CUSTOMIZE, strCustomize, ID_VIEW_TOOLBAR);

    // Schnelles Anpassen von Symbolleisten mit Alt+Ziehen aktivieren
    CMFCToolBar::EnableQuickCustomization();

    if (CMFCToolBar::GetUserImages() == NULL)
    {
        // Benutzerdefinierte Symbolleistenbilder laden
        if (m_UserImages.Load(_T(".\\UserImages.bmp")))
        {
            CMFCToolBar::SetUserImages(&m_UserImages);
        }
    }

    // Menüpersonalisierung aktivieren (zuletzt verwendete Befehle)
    // TODO: Definieren Sie eigene Basisbefehle, wobei jedes Pulldownmenü mindestens einen Basisbefehl enthalten muss.
    CList<UINT, UINT> lstBasicCommands;

    lstBasicCommands.AddTail(ID_FILE_NEW);
    lstBasicCommands.AddTail(ID_FILE_OPEN);
    lstBasicCommands.AddTail(ID_FILE_SAVE);
    lstBasicCommands.AddTail(ID_FILE_PRINT);
    lstBasicCommands.AddTail(ID_APP_EXIT);
    lstBasicCommands.AddTail(ID_EDIT_CUT);
    lstBasicCommands.AddTail(ID_EDIT_PASTE);
    lstBasicCommands.AddTail(ID_EDIT_UNDO);
    lstBasicCommands.AddTail(ID_APP_ABOUT);
    lstBasicCommands.AddTail(ID_VIEW_STATUS_BAR);
    lstBasicCommands.AddTail(ID_VIEW_TOOLBAR);
    lstBasicCommands.AddTail(ID_VIEW_APPLOOK_OFF_2003);
    lstBasicCommands.AddTail(ID_VIEW_APPLOOK_VS_2005);
    lstBasicCommands.AddTail(ID_VIEW_APPLOOK_OFF_2007_BLUE);
    lstBasicCommands.AddTail(ID_VIEW_APPLOOK_OFF_2007_SILVER);
    lstBasicCommands.AddTail(ID_VIEW_APPLOOK_OFF_2007_BLACK);
    lstBasicCommands.AddTail(ID_VIEW_APPLOOK_OFF_2007_AQUA);
    lstBasicCommands.AddTail(ID_VIEW_APPLOOK_WINDOWS_7);
    lstBasicCommands.AddTail(ID_SORTING_SORTALPHABETIC);
    lstBasicCommands.AddTail(ID_SORTING_SORTBYTYPE);
    lstBasicCommands.AddTail(ID_SORTING_SORTBYACCESS);
    lstBasicCommands.AddTail(ID_SORTING_GROUPBYTYPE);

    CMFCToolBar::SetBasicCommands(lstBasicCommands);

    return 0;
}

Das ist ja ein richtiger Brocken. Sie sehen der Assistent arbeitet fleissig für uns. Analysieren Sie diese Zeilen in Ruhe und mittels MSDN. Experimentieren Sie damit und schauen Sie sich die Auswirkungen an.

Beginnen wir sofort mit einfachen Experimenten:

1) Wir wollen keine andockbare Symbolleiste: Das ist leicht, da müssen wir einfach die fünf Zeilen, wie angegeben, auskommentieren. Kommentieren Sie auf jeden Fall aus, also nicht wirklich wegstreichen, es sei denn Sie kennen die Zeilen auswendig, und schon ist es vorbei mit der "vagabundierenden" Symbolleiste. Jetzt hängt sie fest. 

2) Wir wollen gar keine Symbolleiste (Toolbar): Dann wird eben auch die Erzeugung und der damit zusammenhängende Code gestrichen (nur das Nötigste wurde auskommentiert).

int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
    if (CFrameWndEx::OnCreate(lpCreateStruct) == -1)
        return -1;

    //    BOOL bNameValid;

    if (!m_wndMenuBar.Create(this))
    {
        TRACE0("Fehler beim Erstellen der Menüleiste.\n");
        return -1;      // Fehler beim Erstellen
    }

    m_wndMenuBar.SetPaneStyle(m_wndMenuBar.GetPaneStyle() | CBRS_SIZE_DYNAMIC | CBRS_TOOLTIPS | CBRS_FLYBY);

    // Verhindern, dass die Menüleiste beim Aktivieren den Fokus erhält
    CMFCPopupMenu::SetForceMenuFocus(FALSE);

    /*
    if (!m_wndToolBar.CreateEx(this, TBSTYLE_FLAT, WS_CHILD | WS_VISIBLE | CBRS_TOP | CBRS_GRIPPER | CBRS_TOOLTIPS | CBRS_FLYBY | CBRS_SIZE_DYNAMIC) ||
        !m_wndToolBar.LoadToolBar(theApp.m_bHiColorIcons ? IDR_MAINFRAME_256 : IDR_MAINFRAME))
    {
        TRACE0("Fehler beim Erstellen der Symbolleiste.\n");
        return -1;      // Fehler beim Erstellen
    }   

    CString strToolBarName;
    bNameValid = strToolBarName.LoadString(IDS_TOOLBAR_STANDARD);
    ASSERT(bNameValid);
    m_wndToolBar.SetWindowText(strToolBarName);
   
    CString strCustomize;
    bNameValid = strCustomize.LoadString(IDS_TOOLBAR_CUSTOMIZE);
    ASSERT(bNameValid);
    m_wndToolBar.EnableCustomizeButton(TRUE, ID_VIEW_CUSTOMIZE, strCustomize);
   
    // Benutzerdefinierte Symbolleistenvorgänge zulassen:
    InitUserToolbars(NULL, uiFirstUserToolBarId, uiLastUserToolBarId);
    */

    if (!m_wndStatusBar.Create(this))
    {
        TRACE0("Fehler beim Erstellen der Statusleiste.\n");
        return -1;      // Fehler beim Erstellen
    }
    m_wndStatusBar.SetIndicators(indicators, sizeof(indicators)/sizeof(UINT));

    /*
    // TODO: Löschen Sie diese fünf Zeilen, wenn Sie nicht möchten, dass die Symbolleiste und die Menüleiste andockbar sind
    m_wndMenuBar.EnableDocking(CBRS_ALIGN_ANY);
    m_wndToolBar.EnableDocking(CBRS_ALIGN_ANY);
    EnableDocking(CBRS_ALIGN_ANY);
    DockPane(&m_wndMenuBar);
    DockPane(&m_wndToolBar);
    */

    // Andockfensterverhalten wie in Visual Studio 2005 aktivieren
    CDockingManager::SetDockingMode(DT_SMART);
    // Automatisches Ausblenden von Andockfenstern wie in Visual Studio 2005 aktivieren
    EnableAutoHidePanes(CBRS_ALIGN_ANY);

    // Menüelementbild laden (nicht auf Standardsymbolleisten platziert):
    CMFCToolBar::AddToolBarForImageCollection(IDR_MENU_IMAGES, theApp.m_bHiColorIcons ? IDB_MENU_IMAGES_24 : 0);

    // Andockfenster erstellen
    if (!CreateDockingWindows())
    {
        TRACE0("Fehler beim Erstellen der Andockfenster.\n");
        return -1;
    }

    m_wndFileView.EnableDocking(CBRS_ALIGN_ANY);
    m_wndClassView.EnableDocking(CBRS_ALIGN_ANY);
    DockPane(&m_wndFileView);
    CDockablePane* pTabbedBar = NULL;
    m_wndClassView.AttachToTabWnd(&m_wndFileView, DM_SHOW, TRUE, &pTabbedBar);
    m_wndOutput.EnableDocking(CBRS_ALIGN_ANY);
    DockPane(&m_wndOutput);
    m_wndProperties.EnableDocking(CBRS_ALIGN_ANY);
    DockPane(&m_wndProperties);

    // Visuellen Manager und Stil auf Basis eines persistenten Werts festlegen
    OnApplicationLook(theApp.m_nAppLook);
    /*
    // Umpositionieren des Menüs für Symbolleisten und Andockfenster aktivieren
    EnablePaneMenu(TRUE, ID_VIEW_CUSTOMIZE, strCustomize, ID_VIEW_TOOLBAR);
    */
    // Schnelles Anpassen von Symbolleisten mit Alt+Ziehen aktivieren
    CMFCToolBar::EnableQuickCustomization();

    if (CMFCToolBar::GetUserImages() == NULL)
    {
        // Benutzerdefinierte Symbolleistenbilder laden
        if (m_UserImages.Load(_T(".\\UserImages.bmp")))
        {
            CMFCToolBar::SetUserImages(&m_UserImages);
        }
    }

    // Menüpersonalisierung aktivieren (zuletzt verwendete Befehle)
    // TODO: Definieren Sie eigene Basisbefehle, wobei jedes Pulldownmenü mindestens einen Basisbefehl enthalten muss.
    CList<UINT, UINT> lstBasicCommands;

    lstBasicCommands.AddTail(ID_FILE_NEW);
    lstBasicCommands.AddTail(ID_FILE_OPEN);
    lstBasicCommands.AddTail(ID_FILE_SAVE);
    lstBasicCommands.AddTail(ID_FILE_PRINT);
    lstBasicCommands.AddTail(ID_APP_EXIT);
    lstBasicCommands.AddTail(ID_EDIT_CUT);
    lstBasicCommands.AddTail(ID_EDIT_PASTE);
    lstBasicCommands.AddTail(ID_EDIT_UNDO);
    lstBasicCommands.AddTail(ID_APP_ABOUT);
    lstBasicCommands.AddTail(ID_VIEW_STATUS_BAR);
    lstBasicCommands.AddTail(ID_VIEW_TOOLBAR);
    lstBasicCommands.AddTail(ID_VIEW_APPLOOK_OFF_2003);
    lstBasicCommands.AddTail(ID_VIEW_APPLOOK_VS_2005);
    lstBasicCommands.AddTail(ID_VIEW_APPLOOK_OFF_2007_BLUE);
    lstBasicCommands.AddTail(ID_VIEW_APPLOOK_OFF_2007_SILVER);
    lstBasicCommands.AddTail(ID_VIEW_APPLOOK_OFF_2007_BLACK);
    lstBasicCommands.AddTail(ID_VIEW_APPLOOK_OFF_2007_AQUA);
    lstBasicCommands.AddTail(ID_VIEW_APPLOOK_WINDOWS_7);
    lstBasicCommands.AddTail(ID_SORTING_SORTALPHABETIC);
    lstBasicCommands.AddTail(ID_SORTING_SORTBYTYPE);
    lstBasicCommands.AddTail(ID_SORTING_SORTBYACCESS);
    lstBasicCommands.AddTail(ID_SORTING_GROUPBYTYPE);

    CMFCToolBar::SetBasicCommands(lstBasicCommands);

    return 0;
}

 

3) Wir wollen keine Statusleiste, jedoch eine Symbolleiste:

Dazu "streichen" wir nur den nachstehenden Block. Die Teile für die Symbolleiste belassen wir aktiv.
 
/*
        if (!m_wndStatusBar.Create(this))
    {
        TRACE0("Fehler beim Erstellen der Statusleiste.\n");
        return -1;      // Fehler beim Erstellen
    }
    m_wndStatusBar.SetIndicators(indicators, sizeof(indicators)/sizeof(UINT));

*/

Da gibt es doch gleich viel mehr Platz für unsere "Views".


4) Nun wollen wir die Geometrie des Fensters beeinflussen:

Dazu ist CMainFrame::PreCreateWindow(...) wieder der richtige Ort:
 
BOOL CMainFrame::PreCreateWindow(CREATESTRUCT& cs)
{
 /* ... */

 cs.cx = 300; // Breite
 cs.cy = 200; // Höhe

 if( !CFrameWnd::PreCreateWindow(cs) ) return FALSE;

  cs.style = WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_THICKFRAME | FWS_ADDTOTITLE | FWS_PREFIXTITLE;

  return TRUE;
}

... und schon erscheint das Fenster in der gewünschten Größe. Sie können es aber - wie bei Windows gewohnt - durch "Ziehen mit der Maus" in der Größe verändern.


Sie haben nun einen ersten groben Überblick über die Beeinflussung des "Rahmens", den MS Windows um unsere "View" spannt.

Machen Sie sich vor allem noch einmal den elementaren Aufbau einer SDI-Anwendung aus Anwendung, Rahmen, Dokument und Ansicht klar.

Zutaten wie Ressourcen, Menüs, Symbolleisten und Statusleiste sind keine Eckpfeiler, aber sie hängen wie "liebliche Erker" an unserem massiven Rohbau und der heutige GUI-Programmierer darf sich verstärkt mit diesen Feinheiten beschäftigen, an die der Windows-Benutzer gewöhnt wurde. Beginnen wir mit Symbol- und Statusleisten.
 

>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
Trennlinie der Überarbeitung Version 6 nach Version 14


7.2.3 Symbolleiste

7.2.3.1 Standardsymbolleiste IDR_MAINFRAME

Warum heißt die Symbolleiste eigentlich Symbolleiste und nicht Werkzeugleiste? Wahrscheinlich, weil wir in dieser Leiste so viele schöne Symbole darstellen können. Diese Symbole stellen z.B. Werkzeuge und Hilfsmittel dar wie Papier, Ordner, Diskette, Schere, Drucker etc. Schauen wir uns in der Standardsymbolleiste das dritte Symbol "Diskette" an. Es steht für den Arbeitsvorgang "Speichern" In der heutigen Zeit wäre hier eine Festplatte sicher angebrachter. Wir wollen mit unserer Anwendung modern sein und daher dieses Symbol in ein "Festplatten"-Symbol umwandeln. Wie packen wir das an?

Bilder, Icons und Symbole sind Ressourcen. Daher werden wir dort auch fündig. Es gibt einen Ordner "Toolbar" und darin die Ressource IDR_MAINFRAME. In der nachstehenden Abbildung sehen Sie das erste Symbol dieser Leiste: ein Blatt Papier (mit Eselsohr!). Sie finden eine Auflösung von Breite = 16 Pixel mal Höhe = 15 Pixel vor. Übrigens hat die Symbolschaltfläche, also der für den Benutzer sichtbare Button, eine Auflösung von Breite = 24 Pixel mal Höhe = 22 Pixel. Da sind nun Ihre zeichnerichen Fähigkeiten auf engstem Raum gefordert.

Wir tauschen das dritte Symbol mutig durch eine Festplatte (hard disk) aus, nachfolgend mein spartanisches Ergebnis:

Reicht diese Veränderung schon aus? Was denken Sie?
Wir speichern, kompilieren, linken und führen aus:

Es hat funktioniert! Sie sehen das geht einfach.

Sie können natürlich die Größe der Symbole im Ressourcen-Editor durch einfaches Ziehen mit der Maus einstellen und die grafische Verarbeitung mit einem anderen Zeichenprogramm (z.B. Paint) durchführen. Die Bitmap für die Standardsymbolleiste findet sich im Unterverzeichnis /res unter dem Namen toolbar.bmp.

Nun führen wir den vollständigen Bezug von der Ressource IDR_MAINFRAME zu unserem Programm herbei. Dazu lösen wir die Bedingung innerhalb der if-Kontrollstruktur heraus und lassen die beiden Negationen und das logische ODER weg. Als Ergebnis finden wir zwei aufeinander folgende Vorgänge:
 

 m_wndToolBar.CreateEx
 (
  this, 
  TBSTYLE_FLAT, 
  WS_CHILD            | 
  WS_VISIBLE          | 
  CBRS_TOP            | 
  CBRS_GRIPPER        | 
  CBRS_TOOLTIPS       | 
  CBRS_FLYBY          | 
  CBRS_SIZE_DYNAMIC
 )

 m_wndToolBar.LoadToolBar( IDR_MAINFRAME )

Im ersten Schritt erzeugen wir eine Symbolleiste durch Anwendung der Member-Funktion CreateEx(...) auf unser Objekt m_wndToolBar der Klasse CToolBar, und im zweiten Schritt binden wir die Ressource Symbolleiste IDR_MAINFRAME mit der Member-Funktion LoadToolBar(...) an dieses Objekt an. Alles easy? Kompliziert wird das alles nur durch die Werte im dritten Parameter.

Betrachten wir die Funktion CreateEx(...) genau:

BOOL CToolBar::CreateEx
(
  CWnd* pParentWnd,
  DWORD dwCtrlStyle = TBSTYLE_FLAT,
  DWORD dwStyle     = WS_CHILD | WS_VISIBLE | CBRS_ALIGN_TOP,
  CRect rcBorders   = CRect(0, 0, 0, 0),
  UINT  nID         = AFX_IDW_TOOLBAR
);

pParentWnd:   Zeiger auf das Elternfenster der Symbolleiste
dwCtrlStyle:  Styles für das eingebettete Objekt der Klasse CToolBarCtrl
dwStyle:      Styles der Symbolleiste
rcBorders:    Rahmen-Rechteck für die Symbolleiste
nID:          Kindfenster-ID der Symbolleiste

Die Parameter  2, 3 und 4 bestimmen das Aussehen und Verhalten unserer Symbolleiste. Ein weites Betätigungsfeld liegt vor uns.
Wir sehen auch, dass der Assistent kein Standard-Objekt der Klasse CToolBar erzeugt, sondern seine eigene Note bietet.
Eigentlich müssen wir doch nur den ersten Parameter, d.h. den this-Zeiger auf das Objekt der Klasse CMainFrame, angeben.
Also testen wir sofort den eigentlichen MFC-Standard aus:
 
int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
 if (CFrameWnd::OnCreate(lpCreateStruct) == -1) return -1;

 /*
  if (!m_wndToolBar.CreateEx(this, TBSTYLE_FLAT, WS_CHILD | WS_VISIBLE | CBRS_TOP
  | CBRS_GRIPPER | CBRS_TOOLTIPS | CBRS_FLYBY | CBRS_SIZE_DYNAMIC) ||
  !m_wndToolBar.LoadToolBar(IDR_MAINFRAME))
 */

 if (!m_wndToolBar.CreateEx(this) || !m_wndToolBar.LoadToolBar(IDR_MAINFRAME))
 {
  TRACE0("Symbolleiste konnte nicht erstellt werden\n");
  return -1; // Fehler bei Erstellung
 }
 ...
 ...
}

Also programmtechnisch sieht das viel schöner - sprich: klarer - aus.

Der "Griff" am linken Ende und die Tooltips sind jedoch weg.

Wir wenden uns dem dritten Parameter zu. Zunächst befassen wir uns mit dem Standard: WS_CHILD | WS_VISIBLE | CBRS_ALIGN_TOP

Neu ist CBRS_ALIGN_TOP. Es bedeutet, dass die Symbolleiste oben andocken darf.  Gut, das ist nichts Aufregendes. Es sei denn, man läßt es weg und gibt nur WS_CHILD und WS_VISIBLE an. Dann fehlt gleich die ganze Symbolleiste. Keine Andockerlaubnis, keine Symbolleiste. Aha! Also weiter.

Wie ist das mit dem Griff (engl. gripper)? Wir fügen CBRS_GRIPPER hinzu,
... und weil wir experimentierfreudig sind, geben wir die Andockerlaubnis diesmal unten:
 
if (!m_wndToolBar.CreateEx(this, TBSTYLE_FLAT, WS_CHILD | WS_VISIBLE | CBRS_ALIGN_BOTTOM | CBRS_GRIPPER ) || !m_wndToolBar.LoadToolBar(IDR_MAINFRAME))
...

Nun haben wir den gewohnten "Griff" (bei horizontaler Anordnung: links, bei vertikaler Anordnung: oben). Da macht das In-See-stechen (sprich: floaten) und das Andocken doch viel mehr Freude.

Schauen wir weiter. Da gibt es noch eine weitere Einstellung bezüglich der Größeneinstellung der Symbolleiste:
CBRS_SIZE_DYNAMIC und CBRS_SIZE_FIXED.

Wir sind natürlich dynamisch, und fügen diesen Style hinzu. Was ist jetzt möglich?

Dynamisch genug? Sie können das Kindfenster sogar aus dem Bereich des Elternfensters hinaus bewegen? Nein, nein. Das ging auch vorher schon. Der "Zeilenumbruch" innerhalb der Symbolleiste ist die wahre Dynamik!

Keine Tooltips? Das ist für den heutigen Windows-Nutzer eher verblüffend. Dafür gibt es doch CBRS_TOOLTIPS. Bei unserem eigenen Bild für das Speichern müssen wir diesen Komfort bieten. Das nutzen wir auf jeden Fall, solange der Assistent selbst die Texte geschrieben hat:

Na sieht das nicht besser aus? Die Maus zeigt hierbei übrigens (auf dem Bild nicht sichtbar) auf das Speichern-Symbol.

Da gibt es noch CBRS_FLYBY. Was bedeutet dies? Wir fügen es mit dazu und vergleichen die beiden Bilder, wenn wir die Maustaste über das Speichern-Symbol halten:

Sehen Sie den Unterschied? Nein, dann achten Sie auf den Text in der Statuszeile. Der wird nun ständig aktualisiert, schon beim "Vorbeifliegen". Wir müssen nicht mehr "Klick" machen, sondern nur wie ein römischer Imperator unseren virtuell verlängerten Zeigefinger würdevoll (also bitte nicht zu schnell) über die Symbole gleiten lassen.

Sie sehen: Die Grundeinstellungen des MFC-Assistenten sind für die Praxis gut brauchbar, und der Benutzer ist es in dieser Form gewöhnt.

Also zurück zum Standard, den der Assistent erzeugt :
 
if (!m_wndToolBar.CreateEx(this, TBSTYLE_FLAT, WS_CHILD | WS_VISIBLE | CBRS_TOP
  | CBRS_GRIPPER | CBRS_TOOLTIPS | CBRS_FLYBY | CBRS_SIZE_DYNAMIC) ||
  !m_wndToolBar.LoadToolBar(IDR_MAINFRAME))
...

Dann sieht das auch alles wieder ordentlich aus!
Aber halt! Da war doch noch dieser vierte Parameter: das Rechteck. Was nützt diese Einstellung?
Probieren wir es aus:
 
if (!m_wndToolBar.CreateEx(this, TBSTYLE_FLAT, WS_CHILD | WS_VISIBLE | CBRS_TOP
  | CBRS_GRIPPER | CBRS_TOOLTIPS | CBRS_FLYBY | CBRS_SIZE_DYNAMIC, CRect(30, 15, 0, 0)) ||
  !m_wndToolBar.LoadToolBar(IDR_MAINFRAME))
...

Wir haben unsere Symbolleiste nun 30 Einheiten nach rechts und 15 Einheiten nach unten "geschoben". Optisch natürlich sehr unschön.

Mit CRect( 30, 15, 30, 15 ) können wir hier Symmetrie ins Spiel bringen:

Sie sehen, das dieses Rechteck als Rahmen um unsere Symbolleiste benutzt wird.

Probieren Sie verschiedene Einstellungen aus. Es funktionieren auch negative Zahlen, z.B. CRect( -2, -2, -2, -4 ):

Das sieht doch richtig schlank aus, oder? Vielleicht die richtige Einstellung für Anwendungen mit vielen Symbolleisten.
 

7.2.3.2 Eigene Symbolleiste hinzufügen

Damit wir etwas mehr Übung mit Symbolleisten erzielen, fügen wir eine eigene zweite Symbolleiste zur Standardsymbolleiste hinzu.
Erstellen Sie eine völlig neue SDI-Anwendung namens "SDI_plus". Übernehmen Sie dabei alle Voreinstellungen.

Zunächst benötigen wir in der Klasse CMainFrame ein weiteres Objekt der Klasse CToolBar.
Wir verwenden die Bezeichnung m_wndToolBar1:
 
class CMainFrame : public CFrameWnd
{
...
...
protected:  // Eingebundene Elemente der Steuerleiste
 CStatusBar  m_wndStatusBar;
 CToolBar    m_wndToolBar, m_wndToolBar1;
...
};

Eine eigenen Symbolleiste benötigt auch eine eigene Ressource. Fügen Sie hierzu IDR_TOOLBAR1 (vorgegebener Name) ein:

Fangen Sie bitte nicht an zu "pinseln". Wir wollen zunächst eine "nackte" Symbolleiste entwerfen, um die Grundlagen besser zu erkennen.
Zum Speichern müssen Sie vorher zumindest kurz mit dem Stift (in grau, damit kein schwarzer Punkt entsteht) in das Symbol klicken. Ansonsten geht diese Symbolleiste wieder verloren. Bitte beachten.

Jetzt haben wir ein Objekt der Klasse CToolBar und eine Toolbar-Ressource. Diese beiden Elemente fügen wir an der bereits bekannten Stelle in CMainFrame::OnCreate(...) zusammen:
 
int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
 if (CFrameWnd::OnCreate(lpCreateStruct) == -1) return -1;

 if (!m_wndToolBar.CreateEx(this, TBSTYLE_FLAT, WS_CHILD | WS_VISIBLE | CBRS_TOP
  | CBRS_GRIPPER | CBRS_TOOLTIPS | CBRS_FLYBY | CBRS_SIZE_DYNAMIC) ||
  !m_wndToolBar.LoadToolBar(IDR_MAINFRAME))
 {
  TRACE0("Standardsymbolleiste konnte nicht erstellt werden\n");
  return -1; // Fehler bei Erstellung
 }

 if (!m_wndToolBar1.CreateEx(this, TBSTYLE_FLAT, WS_CHILD | WS_VISIBLE | CBRS_TOP
  | CBRS_GRIPPER | CBRS_TOOLTIPS | CBRS_FLYBY | CBRS_SIZE_DYNAMIC) ||
  !m_wndToolBar1.LoadToolBar(IDR_TOOLBAR1))
 {
  TRACE0("Symbolleiste Nr.1 konnte nicht erstellt werden\n");
  return -1;      // Fehler bei Erstellung
 }

 ...

 //Andockerlaubnis des Rahmenfensters
 EnableDocking(CBRS_ALIGN_ANY);

 //Standardsymbolleiste
 m_wndToolBar.EnableDocking(CBRS_ALIGN_ANY);
 DockControlBar(&m_wndToolBar);

 //Eigene Symbolleiste Nr.1
 m_wndToolBar1.EnableDocking(CBRS_ALIGN_ANY);
 DockControlBar(&m_wndToolBar1);

 return 0;
}

Der eingefügte Code ist sozusagen eine Kopie der vom Assistenten vorgegebenen Zeilen, wobei wir m_wndToolBar gegen m_wndToolBar1 und IDR_MAINFRAME gegen IDR_TOOLBAR1 austauschen. Im unteren Teil haben wir umsortiert, damit die "Andockerlaubnisse/-befehle" für das Rahmenfenster und die beiden Symbolleisten klar getrennt sind.

Jetzt haben wir eine eigene "nackte" Symbolleiste hinzugefügt. Unsere Symbolleiste hat nur ein Symbol, nämlich eine graue Fläche ohne Bild.
Wenn wir mit dem Mauszeiger über dieses Symbol gehen, verschwindet der Text "Bereit" in der Statuszeile. Dies passiert auch, wenn wir auf das Symbol klicken. Wir können unsere Symbolleiste auch am "Griff" packen und "floaten" lassen bzw. links, rechts oder unten andocken:

Diese Symbolleiste besitzt bisher nur die Grundfunktionen. Wir werden ihr nun etwas Leben einhauchen. Alles eigene braucht einen Namen. Wir wollen unserer Symbolleiste die Überschrift "Eigene Symbolleiste Nr.1" verpassen. Wie geht dies? Ganz einfach! Unsere Symbolleiste ist ein Fenster und verfügt daher über die vererbten Member-Funktion der MFC-Klasse CWnd. Also wenden wir dies sofort an:
 
 //Standardsymbolleiste
 m_wndToolBar.EnableDocking(CBRS_ALIGN_ANY);
 DockControlBar(&m_wndToolBar);
 m_wndToolBar.SetWindowText("Standardsymbolleiste");

 //Eigene Symbolleiste Nr.1
 m_wndToolBar1.EnableDocking(CBRS_ALIGN_ANY);
 DockControlBar(&m_wndToolBar1);
 m_wndToolBar1.SetWindowText("Eigene Symbolleiste Nr.1");

Die Standardsymbolleiste haben wir einfach mitgetauft. Den Namen sieht man nur im frei beweglichen Zustand:

Unsere Symbolleiste ist im "Urzustand" so winzig, dass man noch nicht einmal den ersten Buchstaben unseres Strings richtig sieht.
Auf jeden Fall könnten wir jetzt beginnen, eigene Funktionalitäten an diese zweite Symbolleiste anbinden. Da sich das Thema Symbolleisten anbietet, werden wir fünf Symbole schaffen, die dann Funktionen ansteuern sollen, um die Standardsymbolleiste oben, unten, rechts und links anzudocken bzw. frei schwebend zu halten.

Der erste Schritt ist der Entwurf der fünf Symbole. Hier ist mein Entwurf, Sie sind natürlich frei in der künstlerischen Gestaltung:

Wenn wir jetzt kompilieren, erscheinen diese Symbole nur grau in grau, da wir noch keine aktive Funktion angebunden haben.
Daher brauchen unsere Symbole nun Bezeichnungen. Ohne diese Namen, die in Windows immer stellvertretend für Zahlen stehen, kommen wir nicht weiter. Also bitte munter IDs vergeben und auch gleich den Statuszeilentext generieren. Hier folgen meine IDs (STB steht bei mir für Standard Tool Bar) und Statuszeilentexte für die Symbole:


 
ID_STB_TOP Standardsymbolleiste oben\nOben
ID_STB_BOTTOM Standardsymbolleiste unten\nUnten
ID_STB_LEFT Standardsymbolleiste links\nLinks
ID_STB_RIGHT Standardsymbolleiste rechts\nRechts
ID_STB_FLOAT Standardsymbolleiste frei\nFrei

Nachdem wir die IDs vergeben haben, ordnen wir diesen 5 IDs in der Klasse CMainFrame Command-Nachrichten zu. Am Beispiel der Objekt-ID ID_STB_TOP wird dies hier gezeigt:

Als Name der Funktion belassen wir es beim Vorschlag von MSVC++: OnStbTop()

Der Funktionsrumpf wartet nun auf unsere Eingaben:
 
void CMainFrame::OnStbTop() 
{
 // TODO: Code für Befehlsbehandlungsroutine hier einfügen

}

So sieht die Anwendung nach Anlegen der ersten Funktion aus:

Die Maus zeigt hier im Bild unsichtbar auf das erste Symbol. Man sieht den zugehörigen Statuszeilentext und den Tooltiptext.

Führen Sie diese Aktion für alle 5 IDs unserer Symbolleiste durch. Nun wollen wir alle Funktionen mit dem zugehörigen Sourcecode füllen. Also beginnen wir mit CMainFrame::OnStbFloat(). Wir wollen die Standardsymbolleiste in diesem Fall frei "floaten" lassen.

Welche Funktion gibt es hierfür? Wenn Sie diese nun selbst suchen, gibt es mehrere Möglichkeiten. Man kann z.B. einfach m_wndTollBar1 mit dem Punkt eintippen und sich alle möglichen Funktionen anschauen. Das führt hier jedoch zu einer unüberschaubaren Vielfalt, da wir die ganzen Funktionen der Klasse CWnd zur Verfügung haben. So wird das nichts.

Eine selektivere Möglichkeit besteht darin, in MSDN die Member-Funktionen der Klasse CToolBar anzuschauen. Da finden wir nur Attribute und Funktionen zur Konstruktion. Also schauen wir nach der Oberklasse CControlBar. Da finden wir CControlBar::EnableDocking(...). Das kann es aber auch nicht sein?! Aber da gibt es doch noch den Hinweis "see also ...". Da finden wir folgende Funktion:

CFrameWnd* CFrameWnd::FloatControlBar( CControlBar * pBar, CPoint point, DWORD dwStyle = CBRS_ALIGN_TOP );

Das könnte es doch sein?! Also probieren wir es aus:
 
void CMainFrame::OnStbFloat() 
{
  CPoint pt( 100,100 );
  ClientToScreen( &pt );
  FloatControlBar( &m_wndToolBar, pt );
}

Geklappt! Die Standardsymbolleiste löst sich auf unseren Befehl aus ihrem "Dock" und "floatet".


 

Der zweite Parameter in der Funktion bezieht sich übrigens auf Bildschirmkoordinaten. Wir wollen die 100,100 jedoch als Client-Koordinaten verstanden wissen. Hierzu wandeln wir diese Daten zunächst in Client-Koordinaten um.
Das erledigt die Funktion CWnd::ClientToScreen( LPPOINT lpPoint ).

Es gibt bei dieser Funktion auch noch einen dritten Parameter, der auf CBRS_ALIGN_TOP voreingestellt ist. Dieser Parameter beschreibt die Ausrichtung der freischwebenden Symbolleiste.

CFrameWnd* FloatControlBar( CControlBar * pBar, CPoint point, DWORD dwStyle = CBRS_ALIGN_TOP );

CBRS_ALIGN_TOP oder CBRS_ALIGN_BOTTOM sorgen für die hoizontale Ausrichtung.
CBRS_ALIGN_LEFT oder CBRS_ALIGN_RIGHT sorgen für die vertikale Ausrichtung.

Was passiert, wenn man z.B. CBRS_ALIGN_TOP | CBRS_ALIGN_LEFT angibt? Da siegt die horizontale Ausrichtung!
 

Das war aber nur der erste Streich. Wir haben noch vier Symbole. Jetzt wollen wir wieder ins Dock mit der Auswahl Nordhafen, Südhafen, Westhafen und Osthafen (klingt irgendwie nach Monopoly, dort waren es aber Bahnhöfe).

Diesmal schauen wir gleich bei CFrameWnd-Funktionen nach und werden auch schnell fündig:

void CFrameWnd::DockControlBar( CControlBar * pBar, UINT nDockBarID = 0, LPCRECT lpRect = NULL );

Die Möglichkeiten für nDockBarID sind AFX_IDW_DOCKBAR_TOP, AFX_IDW_DOCKBAR_BOTTOM, AFX_IDW_DOCKBAR_LEFT und AFX_IDW_DOCKBAR_RIGHT.

Also probieren wir dies aus:
 
void CMainFrame::OnStbTop() 
{
 DockControlBar( &m_wndToolBar, AFX_IDW_DOCKBAR_TOP ); 
}

void CMainFrame::OnStbRight() 
{
 DockControlBar( &m_wndToolBar, AFX_IDW_DOCKBAR_RIGHT );
}

void CMainFrame::OnStbBottom() 
{
 DockControlBar( &m_wndToolBar, AFX_IDW_DOCKBAR_BOTTOM ); 
}

void CMainFrame::OnStbLeft() 
{
 DockControlBar( &m_wndToolBar, AFX_IDW_DOCKBAR_LEFT );
}

void CMainFrame::OnStbFloat() 
{
 CPoint pt( 100,100 );
 ClientToScreen( &pt );
 FloatControlBar( &m_wndToolBar, pt ); 
}

... und es klappt auf Anhieb!  Nun sind Sie gerüstet, eigene Symbolleisten zu erzeugen, "floaten" oder "docken" zu lassen und vor allem Funktionen anzubinden.

Falls Sie unbedingt Text unter die Symbole fügen wollen, dann gelingt dies mit den Funktionen CToolBar::SetButtonText(...) und CToolBar::SetSizes(...). Das macht die Symbolleisten jedoch raumgreifend. Sie haben doch schließlich die Tooltiptexte und die Texte in der Statusleiste. Daher mein Rat: Verzichten Sie lieber auf diesen Überfluß im "Klicki-Bunti-Browser-Stil", damit mehr Platz für die wesentlichen Dinge bleibt.

Ein Problem besteht oft bei der Anordnung von Symbolleisten nebeneinander. Die Lösung liegt in der Funktion

void CFrameWnd::DockControlBar( CControlBar * pBar, UINT nDockBarID = 0, LPCRECT lpRect = NULL );

und in der Anwendung des dritten Parameters. Durch Angabe eines Rechtecks simuliert man das Ziehen auf diese Fläche mit anschließendem Andocken.

Als erste Anregung finden Sie hier ein Beispiel, in dem unsere eigene Symbolleiste rechts neben die Standardsymbolleiste gesetzt wird:
 
int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
...
...
...
    EnableDocking(CBRS_ALIGN_ANY);

    m_wndToolBar.EnableDocking(CBRS_ALIGN_ANY);
    m_wndToolBar.SetWindowText("Standardsymbolleiste");
    DockControlBar( &m_wndToolBar,  AFX_IDW_DOCKBAR_TOP, CRect(  0,30,100,100) );

    m_wndToolBar1.EnableDocking(CBRS_ALIGN_ANY);
    m_wndToolBar1.SetWindowText("Eigene Symbolleiste Nr.1");
    DockControlBar( &m_wndToolBar1, AFX_IDW_DOCKBAR_TOP, CRect(100,30,100,100) );

    return 0;
}

BOOL CMainFrame::PreCreateWindow(CREATESTRUCT& cs)
{
    cs.x = 0; 
    cs.y = 0;

    if( !CFrameWnd::PreCreateWindow(cs) ) return FALSE;
    return TRUE;
}

Eine allgemeine Vorgehensweise finden Sie in MSDN bzw. in Kurzform hier:
http://www.codeproject.com/docking/toolbar_docking.asp?print=true
 
 

7.2.4 Statusleiste

Als Ausgangspunkt wählen wir eine ganz normale SDI-Anwendung, wie diese vom Assistenten erzeugt wird. Der Name sei "SDI002".
In dieser Anwendung wird automatisch eine Statusleiste erzeugt. Sie kennen auch bereits den Ort der Erzeugung. Die Bausteine müssen wir jedoch an drei Stellen zusammen suchen:
 
class CMainFrame : public CFrameWnd
{
...
CStatusBar  m_wndStatusBar;
// MainFrm.cpp : Implementierung der Klasse CMainFrame
//
...
static UINT indicators[] =
{
 ID_SEPARATOR, 
 ID_INDICATOR_CAPS, // Umschalt-Feststelltaste
 ID_INDICATOR_NUM, // Taste Num (Ziffernblockverriegelung)
 ID_INDICATOR_SCRL, // Taste Rollen (Bildlaufverriegelung)
};
int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
 if ( !m_wndStatusBar.Create(this) || !m_wndStatusBar.SetIndicators( indicators, sizeof(indicators)/sizeof(UINT) ) )
 { TRACE0("Statusleiste konnte nicht erstellt werden\n");  return -1; }

Das Objekt "Statusleiste" ist eine Instanz der Klasse CStatusBar.
In der Klasse CMainFrame wird dieses Objekt als Member-Variable definiert.
In der Datei MainFrm.cpp finden wir das globale statische UINT-Array indicators.
In der Funktion CMainFrame::OnCreate(...) wird die Statusleiste mittels CStatusBar::Create(...) erzeugt,
und die Funktion CStatusBar::SetIndicators(...) ordnet das Array indicators der Statusleiste zu.

Wenn Sie sich die Anwendung genau anschauen, erkennen Sie noch einen Beitrag zur Statusleiste:

Der String "Bereit" steht zu Beginn ganz links in der Statusleiste.

Sie finden diesen Text in der String Table unter der Bezeichnung AFX_IDS_IDLEMESSAGE. Den String "Bereit" kann man dort auf individuelle Bedürfnisse anpassen.

Die Abkürzungen in den Indikator-Bereichen ganz rechts bedeuten:
UF: Umschalt-Feststelltaste
NUM: Ziffernblockverriegleung
RF: Rollen-Feststelltaste (Bildlaufverriegleung)

Für japanische Tastaturen gibt es zusätzlich die Kana-Taste ( ID_INDICATOR_KANA, Anzeige:"KANA" ).
Wir bauen dies zur Verdeutlichung in unsere Anwendung ein:

static UINT indicators[] =
{
 ID_SEPARATOR,
 ID_INDICATOR_CAPS,
 ID_INDICATOR_NUM,
 ID_INDICATOR_SCRL,
 ID_INDICATOR_KANA,
};

... und schon haben wir einen weiteren Indikator-Bereich.

Die Reihenfolge der Indikator-Bereiche stimmt übrigens mit der Reihenfolge der Definition im Array überein.
Für die Ausgabe der Befehlsinformationen ist der Eintrag ID_SEPARATOR zuständig.

Jede Statusleiste besteht aus sogenannten "panes" (engl. Scheibe). Das sind rechteckige Bereiche, in denen man Informationen wie z.B. Texte ausgeben kann. Die Zählung der "panes" beginnt links und startet bei 0. Der Text "Bereit" steht also in pane 0.

Wir wollen nun links von den rechts ausgerichteten Panes mit dem 3D-Look einen eigenen Bereich schaffen, in dem wir unsere eigenen Informationen ausgeben können. Zunächst benötigen wir eine ID. Diese fügen wir bei den Ressourcen als Textstring hinzu. Wir benutzen ID_INDICATOR_MYINFO und ordnen den String "Meine Info:__________" (10 Underscores nach dem Doppelpunkt) zu:

Dann fügen wir unsere ID dem indicator[] array an zweiter Stelle zu.
 
static UINT indicators[] =
{
 ID_SEPARATOR,         // Statusleistenanzeige
 ID_INDICATOR_MYINFO,
 ID_INDICATOR_CAPS,
 ID_INDICATOR_NUM,
 ID_INDICATOR_SCRL,
 ID_INDICATOR_KANA,
};

Wenn Sie nun kompilieren, erhalten Sie sofort diesen neuen rechteckigen Bereich, sozusagen Ihr 3D-Look-Pane:

Wenn Sie nun den Text setzen wollen, müssen Sie folgende Schritte unternehmen. Zunächst wollen wir unseren Text aus Funktionen der View heraus verändern. Als Beispiel nehmen wir den Links- und Rechtsklick mit der Maustaste. Also schaffen wir uns zunächst diese beiden Funktionen in unserer View-Klasse:
 
/////////////////////////////////////////////////////////////////////////////
// CSDI002View Nachrichten-Handler

void CSDI002View::OnLButtonDown(UINT nFlags, CPoint point) 
{
 // TODO: Code für die Behandlungsroutine für Nachrichten hier einfügen und/oder Standard aufrufen

 CView::OnLButtonDown(nFlags, point);
}

void CSDI002View::OnRButtonDown(UINT nFlags, CPoint point) 
{
 // TODO: Code für die Behandlungsroutine für Nachrichten hier einfügen und/oder Standard aufrufen

 CView::OnRButtonDown(nFlags, point);
}

Was schreiben wir in diese Funktionen?

Wir brauchen einen Zeiger auf die private Member-Variable m_wndStatusBar der Klasse CMainFrame. Wie erhalten wir diesen?

Hierfür fügen wir mittels Assistent eine neue get-Funktion namens get_StatusBar() in der Klasse CMainFrame hinzu, die uns einen Zeiger auf diese private Member-Variable verschafft. Die Implementierung liefert einfach den Zeiger auf m_wndStatusBar zurück. Damit erhalten wir dann in unseren beiden neu hinzugefügten Member-Funktionen der View-Klasse (für Links- und Rechtsklick) Zugriff auf folgende Member-Funktion der Klasse CStatusBar:

BOOL CStatusBar::SetPaneText( int nIndex, LPCTSTR lpszNewText, BOOL bUpdate = TRUE )

Der erste Parameter ist der Index des Pane, den wir ansprechen wollen, in unserem Fall 1.
Der zweite Parameter ist der String, und der dritte Parameter veranlaßt das Neuzeichnen.

Das Objekt der Klasse CMainFrame erhalten wir übrigens mittels CFrameWnd* CWnd::GetParentFrame().
 
// MainFrm.h : Schnittstelle der Klasse CMainFrame
...

class CMainFrame : public CFrameWnd
{
...
public:
 CStatusBar* get_StatusBar();
 

// MainFrm.cpp : Implementierung der Klasse CMainFrame
...

/////////////////////////////////////////////////////////////////////////////
// CMainFrame Nachrichten-Handler

CStatusBar* CMainFrame::get_StatusBar()
{
 return &m_wndStatusBar;
}
 

// SDI002View.cpp : Implementierung der Klasse CSDI002View

...
#include "MainFrm.h"
...

void CSDI002View::OnLButtonDown(UINT nFlags, CPoint point) 
{
 CMainFrame* pMainFrame = (CMainFrame*)GetParentFrame();
 CStatusBar* pStB        = pMainFrame->get_StatusBar();
 pStB->SetPaneText(1,"Meine Info: Linke Maus");

 CView::OnLButtonDown(nFlags, point);
}

void CSDI002View::OnRButtonDown(UINT nFlags, CPoint point) 
{
 CMainFrame* pMainFrame = (CMainFrame*)GetParentFrame();
 CStatusBar* pStB        = pMainFrame->get_StatusBar();
 pStB->SetPaneText(1,"Meine Info: Rechte Maus");

 CView::OnRButtonDown(nFlags, point);
}

Hinweis zur OOP:
An diesem Beispiel sehen Sie recht gut, wie man die Klassengrenze zwischen CMainFrame und C...View durch die selbst geschriebene get-Funktion auf saubere Weise überbrücken kann. Wir hätten natürlich auch einfach m_wndStatusBar auf public setzen können. Dies soll man jedoch vermeiden. Member-Variablen (Attribute) sind privat! Dafür gibt es Member-Funktionen (Methoden). Fügen Sie in solchen Fällen auch keine friend-Anweisungen ein, sondern behalten Sie die gewollte Kapselung bei. Ansonsten macht OOP und MFC keinen Sinn.

Wenn Sie diese Hürden überwunden haben, erhalten Sie unseren eigenen Text in der Statuszeile:


 
 

7.3 Die Anwendung wird initialisiert

Die Funktion InitInstance() unserer Anwendungsklasse startet sozusagen unser SDI-Programm.
Dort findet sich eine ganze Sammlung von Vorgängen, die wenig miteinander zu schaffen haben.
Daher wirkt diese Funktion auf den ersten Blick recht verwirrend. Die dort eingesetzten Anweisungen sind ebenfalls wenig selbsterklärend.
Nachfolgend zunächst eine etwas bereinigte Form des Programms:
 
BOOL CSDI001App::InitInstance()
{
 AfxEnableControlContainer();

 #ifdef _AFXDLL Enable3dControls();       // MFC in DLLs
 #else          Enable3dControlsStatic(); // statische MFC-Anbindung
 #endif

 SetRegistryKey(_T("Local AppWizard-Generated Applications"));

 LoadStdProfileSettings(); 

 CSingleDocTemplate* pDocTemplate;
 pDocTemplate = new CSingleDocTemplate
 (
  IDR_MAINFRAME, 
  RUNTIME_CLASS(CSDI001Doc),
  RUNTIME_CLASS(CMainFrame), 
  RUNTIME_CLASS(CSDI001View)
 );
 AddDocTemplate(pDocTemplate);

 CCommandLineInfo cmdInfo;
 ParseCommandLine(cmdInfo);
 if (!ProcessShellCommand(cmdInfo)) return FALSE;

 m_pMainWnd->ShowWindow(SW_SHOW);
 m_pMainWnd->UpdateWindow();

 return TRUE;
}

AfxEnableControlContainer():
Schaltet die Unterstützung für ActiveX-Steuerelemente ein. Ein ActiveX-Steuerelement-Container unterstützt ActiveX-Steuerelemente und kann diese in seine eigenen Fenster und Dialogfelder integrieren.

Sie haben diese Anweisung in Schritt 3 des Assistenten eingefügt:


 

Enable3dControls()  bzw.
Enable3dControlsStatic():
Hierdurch wird das dreidimensionale Aussehen von Steuerlementen unterstützt. Man spricht von "3D Window Controls". Die Datei CTL3D32.DLL wird mit unserer Anwendung geladen. Das statische Linken der MFC ist in der Standard-Version von MS VC++ noch nicht möglich. Entschieden haben Sie sich für diese Anweisung in Schritt 4 des Assistenten:


 

SetRegistryKey(...):
Vielleicht ist es Ihnen noch garnicht aufgefallen, dass unsere SDI-Anwendung sich in der Registry verewigt.
Unter HKEY_CURRENT_USER\Software\ finden Sie den Eintrag "Local AppWizard-Generated Applications". Als Unterschlüssel finden sich dort verschiedene MFC-Programme, auch unser SDI001. Wichtig ist dort wiederum der Unterschlüssel Recent File List. Dort werden die zuletzt verwendeten Dateien unserer Anwendung abgelegt. Üblich sind vier Files.

Der allgemeine Aufbau ist:
HKEY_CURRENT_USER\Software\ <company> \ <application> \ <section> \ <value>

Ersetzen Sie "Local AppWizard ..." durch den Namen Ihrer "company", und schon haben Sie der Registry Ihren persönlichen Stempel aufgedrückt.

Hier sehen Sie links den Menüeintrag einer zuletzt benutzten Datei. Rechts sehen Sie die Speicherung dieser Information in der Registry.
Die Bezeichnung des Menüeintrages ist übrigens ID_FILE_MRU_FILE1. Hierbei bedeutet MRU Most Recently Used.
 

LoadStdProfileSettings(UINT nMaxMRU = _AFX_MRU_COUNT ):
Diese Funktion lädt die gespeicherten Namen der zuletzt benutzten Files.
In der Datei afxwin.h findet man folgende Festlegung für den Parameter:

#define _AFX_MRU_COUNT 4// default support for 4 entries in file MRU

Nun wissen Sie auch, warum standardisiert maximal vier MRU Files abgelegt werden. Wenn Sie mehr speichern wollen, geben Sie selbst den entsprechenden Parameter nMaxMRU an. Wenn Sie LoadStdProfileSettings(0) vorgeben, werden keine MRU Files gespeichert.
 

Den nächsten Anweisungsblock muß man als Ganzheit betrachten:

// Dokumentvorlagen registrieren
CSingleDocTemplate* pDocTemplate;
pDocTemplate = new CSingleDocTemplate
(
  IDR_MAINFRAME,
  RUNTIME_CLASS( CSDI001Doc  ),
  RUNTIME_CLASS( CMainFrame  ),   // Haupt-SDI-Rahmenfenster
  RUNTIME_CLASS( CSDI001View )
);
AddDocTemplate(pDocTemplate);

Man erzeugt hier mittels new auf dem Heap ein Objekt der MFC-Klasse CSingleDocTemplate. Die Adresse dieses Objektes übergeben wir an die Funktion AddDocTemplate(...). Diese fügt den Zeiger in ein Zeiger-Array ein. Eine Anwendung kann also mehrere Zeiger auf solche CDocTemplate besitzen.

Jetzt hat unsere Anwendung eine Aufzeichnung, in der die Adressen von Rahmenfenster, Dokument und Ansicht stehen. Das ist sozusagen die Auskunft, wenn Beziehungen zwischen den Einzelteilen gesucht werden. Stellen Sie sich das vor wie eine Familie: Vater, Mutter, Kinder. Der Familienname ist der Zeiger auf die Familie. Wenn ein Kind fragt: "Wer/Wo ist meine Mutter?", dann wird das Kind nach dem Familiennamen gefragt. Hat es diesen parat, dann erhält es eine Antwort.

Der "Familienname" hier ist pDocTemplate. Machen wir uns das etwas klarer. Typische Fragen nach anderen "Familienmitgliedern" sind z.B.:
 

CDocTemplate* CDocument::GetDocTemplate( ) const;
Dokument fragt: 
Wer ist mein CDocTemplate? (sozusagen Familienname)

POSITION      CDocument::GetFirstViewPosition( )  const;
CView*        CDocument::GetNextView( POSITION& ) const;
Dokument fragt: 
Wo ist meine erste Ansicht? 
Wer ist meine nächste Ansicht?

CDocument*        CView::GetDocument( ) const;
Ansicht fragt: 
Wer ist mein Dokument?

CFrameWnd*         CWnd::GetParentFrame( ) const;
Ansicht (oder Kind) fragt: 
Wer ist mein Rahmenfenster?

CView*        CFrameWnd::GetActiveView( ) const;
Rahmenfenster fragt: 
Wer ist meine aktuelle Ansicht?

CDocument*    CFrameWnd::GetActiveDocument( );
Rahmenfenster fragt: 
Wer ist das Dokument meiner aktuellen Ansicht?

CWinApp* AfxGetApp( ); 
LPCTSTR  AfxGetAppName( ); 
Alle fragen: 
Wer ist unsere Anwendung?
Wie heißt unsere Anwendung?

Sie sehen: Es existieren zahlreiche Get-Funktionen, damit man sich überhaupt gegenseitig findet. Eine tolle "Familie".

Eine weitere Auskunft erteilt uns CSingleDocTemplate auch noch, nämlich über die Ressourcen unserer "Familie", sozusagen der Hausrat.
Sie wissen: AcceIerator-Tabellen, Icons, Menüs, Symbolleisten, etc.

Der Name für den "Hausrat" (sprich: wichtige Ressourcen) ist IDR_MAINFRAME. Der Name signalisiert: Der Hausrat gehört zum Rahmenfenster. Sie sehen, wie oft diese Bezeichnung in unserer Ressourcenübersicht auftaucht. Auch der erste Eintrag in der Zeichenfolgentabelle (String Table) ist IDR_MAINFRAME:


 

Auch der nächste Anweisungsblock sollte als Ganzheit betrachtet werden:

CCommandLineInfo  cmdInfo;
ParseCommandLine( cmdInfo );
if ( !ProcessShellCommand( cmdInfo ) ) return FALSE;

Zunächst wird ein Objekt der MFC-Klasse CCommandLineInfo generiert. Diese Klasse ermöglicht die Auswertung der Parameter, die in der Kommandozeile nach dem Programmnamen übergeben werden.

ParseCommandLine(...) ruft für jeden übergebenen Parameter die Funktion

void CCommandLineInfo::ParseParam( LPCTSTR lpszParam, BOOL bFlag, BOOL bLast )

auf.

Die Funktion

BOOL CWinApp::ProcessShellCommand( CCommandLineInfo& rCmdInfo )

kann anschließend in Abhängigkeit der erkannten Parameter in der Kommandozeile folgende Aktionen ausführen:
 
Parameter in Kommandozeile Resultierende Aktion
app Neues Dokument 
app filename Öffnet Datei als Dokument
app /p filename Druckt Datei auf Standard-Drucker 
app /pt filename printer driver port Druckt Datei auf angegebenen Drucker 
app /Automation statet als Automation-Server 
app /dde wartet auf  DDE-Befehle
app /Embedding  bearbeitet ein eingebettetes OLE-Objekt 

Wenn Sie z.B. "SDI001.exe SchoenesBild.bmp" unter Start - Ausführen angeben,

entspricht dies app filename und unsere Anwendung startet mit diesem Bild. Wirklich? Natürlich nicht, denn wir haben keinerlei Funktionalität für die Bearbeitung von bmp-Dateien in unsere Anwendung implementiert. Aber prinzipiell könnte das so ablaufen.
 

Die beiden letzen Anweisungen

m_pMainWnd->ShowWindow( SW_SHOW );
m_pMainWnd->UpdateWindow();

sind sicher schon bekannt. ShowWindow(...) zeigt das Rahmenfenster an, und UpdateWindow() erzwingt durch die Erzeugung der Nachricht WM_PAINT an der Nachrichtenschlange vorbei ein sofortiges Neuzeichnen des Client-Bereiches.

Bezüglich der möglichen Parameter bei der Fensteranzeige schauen Sie bitte bei der Funktion

BOOL CWnd::ShowWindow( int nCmdShow )

nach.
 

Den Abschluß bildet die Rückgabe des Wertes TRUE. Dies signalisiert die korrekte Initialisierung.
 
 
 

Zurueck zum Inhaltsverzeichnis

zum nächsten Kapitel