Du bist nicht angemeldet.

Stilllegung des Forums
Das Forum wurde am 05.06.2023 nach über 20 Jahren stillgelegt (weitere Informationen und ein kleiner Rückblick).
Registrierungen, Anmeldungen und Postings sind nicht mehr möglich. Öffentliche Inhalte sind weiterhin zugänglich.
Das Team von spieleprogrammierer.de bedankt sich bei der Community für die vielen schönen Jahre.
Wenn du eine deutschsprachige Spieleentwickler-Community suchst, schau doch mal im Discord und auf ZFX vorbei!

Werbeanzeige

BlazeX

Alter Hase

  • »BlazeX« ist der Autor dieses Themas

Beiträge: 478

Wohnort: DD

Beruf: Maschinenbau-Student

  • Private Nachricht senden

1

21.03.2010, 21:07

Direct3D11 Settings Dialog - Teil 2 - Tabbed Dialog

Direct3D11 Settings Dialog - Teil 2 - Tabbed Dialog

Teil 1: Link
Teil 2: Den liest du gerade ;)
Teil 3: Link

Inhalt

1. Einleitung
2. Tabbed Dialog - Die Theorie
3. Der Dialog - Resource
4. Die Programmierung des Dialogs
5. Eine Basisklasse für Pages
6. Dialog-Resourcen für Pages
7. Integration der Pages in den SettingsDialog
8. Demoprojekt
9. Ausblick


1. Einleitung

Hi Leute!
Willkommen zum 2. Teil des Tutorials.
Und nochmal, so soll er mal aussehen:

(Link)


In diesem Teil geht es um den Dialog an sich. Ich erkläre, wie man mit Tabs arbeitet und das ganze soweit abstrahiert, dass man sich nur noch einzelne Klassen für die Pages erstellen muss.
Dazu sei noch gesagt, dass ich nicht auf die Implementierung von Laden und Speichern von Einstellungen eingehe. An entsprechenden Stellen weise ich aber darauf hin, was getan werden sollte.

2. Tabbed Dialog - Die Theorie

Ein "Tabbed Dialog" ist eine "DialogBox" (kennt ihr sicher), die aus mehreren Seiten (sogenannte Pages) besteht. Zwischen diesen Pages kann man umschalten.
Die eigentlichen Tabs sind die Viereckigen "Registerkarten" oben. Diese werden als "Tab Control" zusammengefasst und haben dazu noch unterhalb einen Bereich, der für die Pages genutzt wird.
Hier eine kleines Bildchen dazu:

(Link)


Um so etwas zu implementieren, gibt es 2 Möglichkeiten:
1.) Die "Property Sheets" benutzen
2.) Selbst implementieren

Ich habe mich für letzteres entschieden.

3. Der Dialog - Resource
Fangen wir an mit dem Design des Dialogs.

Was soll er alles machen?
- Er soll mit einer einzigen Funktion (wird später DoSettingsDialog heißen) aufgerufen werden.
- Er soll die drei Standard-Buttons (OK, Cancel, Apply) enthalten
-- Bei "Cancel" wird abgebrochen
-- Bei "Apply" wird gespeichert, es wird nicht beendet
-- Bei OK wird erst gespeichert und dann beendet
- Er soll Tabs enthalten, mit denen zwischen den Pages gewechselt werden kann.
- Das Speichern soll der Dialog übernehmen, sodass das Programm nur noch die (veränderten) Einstellungen laden muss.

Los gehts: "Resource.h" erstellen.
So sieht sie aus:

C-/C++-Quelltext

1
2
3
4
5
6
7
8
#pragma once

////////////////////////////////////////////////////////////////
//SettingsDialog
#define IDD_SETTINGS_DIALOG                     100 //Der Dialog
#define IDC_TAB_CONTROL                         101 //Das Tab Control
#define IDC_APPLY                               102 //Apply-Button
#define IDC_DSMA                                103 //"Don't show me again" Check-Box

Hier werden die IDs der einzelnen Dialoge und Controls definiert, über die sie dann im Programm angesprochen werden können.

Und nun zur Dialog-Resource (Resource.rc). Am Anfang sieht die Datei so aus:

C-/C++-Quelltext

1
2
3
4
5
6
7
8
9
10
////////////////////////////////////////////////////////////////
//Includes
#include<Windows.h>
#include"Resource.h"

