Dr. Erhard Henkes (e.henkes@gmx.net) - C++ und MFC - Stand: 06.08.2002

Zurueck 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 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 Schreiben und Lesen der Daten in Dateien sorgt: Dies ist 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.
 

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 verstehen, zwischen MFC-Rahmenprogramm, das der Assistent erstellt, und Ihrem eigenen Programmcode klar zu trennen.

Wir starten wie gewohnt mit "Datei / Neu/ Projekte / MFC-Anwendungs-Assistent(exe)".
Wählen Sie als Projektname "Test_", damit Sie die Namensanhänge, die der Assistent den zu erstellenden Klassen verpasst,
möglichst gut erkennen können.

In Schritt 1 entscheiden Sie für "Einzelnes Dokument (SDI)" und lassen das Häkchen bei
"Unterstützung der Dokument-/Ansichts-Architektur" gesetzt.

Schritt 2 und 3 akzeptieren Sie ohne Änderung.

In Schritt 4 wählen Sie alles außer 3D-Steuerelemente ab.

Abb. 7.2: Schritt 4 bei der Erstellung des Doc-/View-Beispiels

Ändern Sie unter "Weitere Optionen" die Eingaben wie folgt ab:

Abb. 7.3: Unterfenster bei Schritt 4 "Weitere Optionen"

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.

Abb. 7.4: Die vom Assistent erstellten Klassen
 

In Schritt 6 zeigt der Assistent dann die zu erstellenden Klassen:

Sie erkennen die vier Grundpfeiler, auf denen die Anwendung steht:

Ansicht (View),
Daten (Doc),
Fenster (MainFrame) und
Anwendung (App).

Es ist wichtig, dass Sie sich diese Unterteilung gut einprägen, damit Sie beim Programmieren die Übersicht behalten.

Abb. 7.5: Die kompilierte und ausgeführte Anwendung

Wenn Sie das Ganze nun kompilieren und ausführen, erhalten Sie obiges Fenster.
Schauen Sie sich die Menüs genauer an. Unter "Datei" finden Sie die wesentlichen Möglichkeiten in Bezug auf unsere sav-Dateien:

Neu - Beenden -
Öffnen einer vorhandenen Datei - Speichern/Überschreiben einer Datei

Der Menüpunkt "Bearbeiten" ist z.Z. nicht interessant.
Unter "?" finden Sie die bekannte About-Dialogbox.

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

Früher 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 mit vier diagonalen Linien.
Damit erhält man schnell und einfach 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 auch in unseren sav-Dateien landen.

Quadrate und 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
...
class CTest_Doc : public CDocument
{
...
// Überladungen
// Vom Klassenassistenten generierte Überladungen virtueller Funktionen
//{{AFX_VIRTUAL(CTest_Doc)
public:
virtual BOOL OnNewDocument();
virtual void Serialize(CArchive& ar);
//}}AFX_VIRTUAL

// Implementierung
public:
CPoint ZweitesQuadrat;
CPoint ErstesQuadrat;
int Laenge;
virtual ~CTest_Doc();
...
};

Wir werden diese Member-Variablen der Dokumentenklasse nun an zwei Stellen initialisieren:

Im Konstruktor der Dokumentenklasse,
In der Member-Funktion "OnNewDocument" der Dokumentenklasse:
 
 
///////////////
// CTest_Doc Konstruktion/Destruktion

CTest_Doc::CTest_Doc()
{
  // ... Hier Code für One-Time-Konstruktion einfügen

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

     Diese Zahlen sind unsere konkreten Daten

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, um die Anzeige der Daten zu realisieren. Die grafische Anzeige erfolgt in der Member-Funktion OnDraw(), die uns bisher folgendes bietet:
 
void CTest_View::OnDraw(CDC* pDC)
{
  CTest_Doc* pDoc = GetDocument();
  ASSERT_VALID(pDoc);
}

Wichtig sind die beiden Zeiger pDC 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 zeichen 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 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.

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

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())
    {
        // ZU ERLEDIGEN: Hier Code zum Speichern einfügen
  }
    else
    {
        // ZU ERLEDIGEN: 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 Daten speichern, sagen wir unter dem Dateinamen "Wuerfel1.sav".

Abb. 7.7: Das Ergebnis der Serialisierung, die Datei "Wuerfel.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 auch zusammengesetzte
Daten wie CPoint einfach hin- und herschieben kann (dies erinnert Sie sicher an die C++-Streams cin und cout).

Zum Abschluß 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 + W drücken; der Klassen-Assistent erscheint
2) Klassenname auf CTest_View abändern
3) Unter Nachrichten 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(...):
 