////////////////////////////////////////////////////////////////
//IDC_STATIC definieren, da dies nicht mehr in Windows.h definiert wird
#ifndef IDC_STATIC
#define IDC_STATIC -1
#endif

Hier werden die beiden Dateien "Windows.h" (Vordefinierte Dinge) und "Resource.h" (eigene Definitionen) inkludiert.
Dazu wird für statische Elemente (wie Text) noch IDC_STATIC definiert, da das nicht mehr Windows.h übernimmt.

Jetzt kann der eigentliche Dialog auch schon gebaut werden.
Dazu kann man entweder den Dialog-Wizard (wenn der so heißt) benutzen, oder das selbst schreiben.
Am Ende sollte es aber in etwa so aussehen:

C-/C++-Quelltext

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
////////////////////////////////////////////////////////////////
//SettingsDialog
//Größe insgesamt: 220 x 270. Er wird beim Erstellen genau in die Mitte des Bildschirms geschoben.
IDD_SETTINGS_DIALOG DIALOG 0, 0, 220, 270
//Mit Titel, Minimierknopf, Menü (per Icon) und allem Schnickschnack.
STYLE WS_POPUP|WS_CAPTION|DS_MODALFRAME|WS_SYSMENU|WS_MINIMIZEBOX
//Meine bevorzugte Schriftart
FONT 8, "Tahoma"
//Der Titel (sowie der ganze Dialog) in English gehalten
CAPTION "Settings"
{
    //Das Tab Control
    //Genug Platz für 200x200 große Pages
    //Dazu noch links, rechts und unten je 5 Einheiten Platz. Oben muss noch die Größe der Tabs mit einberechnet werden (nochmal 10)
    CONTROL         "", IDC_TAB_CONTROL, "SysTabControl32", TCS_TABS, 5, 5, 210, 220

    //Wenn man nicht jedesmal alles neu einstellen will...
    AUTOCHECKBOX    "Don't show me again.", IDC_DSMA,   120,235, 80, 10

    //Und die 3 Standard-Buttons
    PUSHBUTTON      "Exit",                 IDCANCEL,       10, 250, 60, 15     //Beenden
    PUSHBUTTON      "Apply",                IDC_APPLY,      80, 250, 60, 15     //Speichern
    DEFPUSHBUTTON   "OK",                   IDOK,           150,250, 60, 15     //Speichern und Beenden
}

Die Kommentare sollten alles soweit erklären.
Ich lasse also Platz für die Pages in der Größe: 200x200. Ich denke, das sollte reichen.
Zu den Pages komme ich später.

4. Die Programmierung des Dialogs

Auch wenn der Dialog noch nicht wirklich etwas kann (außer geschlossen werden) ist es trotzdem wichtig, dass man sich frühzeitig Gedanken macht, wie man den Dialog in der eigentliche Programm einbindet.
Dazu gibt es 2 separate Dateien: SettingsDialog.h und SettingsDialog.cpp. Darin wird alles, was mit dem Dialog zu tun hat, implementiert.

Kommen wir zur Header-Datei. Was muss denn alles rein? Eigentlich bloß die Deklaration von DoSettingsDialog und die DialogProc.
Tada:

C-/C++-Quelltext

1
2
3
4
5
6
7
8
9
#pragma once

//Settings-Dialog aufrufen
//Wenn der User auf OK geklickt hat, wird FALSE zurückgegeben.
//Wenn er abgebrochen hat, wird TRUE zurückgegeben.
BOOL DoSettingsDialog();

//Settings-Dialog-Proc
BOOL WINAPI SettingsDialogProc(HWND hDialog, UINT uMessage, WPARAM wParam, LPARAM lParam);

Auf den Rückgabewert sollte unbedingt geachtet werden!

Was muss den DoSettingsDialog alles machen?
Wie der Name schon sagt, soll die Funktion den SettingsDialog anzeigen und verwalten.
Die Pages, auf die ich später näher eingehe, werden als Child-Fenster erstellt. Darum reicht ein einfaches DialogBox nicht aus.
Der Dialog wird mit CreateDialog erstellt und die Nachrichten werden ordnungsgemäß bearbeitet.
Anhand der letzem Message (WM_QUIT) wird entweder TRUE oder FALSE zurückgegeben - je nachdem, was der Benutzer eben wollte.

Vorläufig sieht die Funktion so aus:

C-/C++-Quelltext

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
//Settings-Dialog aufrufen
BOOL DoSettingsDialog()
{
    //An dieser Stellt müsste geklärt werden, ob der User das letzte mal DSMA
    //(Don't show me again) gewählt hat und falls ja abbrechen!

    //Den Dialog Erstellen
    HWND hDialog= CreateDialog(GetModuleHandle(NULL), MAKEINTRESOURCE(IDD_SETTINGS_DIALOG), NULL, SettingsDialogProc);
    if(!hDialog)
        throw(std::exception("Creating Settings Dialog failed!"));

    //Und Nachrichten bearbeiten
    MSG Message;
    while(GetMessage(&Message, NULL, 0, 0))
    {
        if(!IsDialogMessage(NULL, &Message))
        {
            TranslateMessage(&Message);
            DispatchMessage(&Message);
        }
    }
    
    //Ergebnis auswerten
    //Bei Abbruch wird TRUE zurückgegeben
    //Mit OK bestätigt -> FALSE
    //Immernoch -1 -> Fehler!
    if(Message.wParam == IDCANCEL)  return(TRUE);       
    else if(Message.wParam == IDOK) return(FALSE);
    else                            throw(std::exception("Settings Dialog did not return a valid status code!"));
}


Kommen wir nun zur SettingsDialogProc.
Ich habe alles kommentiert:

C-/C++-Quelltext

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
//Settings-Dialog-Proc
BOOL WINAPI SettingsDialogProc(HWND hDialog, UINT uMessage, WPARAM wParam, LPARAM lParam)
{
    static HWND hTabControl= NULL;                      //das Tab-Control
    
    switch(uMessage)
    {
    case WM_INITDIALOG:
        {
            //TabControl initialisieren
            InitCommonControls();
            hTabControl= GetDlgItem(hDialog, IDC_TAB_CONTROL);

            //An dieser Stelle werden später die Tabs eingetragen und die Pages erstellt.
            
            //"Don't show me again." initialisieren
            //Dazu müsste die letzte Wahl geladen werden und das Häkchen entsprechend gesetzt werden.         

            //Dialog in die Mitte rücken und anzeigen
            CenterWindow(hDialog);
            ShowWindow(hDialog, TRUE);

            return(TRUE);
        }

    //Der Benutzer hat wieder irgendetwas angestellt...
    case WM_COMMAND:
        switch(LOWORD(wParam))
        {
        case IDC_APPLY:
            {
                //Speichern

                //Das gilt auch für "Don't show me again." -> Lass dir etwas einfallen ;)
                
                //Später werden hier noch alle Pages gespeichert.

                //Aber nicht beenden!
                return(TRUE);
            }

        case IDCANCEL:
            {
                //Beenden
                DestroyWindow(hDialog);
                PostQuitMessage(LOWORD(wParam));

                //Alle Pages beenden
                //-> später

                return(TRUE);
            }

        case IDOK:
            {
                //Speichern und beenden 
                SendMessage(hDialog, WM_COMMAND, IDC_APPLY, 0);

                DestroyWindow(hDialog);
                PostQuitMessage(LOWORD(wParam));

                //Alle Pages beenden
                //-> später
            }
        }

    default:
        break;
    }

    return(FALSE);
}


Beim Erstellen des Dialogs ist euch sicher CenterWindow aufgefallen. Diese Funktion verschiebt ein Fenster so, dass es genau in der Mitte des Bildschirms liegt.

C-/C++-Quelltext

1
2
3
4
5
6
7
8
9
10
//Fenster mittig ausrichten
void CenterWindow(HWND hWindow)
{
    RECT Size;
    GetWindowRect(hWindow, &Size);
    SetWindowPos(hWindow, 0,
        (GetSystemMetrics(SM_CXSCREEN) - (Size.right - Size.left))/2,
        (GetSystemMetrics(SM_CYSCREEN) - (Size.bottom - Size.top))/2,
        0, 0, SWP_NOZORDER|SWP_NOSIZE);
}


Wenn man jetzt DoSettingsDialog aufruft, wird man diesen Dialog vorfinden:

(Link)

Wenn nur schon der Inhalt drin wäre...

Nichts desto trotz, war das schon ein großer Schritt.
Durch neue Funktionen wie InitCommonControls kamen einige zusätzliche Header und Libs hinzu.
Das wird alles von SettingsDialog.h erledigt. Der Anfang der Datei sieht jetzt so aus:

C-/C++-Quelltext

1
2
3
4
5
6
7
#pragma once
#include<exception>
#include<map>
#include<Windows.h>
#include<Commctrl.h>
#pragma comment(lib, "Comctl32.lib")
#include"Resource.h"


Mit einer std::map werden später die Pages verwaltet.


5. Eine Basisklasse für Pages

Ich habe so oft von Pages geredet und gesagt, was damit alles gemacht werden soll. Jetzt ist es an der Zeit das umzusetzen.
Um das ganze etwas objektorientierter zu gestalten, benutze ich eine Basisklasse für die Pages, von der zukünftig alle speziellen Pages (für Video, Audio, etc.) abgeleitet werden.
Eine Page soll:
- Ihre Daten (Titel für Tab, ihre Resource) angeben
- Auf Commands (WM_COMMAND) reagieren
- Die Erstellung und Zerstörung der Page übernehmen die Init- und Exit-Methode

Das ganze sieht am Ende so aus:

C-/C++-Quelltext

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
//String definieren
typedef std::string String;

//...

//////////////////////////////////////////////////////////////////
//Basisklasse für Pages im Settings-Dialog
class SettingsPageBase
{
private:
    HWND hPageWindow;       //Das Dialog-Fenster

public:
    ////////////////////////////////////////////////////////////////////////////////
    //Konstruktor und Destruktor
    SettingsPageBase();
    virtual ~SettingsPageBase();

    ////////////////////////////////////////////////////////////////////////////////
    //Init, Exit und Save

    //Initialisieren
    //Diese Methode muss abgeleitet werden!
    //In der eigenen Init-Methode muss erst SettingsPageBase::Init() aufgerufen werden
    //und erst dann können eigene Initialisierungs-methoden aufgerufen werden.
    //Diese Methode initialisiert die Dialog-Box der Page.
    virtual void Init(HWND hDialog);

    //Herunterfahren
    // Diese Methode muss nicht abgeleitet werden, sollte aber.
    // Nachdem alles eigene heruntergefahren wurde, muss SettingsPageBase::Exit() eufgerufen werden!
    // Diese Methode zerstört die Page-Dialogbox.
    // Diese Methode wird automatisch aufgerufen, sobald der User "OK" oder "Exit" gedrückt hat. Bei "OK" wird vorher noch Apply Aufgerufen.
    virtual void Exit();

    //Einstellungen übernehmen
    //Diese Methode muss implementiert werden! Die Basismethode muss nicht aufgerufen werden, da es gar keine gibt.
    //Sie wird automstisch aufgerufen, sobald der User auf "OK" oder "Apply" gedrückt hat.
    //Bei "OK" kommt zusätzlich noch hinzu, dass Exit aufgerufen wird.
    virtual void Apply() =0;

    ////////////////////////////////////////////////////////////////////////////////
    //Getter
    //Gibt das Handle des Dialogs (= Page) zurück.
    HWND GetPageWindow();

    //Gibt den Name der Page zurück.
    //Muss implementiert werden!
    virtual String GetPageName() =0;

    //Gibt das Modul (Instanz) an, in dem sich die Dialog-Resource befindet.
    //Muss implementiert werden!
    virtual HMODULE GetDialogResourceModule() =0;

    //Gibt die Resource-ID der Dialog-Resource zurück.
    //Muss implementiert werden!
    virtual DWORD GetDialogResourceID() =0;

    ////////////////////////////////////////////////////////////////////////////////
    //Event-Handling
    //Command-Handling
    //Bei einer WM_COMMAND Nachricht wird diese Methode aufgerufen. Sie muss implementiert werden!
    //Auf OK, Apply oder Cancel-Messages darf nicht reagiert werden - das übernimmt der Dialog.
    //Nur auf die Messages der eigenen Controls muss eingegangen werden.
    //Wenn die Nachricht verarbeitet werden konnte, muss TRUE zurückgegeben werden, ansonsten FALSE.
    virtual BOOL OnCommand(WPARAM wParam, LPARAM lParam) =0;
};