///////////////
// CTest_View Nachrichten-Handler

void CTest_View::OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags)
{
    // TODO: Code für die Behandlungsroutine für Nachrichten hier einfügen und/oder Standard aufrufen
    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)
{
  // TODO: Code für die Behandlungsroutine für Nachrichten hier einfügen und/oder Standard aufrufen

  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 den Zeiger auf die Dokumentklasse besorgen 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 mußten wir ihm einfach nacheifern.

Nachdem wir Daten der Dokumentklasse verändern, teilen wir dies auch dieser Klasse mit.
Dies erfolgt mittels pDoc->SetModifiedFlag();
Die Aktualisierung der Ansichtsklasse erfolgt mittels Invalidate();

Das Setzen des "ModifiedFlags" führt dazu, dass die Anwendung uns bei Veränderungen (hier mit den Pfeiltasten)
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.
 
 

7.2 Single Document Interface (SDI)

Nachdem Sie nun das Doc/View-Modell mit der Trennung von Daten und Ansicht verstanden haben, wenden wir uns nun verstärkt dem Rahmenfenster, den Ansichten und der Anwendung zu. Beginnen wir mit dem sogenannten Single Document Interface (SDI) (Gegensatz: Multiple Document Interface, MDI).
 

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 Schritt 1. Dort wählen Sie nicht MDI, sondern Einzelnes Dokument (SDI) aus. Genau genommen ist es merkwürdig, dass MDI voreingestellt ist, denn MicroSoft 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.

Beachten Sie den Schritt 4 des Assistenten:

Dort sind verschiedene Zutaten bereits eingestellt: Symbolleiste, Statusleiste, Drucken plus Druckvorschau, 3D-Steuerelemente. Lassen Sie diese Punkte ausgewählt, weil wir uns damit noch beschäftigen wollen. Interessant ist die Schaltfläche "Weitere Optionen". Wenn Sie dieses Feld anwählen, können Sie eine Vorauswahl für die Fensterstile (Window Styles) treffen und sogar ein geteiltes Fenster verwenden:

Wenn Sie diese Vorgaben in diesem oft übersprungenen Nebenzweig des Schrittes 4 akzeptieren, sieht ihre Funktion CMainFrame::PreCreateWindow(...) vereinfacht wie folgt aus:
 
BOOL CMainFrame::PreCreateWindow(CREATESTRUCT& cs)
{
 if( !CFrameWnd::PreCreateWindow(cs) ) return FALSE;
 return TRUE;
}

Hier wird also nur die Funktion der Oberklasse CFrameWnd::PreCreateWindow(...) ausgeführt.

Wenn Sie die Minimieren- und Maximieren-Schaltfläche mittels Assistent abwählen würden, dann sähe das Ergebnis wie folgt aus:
 
BOOL CMainFrame::PreCreateWindow(CREATESTRUCT& cs)
{
 if( !CFrameWnd::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 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:


 

FWS_ADDTOTITLE:


 

FWS_ADDTOTITLE | FWS_PREFIXTITLE:


 

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 bei der Funktion OnNewDocument():
 
BOOL CSDI001Doc::OnNewDocument()
{
 if (!CDocument::OnNewDocument()) return FALSE;

 // ZU ERLEDIGEN: Hier Code zur Reinitialisierung einfügen
 // (SDI-Dokumente verwenden dieses Dokument)

 SetTitle("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.
 

Was steht eigentlich in CFrameWnd::PreCreateWindow(...)? MFC erlaubt den Blick in die cpp's. Man muß nur wissen, wo man was findet. Hinweise findet man z.B. im Anhang des Buches "MFC Internals" von George Shepherd und Scot Wingo. Also öffnen wir die Motorhaube:

"MFC under the hood" - Die Implementation von CFrameWnd::PreCreateWindow(...) in winfrm.cpp:
 
BOOL CFrameWnd::PreCreateWindow(CREATESTRUCT& cs)
{
 if (cs.lpszClass == NULL)
 {
  VERIFY(AfxDeferRegisterClass(AFX_WNDFRAMEORVIEW_REG));
  cs.lpszClass = _afxWndFrameOrView;  // COLOR_WINDOW background
 }

 if ((cs.style & FWS_ADDTOTITLE) && afxData.bWin4) cs.style |= FWS_PREFIXTITLE;

 if (afxData.bWin4) cs.dwExStyle |= WS_EX_CLIENTEDGE;

 return TRUE;
}

afxData.bWin4: Windows 95 (oder höher)
 

Nach diesem Kurzausflug zu CMainFrame::PreCreateWindow(...), die vor dem Erstellen des Rahmenfensters ausgeführt wird, belassen wir es bei diesem Standard, den uns die Basisklasse CFrameWnd serviert.Nach dem Erstellen und Ausführen der EXE-Datei sehen Sie folgende Anwendung auf ihrem Bildschirm (hier ist das Fenster verkleinert):

Der Assistent hat seine Arbeit geleistet. Sie haben aber vielleicht mehr oder etwas anderes als Sie wirklich brauchen oder wollen.
Schauen wir uns die optische Erscheinung zunächst 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)
 

7.2.2 Elemente des Rahmenfensters - Grundlagen

Nun suchen wir in unserer Anwendung nach diesen Elementen:

Beginnen wir mit der Klassendefinition unserer von CFrameWnd abgeleiteten Klasse CMainFrame.
Dort entdecken wir unsere Statusleiste und Symbolleiste in folgenden Member-Variablen:

CStatusBar  m_wndStatusBar;
CToolBar    m_wndToolBar;

Darüber hinaus gibt es dort auch folgende Struktur für unser Rahmenfenster:

CREATESTRUCT cs.
 
class CMainFrame : public CFrameWnd
{
protected: // Nur aus Serialisierung erzeugen
 CMainFrame();
 DECLARE_DYNCREATE(CMainFrame)

// Attribute
public:

// Operationen
public:

// Überladungen
 // Vom Klassenassistenten generierte Überladungen virtueller Funktionen
 //{{AFX_VIRTUAL(CMainFrame)
 virtual BOOL PreCreateWindow(CREATESTRUCT&cs);
 //}}AFX_VIRTUAL

// Implementierung
public:
 virtual ~CMainFrame();

#ifdef _DEBUG
 virtual void AssertValid() const;
 virtual void Dump(CDumpContext& dc) const;
#endif

protected:  // Eingebundene Elemente der Steuerleiste
 CStatusBar  m_wndStatusBar;
 CToolBar    m_wndToolBar;

// Generierte Message-Map-Funktionen
protected:
 //{{AFX_MSG(CMainFrame)
 afx_msg int OnCreate(LPCREATESTRUCT lpCreateStruct);
 //}}AFX_MSG
 DECLARE_MESSAGE_MAP()
};

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

public:
  virtual BOOL PreCreateWindow(CREATESTRUCT& cs);

public:
  virtual ~CMainFrame();

protected: 
  CStatusBar  m_wndStatusBar;
  CToolBar    m_wndToolBar;

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

Das sieht doch gleich viel übersichtlicher aus. 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 wieder. Das sind C++-Standardelemente einer Klasse. Was gibt es Spezifisches?

Da finden wir zwei 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;

  // ZU ERLEDIGEN: Ändern Sie hier die Fensterklasse oder das Erscheinungsbild, indem Sie
  // CREATESTRUCT cs modifizieren.

 return TRUE;
}

Wir gehen davon aus, das es klappt. Interessanter ist die Strukur cs vom Typ CREATESTRUCT. Was steht denn da alles drinnen?
Also 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 (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("Symbolleiste konnte nicht erstellt werden\n");
  return -1;      // Fehler bei Erstellung
 }

 if (!m_wndStatusBar.Create(this) ||
  !m_wndStatusBar.SetIndicators(indicators,
    sizeof(indicators)/sizeof(UINT)))
 {
  TRACE0("Statusleiste konnte nicht erstellt werden\n");
  return -1;      // Fehler bei Erstellung
 }

 // ZU ERLEDIGEN: Löschen Sie diese drei Zeilen, wenn Sie nicht wollen, dass die Symbolleiste
 //  andockbar ist.
 m_wndToolBar.EnableDocking(CBRS_ALIGN_ANY);
 EnableDocking(CBRS_ALIGN_ANY);
 DockControlBar(&m_wndToolBar);

 return 0;
}

Das ist ja ein richtiger Brocken. Sie sehen der Assistent arbeitet fleißig für uns. Zunächst bereinigen und vereinfachen wir wieder, damit wir einen klaren Blick haben:
 
int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
 if ( CFrameWnd::OnCreate(lpCreateStruct) == -1 ) return -1;

 if ( !m_wndToolBar.CreateEx( ...    ||  !m_wndToolBar.LoadToolBar(IDR_MAINFRAME) ) return -1; // Fehler Toolbar

 if ( !m_wndStatusBar.Create( (this) ||  !m_wndStatusBar.SetIndicators(...)       ) return -1; // Fehler Statusbar

 // ZU ERLEDIGEN: Löschen Sie diese drei Zeilen, wenn Sie nicht wollen, dass die Symbolleiste andockbar ist.
 m_wndToolBar.EnableDocking(CBRS_ALIGN_ANY);
 EnableDocking(CBRS_ALIGN_ANY);
 DockControlBar(&m_wndToolBar);

 return 0;
}

So sieht das doch gleich viel klarer aus. Nun erkennt man die Erstellung der Symbolleiste, der Statusleiste und den Anweisungsblock, der sich mit der "Andockbarkeit" der Symbolleiste  beschäftigt. Hier ist also der richtige Platz, um Veränderungen an diesen beiden Elementen vorzunehmen.

Beginnen wir sofort mit einfachen Experimenten:

1) Wir wollen keine andockbare Symbolleiste:
Das ist leicht, da müssen wir einfach die drei Zeilen, wie angegeben, durch einen Kommentar (im C-Stil) streichen. Kommentieren Sie auf jeden Fall aus, also nicht wirklich wegstreichen, es sei denn Sie kennen die drei Zeilen auswendig.
 
int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
 ...

 //  ZU ERLEDIGEN: Löschen Sie diese drei Zeilen, wenn Sie nicht wollen, dass die Symbolleiste
 //  andockbar ist.
 /*
 m_wndToolBar.EnableDocking(CBRS_ALIGN_ANY);
 EnableDocking(CBRS_ALIGN_ANY);
 DockControlBar(&m_wndToolBar);
 */

 return 0;
}

... und schon ist es vorbei mit der vagabundierenden Symbolleiste. Jetzt hängt sie fest.

2) Wir wollen gar keine Symbolleiste:
Wieder einfach. Dann wird eben auch die Erzeugung gestrichen.
 
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("Symbolleiste konnte nicht erstellt werden\n");
   return -1;      // Fehler bei Erstellung
 }
 */

 if (!m_wndStatusBar.Create(this) || !m_wndStatusBar.SetIndicators(indicators, sizeof(indicators)/sizeof(UINT)))
 {
   TRACE0("Statusleiste konnte nicht erstellt werden\n");
   return -1;      // Fehler bei Erstellung
 }

 // ZU ERLEDIGEN: Löschen Sie diese drei Zeilen, wenn Sie nicht wollen, dass die Symbolleiste
 //  andockbar ist.
 /*
 m_wndToolBar.EnableDocking(CBRS_ALIGN_ANY);
 EnableDocking(CBRS_ALIGN_ANY);
 DockControlBar(&m_wndToolBar);
 */

 return 0;
}