Zusammengefasst muss eine abgeleitete die folgenden Methoden implemenieren:
- Konstruktor und Destruktor
- Init (Geerbte Methode zu erst aufrufen!)
- Exit (Geerbte Methode zu letzt aufrufen!)
- GetPageName, GetDialogResourceModule, GetDialogResourceID - Damit kann dann die Page (Child-Dialog) und der Tab erstellt werden
- OnCommand um auf die Messages der eigenen Controls zu reagieren

Fangen wir an mit Konstruktor und Destruktor: Die machen das übliche.

C-/C++-Quelltext

1
2
3
4
5
6
7
8
9
10
11
//////////////////////////////////////////////////////////////////
//Konstruktor und Destruktor
SettingsPageBase::SettingsPageBase()
{
    hPageWindow= NULL;
}

SettingsPageBase::~SettingsPageBase()
{
    hPageWindow= NULL;
}

Nicht vergessen: Im eigenen Konstruktor muss der Konstruktor von SettingsPageBase aufgerufen werden!

Interessanter wird dann schon die Init-Methode. Sie erstellt den Child-Dialog - die Page.
Um auch die richtige Dialog-Resource zu benutzen (zur Resource komme ich später) gibt es ja die Getter.
Exit schließt die Page.
Am Ende sieht das dann so aus:

C-/C++-Quelltext

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//////////////////////////////////////////////////////////////////
//Initialisieren
void SettingsPageBase::Init(HWND hDialog)
{
    //Dialog erstellen
    hPageWindow= CreateDialog(GetDialogResourceModule(),
        MAKEINTRESOURCE(GetDialogResourceID()),
        hDialog, PageProc);

    //Der Tab wird vom Settings-Dialog (= Tab-Dialog) automatisch eingetragen
}

//Herunterfahren
void SettingsPageBase::Exit()
{
    //Aufräumen
    DestroyWindow(hPageWindow);
}


Hier taucht die Funktion PageProc auf. Das ist eine einheitliche Dialog-Proc für die Pages.
Sie leitet WM_COMMAND Messages an den Settings-Dialog weiter, dieser kann sie dann verarbeiten und schickt sie zum Schluss zurück an die Page mittels SettingsPageBase::OnCommand.
Als Code formuliert sieht das folgendermaßen aus:

C-/C++-Quelltext

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//uniforme Page-Proc
BOOL WINAPI PageProc(HWND hDialog, UINT uMessage, WPARAM wParam, LPARAM lParam)
{
    switch(uMessage)
    {
    case WM_INITDIALOG:
        return(TRUE);

    case WM_COMMAND:
        //Commands weiterleiten an Parent, der leitet es dann an die Page weiter
        SendMessage(GetParent(hDialog), WM_COMMAND, wParam, lParam);
        return(TRUE);

    default:
        break;
    }

    return(FALSE);
}


Die Getter sollten klar sein, deshalt gibt es an dieser Stellt keine Posts dazu.

6. Dialog-Resourcen für Pages

Jetzt kommt wieder der Designer-Teil.
Dazu gibt es aber klare Vorgaben, damit es überhaupt funktioniert.
Die Resource für eine Page sollte so aussehen:

C-/C++-Quelltext

1
2
3
4
5
6
7
8
//SettingsPageBase Resource
IDD_PAGE_BASE DIALOG 10, 20, 200, 200   //Genaue Position und Größe!
STYLE WS_CHILD  //Wird ein Child des Settings-Dialogs
FONT 8, "Tahoma"
{
    //Hier kommt der Inhalt der Page rein.
    //Aufpassen, dass die 200x200 eingehalten werden!
}

Dieses Copy'n'Paste-Snippet sollte man als Grundstein für alle Pages nutzen.

7. Integration der Pages in den SettingsDialog

Der Settings-Dialog lief bereits ganz gut. Allerdings noch ganz ohne Pages.
Das wird jetzt geändert.
Woher kommen denn die Pages?
- Einige sind fest vorgegeben, da sie überaus notwendig sind. Wie zum Beispiel eine "Video"-Page (kommt in Teil 3 ;) ) für 3D-Spiele.
- Eigene spiel-spezifische Pages

Verpackt werden einfach alle in eine Liste:

C-/C++-Quelltext

1
2
//Liste mit SettingsPages
typedef std::list<SettingsPageBase*> PageList;

Solch eine Liste füllt man dann mit eigenen Pages und übergibt sie an den SettingsDialog per Parameter von DoSettingsDialog.
Dieser muss etwas abgewandelt werden.
- Zuerst muss er eine Liste erhalten
- Diese Liste muss an die DialogProc übergeben werden, da sie ja die Pages verwalten soll