... und weg ist dieser "Schnickschnack".


 

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) || !m_wndStatusBar.SetIndicators(indicators, sizeof(indicators)/sizeof(UINT)))
 {
  TRACE0("Statusleiste konnte nicht erstellt werden\n");
  return -1;      // Fehler bei Erstellung
 }
*/

... da ist doch gleich viel mehr Platz für unsere "Views".

4) Jetzt wollen wir plötzlich nichts mehr, keine Symbolleiste, keine Statusleiste und vor allem kein Menü:

Die ersten beiden Wünsche sind mit dem bisherigen Wissen leicht zu erfüllen, das brauchen wir hier gar nicht mehr zu zeigen. Aber wie geht das mit dem Menü? Wo war das doch gleich? Das handle auf das Menü war cs.hMenu. Also weg damit! Die richtige Funktion ist
CMainFrame::PreCreateWindow(...):
 
BOOL CMainFrame::PreCreateWindow(CREATESTRUCT& cs)
{
 if ( cs.hMenu != NULL )
 {
  ::DestroyMenu( cs.hMenu ); // Geladenes Menü entfernen
  cs.hMenu = NULL;           // Rahmenfenster hat kein Menü
 }

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

... und das klappt ja prächtig.

Nun sieht das richtig primitiv aus! Ist natürlich kein Wunder, wenn wir alles Zubehör außer Kraft setzen.

Wollen Sie das Menü wieder haben, müssen Sie nur den Zwischenblock wieder per Kommentar deaktivieren.
 
BOOL CMainFrame::PreCreateWindow(CREATESTRUCT& cs)
{
 /*
 if ( cs.hMenu != NULL )
 {
  ::DestroyMenu( cs.hMenu ); // Bereits geladenes Menü entfernen
  cs.hMenu = NULL;           // Hauptfenster hat kein Menü
 }
 */

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

5) 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;
 return TRUE;
}

... und schon erscheint das Fenster in der gewünschten Größe. Tool- und Statusleiste sind auch wieder aktiviert. Aber das können Sie ja nach Belieben ändern. Wie Sie sehen, ist unser Fenster für den rechten Teil der Statusleiste mit cs.cx = 300 sogar zu kurz. Aber man kann es durch Ziehen mit der Maus auf die notwendige Größe verändern.

Von den fünf Bestandteilen

1. Systemmenü
2. Menü
3. Symbolleiste
4. Client-Bereich
5. Statusleiste

haben wir bereits 2, 3 und 5 aufgespürt.

Gehen wir zu Punkt 1: Wie findet man das Systemmenü (heute auch Fenstermenü genannt)?
Wir möchten z.B. das Schließen per Mausklick auf "X" unterbinden. Die Benutzer sollen mit unserer Anwendung arbeiten und diese nicht einfach "weg klicken". Wie erreichen wir dies? Hierzu muß man dieses "X" bzw. das "Schließen" im Systemmenü desaktivieren. Der richtige Ort für diese Missetat ist die Funktion CMainFrame::OnCreate(...). Dort erstellt man immerhin auch andere Verzierungen unseres Rahmenfensters wie Symbol- und Statusleiste:
 
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("Symbolleiste konnte nicht erstellt werden\n");
  return -1; // Fehler bei Erstellung
 }

 if (!m_wndStatusBar.Create(this) || !m_wndStatusBar.SetIndicators(indicators,
    sizeof(indicators)/sizeof(UINT)))
 {
  TRACE0("Statusleiste konnte nicht erstellt werden\n");
  return -1; // Fehler bei Erstellung
 }

 // ZU ERLEDIGEN: Löschen Sie diese drei Zeilen, wenn Sie nicht wollen, dass die Symbolleiste andockbar ist.
 m_wndToolBar.EnableDocking(CBRS_ALIGN_ANY);
 EnableDocking(CBRS_ALIGN_ANY);
 DockControlBar(&m_wndToolBar);

 CMenu* pSystemMenu = GetSystemMenu(FALSE);
 pSystemMenu->EnableMenuItem( SC_CLOSE, MF_GRAYED );

 return 0;
}