Kurz und knapp:

C-/C++-Quelltext

1
2
3
4
5
6
7
8
9
10
11
BOOL DoSettingsDialog(PageList * pPages)
{
    //An dieser Stellt müsste geklärt werden, ob der User das letzte mal DSMA
    //(Don't show me again) gewählt hat und falls ja abbrechen!

    //Die Pages als zusätzlichen Parameter mit auf den Weg geben
    LPARAM InitParam= (LPARAM)(pPages);

    //Den Dialog Erstellen
    HWND hDialog= CreateDialogParam(GetModuleHandle(NULL), MAKEINTRESOURCE(IDD_SETTINGS_DIALOG), NULL, SettingsDialogProc, InitParam);
...


Ab hier liegt es an der DialogProc. Also ran ans Wert!
Was muss den alles geändert werden?

Neue statische Variablen:
- Ein Container für alle Pages
- Ein Zeiger auf die fokusierte Page

In der Init-Phase (WM_INITDIALOG):
- Die Liste aus dem LPARAM Parameter herausfischen
- Für alle Einträge in der Pages-Liste:
-- Tab erstellen
-- Page erstellen
- Erste Page wählen

In der Exit-Phase (OK- oder Exit-Button gedrückt):
- Bei OK erstmal so tun als ob Apply gedrückt wurde
- Alle Pages durchgehen und alle beenden

Für Apply: Alle Pages durchgehen und Apply aufrufen.

Na dann mal los.
Der vorhin angesprochene Container ist bei mir eine std::map. Darin werden die Pages (SettingsPageBase*) nach einer laufenden Nummer (DWORD) sortiert.
Das mit der Nummer ist ein absolutes Muss, da Windows für die einzelnen Tabs ja auch nur Nummern hat.

C-/C++-Quelltext

1
2
static std::map<DWORD, SettingsPageBase*> Pages;    //Pages nach Nummer sortiert
static SettingsPageBase * pCurrentPage;             //Aktuelle Page


Nach dem das Tab-Control initialisiert wurde, werden die einzelnen Tabs und die Pages erstellt.
Jedes Tab erhält eine Nummer (0, 1, 2, 3, 4, ...) und damit kann in der Map die zugehörige Page gefunden werden.
Ein bisschen Code sagt mehr als tausend Worte:

C-/C++-Quelltext

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
            //Pages eintragen
            PageList *pPageList= (PageList*)(lParam);
            if(pPageList->empty())  throw(std::exception("Page List is empty!"));

            TCITEM TabItem;             //Grundstruktur für einen Tab
            TabItem.mask = TCIF_TEXT;   //Nur Text im Tab (es sich auch Icons möglich!)
            DWORD dwPage= 0;            //Laufende Nummer
            //Alle durchgehen
            for(PageList::iterator Iter= pPageList->begin(); Iter != pPageList->end(); Iter++)
            {
                //Tab-Page-Eintrag erstellen
                //Zuerst den Name eintragen
                char Buffer[256];
                strncpy_s(Buffer, (*Iter)->GetPageName().c_str(), (*Iter)->GetPageName().length());
                TabItem.pszText= Buffer;
                //Und jetzt den Tab erstellen
                TabCtrl_InsertItem(hTabControl, dwPage, &TabItem);

                //Page initialisieren und eintragen
                (*Iter)->Init(hDialog);     //Init-Methode aufrufen
                Pages[dwPage++]= (*Iter);   //Und in der Map verewigen
            }
            
            //Erste Page wählen
            pCurrentPage= Pages[0];
            TabCtrl_SetCurSel(hTabControl, 0);
            ShowWindow(pCurrentPage->GetPageWindow(), TRUE);

Die erste Page wird als aktuelle Page gewählt, im TabControl selektiert und sichtbar gemacht.

Wen die TabCtrl-Funktionen interessieren, der sollte mal in der MSDN nachsehen. Da gibt es viele Infos dazu.
Es gibt noch viele weitere nützliche Funktionen rund um Tabs!

An den entsprechenden Stellen (siehe Code) kommen die folgenden Zeilen hinzu, damit auf die Knopfdrücke ordentlich reagiert werden kann:
Für case IDC_APPLY:

C-/C++-Quelltext

1
2
3
                //Alle Pages speichern
                for(std::map<DWORD, SettingsPageBase*>::iterator Iter= Pages.begin(); Iter != Pages.end(); Iter++)
                    Iter->second->Apply();


Für case IDCANCEL: und case IDOK:

C-/C++-Quelltext

1
2
3
                //Alle Pages beenden
                for(std::map<DWORD, SettingsPageBase*>::iterator Iter= Pages.begin(); Iter != Pages.end(); Iter++)
                    Iter->second->Exit();


Wenn jetzt aber der Spieler ein Control der Page bedient (z.B. CheckBox betätigen) erfährt das der SettingsDialog, aber nicht die Page.
Also werden kurzerhand alle unbekannten WM_COMMANDS an die aktuelle Page weitergeleitet:

C-/C++-Quelltext

1
2
3
        default:
            //An aktuelle Page weiterleiten
            return(pCurrentPage->OnCommand(wParam, lParam));



Ein wichtiges Problem wäre da schon noch. Die Pages sind initialisiert, werden gespeichert und beendet. Was fehlt denn da noch?
Irgendwie muss doch auch zwischen den Pages umgeschalten werden können!
Man muss irgendwie reagieren können, wenn der Spieler einen Tab gewählt hat.
Dafür gibt es die WM_NOTIFY Nachricht. Wenn der Spieler jetzt einen anderen Tab gewählt hat, dann wird die aktuelle Page unsichtbar gemacht und die neue sichtbar.
Umgesetzt liefert das folgenden Code, der am besten nach WM_COMMAND eingefügt wird:

C-/C++-Quelltext

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
    case WM_NOTIFY:
        switch(((LPNMHDR)lParam)->code)
        {
        case TCN_SELCHANGE: //Der User hat einen anderen Tab gewählt
            { 
                //Zuerst alte Page unsichtbar machen
                ShowWindow(pCurrentPage->GetPageWindow(), FALSE);

                //Und jetzt die gewählte Page sichtbar machen
                DWORD dwChoosenPage = (DWORD)TabCtrl_GetCurSel(hTabControl);
                pCurrentPage= Pages[dwChoosenPage];
                ShowWindow(pCurrentPage->GetPageWindow(), TRUE);

                return(TRUE);
            } 
        }
        break;



Das war es! Fertig!
Allerdings kann man mit dem Dialog noch recht wenig anfangen, da er keine Pages hat, die er anzeigen kann.
Also demonstriere ich mal, wie man bei einer neuen Page vorgeht.

8. Demoprojekt

Als Demo für den Dialog hab ich mir wirklich etwas ganz kleines einfallen lassen, da es im 3. Teil des Tutorials eine ganze "Video-Page" geben wird.
Die Page beinhaltet nur eine CheckBox und etwas Text.
Die Resourcen:

Resource.h:

C-/C++-Quelltext

1
2
3
//Demo Page
#define IDD_DEMO_PAGE                           110
#define IDC_DEMO_CB                             111


Resource.rc:

C-/C++-Quelltext

1
2
3
4
5
6
7
8
//Demo Page
IDD_DEMO_PAGE DIALOG 10, 20, 200, 200
STYLE WS_CHILD
FONT 8, "Tahoma"
{
    AUTOCHECKBOX    "Demo Check Box",   IDC_DEMO_CB,    5, 5, 80, 10
    LTEXT           "This is a Demo Check Box. Check it!", IDC_STATIC, 5, 15, 80, 50
}


Dann wird eine Klasse von SettingsPageBase abgeleitet, die sogar einen individuellen Page-Name erhält:
DemoPage.h:

C-/C++-Quelltext

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#pragma once
#include<WindowsX.h>
#include"Resource.h"
#include"SettingsPageBase.h"

class DemoPage : public SettingsPageBase
{
private:
    ///////////////////////////////////////////////////////
    //Variablen
    HINSTANCE hProgramInstance;
    String StrSubName;

    HWND hCheckBox;
    BOOL bChecked;

public:
    ///////////////////////////////////////////////////////
    //Konstruktor und Destruktor
    DemoPage(const String & SubName, HINSTANCE hInstance);
    ~DemoPage();

    ///////////////////////////////////////////////////////
    //Init, Exit und Save
    void Init(HWND hDialog);
    void Exit();
    void Apply();

    ///////////////////////////////////////////////////////
    //Getter
    String GetPageName();
    HMODULE GetDialogResourceModule();
    DWORD GetDialogResourceID();

    ///////////////////////////////////////////////////////
    //CommandHandling
    BOOL OnCommand(WPARAM wParam, LPARAM lParam);
};


DemoPage.cpp:

C-/C++-Quelltext

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
#include"DemoPage.h"

///////////////////////////////////////////////////////
//Konstruktor und Destruktor
DemoPage::DemoPage(const String & SubName, HINSTANCE hInstance) : SettingsPageBase()
{
    hProgramInstance= hInstance;
    StrSubName= SubName;
}

DemoPage::~DemoPage()
{}

///////////////////////////////////////////////////////
//Init, Exit und Save
void DemoPage::Init(HWND hDialog)
{
    //Geerbte Methode zu erst aufrufen
    SettingsPageBase::Init(hDialog);

    //Laden
    bChecked= FALSE;
    
    hCheckBox= GetDlgItem(hDialog, IDC_DEMO_CB);
    Button_SetCheck(hCheckBox, bChecked);
}

void DemoPage::Exit()
{
    //Exit
    //Hier muss nichts weiter gemacht werden

    //Geerbte Methode zum Schluss aufrufen
    SettingsPageBase::Exit();
}

void DemoPage::Apply()
{
    //Abfragen
    bChecked= Button_GetCheck(hCheckBox);
    
    //Speichern
    MessageBox(NULL, "Die Demo Page würde jetzt speichern.", StrSubName.c_str(), MB_OK|MB_ICONINFORMATION);
}

///////////////////////////////////////////////////////
//Getter
String DemoPage::GetPageName()
{
    return(String("Demo Page - ") + StrSubName);
}

HMODULE DemoPage::GetDialogResourceModule()
{
    return(hProgramInstance);
}

DWORD DemoPage::GetDialogResourceID()
{
    return(IDD_DEMO_PAGE);
}

///////////////////////////////////////////////////////
//CommandHandling
BOOL DemoPage::OnCommand(WPARAM wParam, LPARAM lParam)
{
    //Hier muss nichts weiter getan werden

    return(FALSE);
}


Und dazu noch WinMain:

C-/C++-Quelltext

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include<Windows.h>
#include"SettingsDialog.h"
#include"DemoPage.h"

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE, char*, int)
{
    try
    {
        //Demo-Page erstellen
        DemoPage DemoPage1("#1", hInstance);
        DemoPage DemoPage2("#2", hInstance);
        PageList Pages;
        Pages.push_back(&DemoPage1);
        Pages.push_back(&DemoPage2);
        
        //Dialog starten
        if(DoSettingsDialog(&Pages))
        {
            //Abgebrochen!
            //Normal beenden
            MessageBox(NULL, "Abbrechen geklickt", "", MB_OK);
            return(0);
        }
        MessageBox(NULL, "OK geklickt", "", MB_OK);
        //Alles klar! Der User ist mit den Einstellungen einverstanden.

        //Das Programm kann jetzt die (veränderten) Einstellungen laden.
    }
    catch(std::exception Err)
    {
        MessageBox(NULL, Err.what(), "Error!", MB_OK|MB_ICONERROR);
    }

    return(0);
}


Das Demo-Projekt kann hier heruntergeladen werden: Link

9. Ausblick

So, Teil 2 geschafft!
Wer Teil 1 ausgelassen hat, sollte ihn jetzt lesen, bevor er sich an Teil 3 macht.
Ansonsten hoffe ich ein einigermaßen gutes Framework für Tabbed Dialogs geliefert zu haben.
Im dritten Teil des Tutorials geht es dann ganz speziell um eine "Video-Page". Sie nutzt die Hilfsfunktionen aus Teil 1 um anständig alles wählbar zu machen.

Der SettingsDialog kann noch verbessert werden!
- Ihm fehlt noch eine Möglichkeit, wie er "Don't show me again." speichern und laden kann. Im Code wird an entsprechenden Stellen darauf hingewiesen.
- Ein Icon wäre nicht schlecht. PS: Benutzt LoadImage anstatt LoadIcon ;)


Wie immer dürft ihr fleißig fragen und kritisieren.

Dieser Beitrag wurde bereits 2 mal editiert, zuletzt von »BlazeX« (25.11.2010, 16:01)