Na, wer sagt's denn, geht doch. Jetzt ist das "X" nicht mehr so einladend, und nicht jeder kennt "Alt+F4", oder doch?

Diese Anweisung schauen wir uns genauer an:

 CMenu* pSystemMenu = GetSystemMenu(FALSE);
 pSystemMenu->EnableMenuItem( SC_CLOSE, MF_GRAYED);

In der ersten Zeile beschaffen wir uns einen Zeiger auf das Systemenü-Objekt unseres Rahmenfensters mittels

CMenu* CWnd::GetSystemMenu ( BOOL bRevert ) const

Wir müssen den Parameter auf FALSE setzen, damit wir einen Zeiger auf eine manipulierbare Kopie von CMenu erhalten.
Diesen Zeiger nennen wir in unserem Fall pSystemMenu. Damit können wir verschiedene Member-Funktionen der Klasse CMenu auf unser Objekt anwenden. Im aktuellen Beispiel haben wir

UINT CMenu::EnableMenuItem( UINT nIDEnableItem, UINT nEnable )

verwendet.

Der erste Parameter gibt die ID (default) oder Position des Menüpunktes an, und der zweite Parameter den erwünschten Zustand. Nun benötigen wir nur noch Werte für die entsprechenden Konstanten zu unserem Verständnis:

UINT nIDEnableItem:
einige Beispiele (SC steht für system command):

SC_CLOSE:      "X"
SC_MAXIMIZE:   Maximiert das Fenster
SC_MINIMIZE.   Minimiert das Fenster

UINT nEnable
wichtige Parameter:

MF_BYCOMMAND:  zeigt an, dass der erste Parameter durch sein ID angezeigt wird (default)
MF_BYPOSITION: zeigt an, dass der erste Parameter durch seine Position angezeigt wird
MF_DISABLED:   Desaktiviert den Menüpunkt (Farbe des Strings: normal)
MF_ENABLED:    Aktiviert den Menüpunkt
MF_GRAYED:     Desaktiviert den Menüpunkt (Farbe des Strings: hellgrau)

Der Menüpunkt "Schließen" existiert nicht nur als einladendes "X", sondern zusätzlich etwas versteckt als String auf Platz 6 auch im Menü (ganz links), das durch Anklicken des MFC-Icons erreicht wird.

Wenn Sie MF_DISABLED anstelle MF_GRAYED verwenden, wird zwar das "X" heller dargestellt,  aber "Schließen" immer noch als normaler String angezeigt. Nur, wenn Sie MF_GRAYED wählen, sind "X" und "Schließen" in hellgrauer Frabe dargestellt und desaktiviert.

Wollen Sie im ersten Parameter die Positionsnummer anstelle der ID angeben, dann wählen Sie die 6 als ersten Parameter in Verbindung mit MF_BYPOSITION als zweiten Parameter:

pSystemMenu->EnableMenuItem( 6, MF_BYPOSITION | MF_GRAYED);
 

Die Minimize- und Maximize-Box können Sie über die Windows-Styles entfernen. Aber das kennen Sie ja schon von oben:
 
BOOL CMainFrame::PreCreateWindow(CREATESTRUCT& cs)
{
 /* 
 if (cs.hMenu!=NULL)
 {
  ::DestroyMenu(cs.hMenu); // Bereits geladenes Menü entfernen
  cs.hMenu = NULL;         // Hauptfenster hat kein Menü
 }
 */

 cs.cx = 300;
 cs.cy = 200;

 cs.style &= ~WS_MINIMIZEBOX;
 cs.style &= ~WS_MAXIMIZEBOX;

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

Damit sind dann auch die Menüeintrage "Minimieren" und "Maximieren" auf hellgrau und inaktiv gesetzt.
Sie sehen, dass man mit dieser Funktion CMainFrame::PrecreateWindow(...) umfangreiche Einstellungsmöglichkeiten hat.

Nachfolgend noch ein kleines Beispiel für eine SDI-Anwendung, die ein Rahmenfenster ohne Minimieren-/Maximieren-Schaltflächen und ohne einen in der Größe veränderbaren Rahmen erzeugt. Das Fenster wird auf n % des Bildschirms eingestellt und zentriert:
 
BOOL CMainFrame::PreCreateWindow(CREATESTRUCT& cs)
{
    cs.style = WS_OVERLAPPED | WS_BORDER | WS_SYSMENU;

    const double n = 33;     // n: Prozent der Fenstergröße
    double factor = 100/n;
    cs.cy = ::GetSystemMetrics( SM_CYSCREEN ) / factor;
    cs.cx = ::GetSystemMetrics( SM_CXSCREEN ) / factor;
    cs.y = ( ( cs.cy * factor ) - cs.cy ) / 2;
    cs.x = ( ( cs.cx * factor ) - cs.cx ) / 2;

    return CFrameWnd::PreCreateWindow(cs);
}

Sie haben nun einen 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. Zierrat wie Ressourcen, Menüs, Symbolleisten und Statusleiste sind keine Eckpfeiler. Sie hängen vielmehr wie "liebliche Erker" an unserem massiven Rohbau, und mit fortschreitender  Entwicklung des Betriebssystems MS Windows muß der GUI-Programmierer sich wahrscheinlich verstärkt mit diesen Feinheiten beschäftigen. Beginnen wir mit Symbol- und Statusleisten.
 

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