List-/Comboboxen und Editfelder

In dieser Lektion betrachten wir die etwas komplexeren Controls wie Editfelder, Listboxen und Comboboxen. Außerdem lernen Sie einen neuen Typ von Anwendung kennen, die dialogbasierende Anwendung. Eine dialogbasierende Anwendung besitzt kein Hauptfenster im üblichen Sinne sondern besteht nur aus einem Dialog. Im Beispiel zu dieser Lektion werden wir eine dialogbasierende Anwendung entwickeln die folgenden Dialog darstellt:

Der fertige Dialog des Beispiels

Der Dialog besteht unter anderem aus drei Listboxen, einem Editfeld sowie einer Combobox. Zwischen den beiden oberen Listboxen können Einträge durch anklicken der dazwischen liegenden Buttons verschoben werden. Die Listbox in der Mitte des Dialogs kann zur Laufzeit um Einträge erweitert werden. Dazu wird im darunter stehenden Editfeld der hinzuzufügende Eintrag eingegeben und danach der Button Übernehmen angeklickt. Die Combobox unten im Dialog erlaubt die Auswahl eines Dateityps aus einer vorgegebenen Liste.

Beginnen wir das Beispiel nun mit der Erstellung des Anwendungsrahmens. Als Anwendungstyp wird wie gesagt eine dialogbasierende Anwendung gewählt.

Erstellen Sie zunächst wie gewohnt eine MFC Exe-Anwendung. Geben Sie dem Projekt den Namen LCEdit. Im daraufhin von Anwendungs-Assistenten eingeblendeten Dialog Schritt 1 markieren Sie nun unter Anwendungsart nicht mehr Einzelnes Dokument (SDI) sondern Dialogfeldbasierend.

Eine dialogfeldbasierende Anwendung

Ebenfalls geändert hat sich nun der Schritt 2. Wählen Sie aus den verfügbaren Merkmalen für das Beispiel lediglich das Merkmal 3D-Steuerelemente aus. Im Eingabefeld ganz unten können Sie dann noch den Titel des Dialogs eingeben.

Parameter der Anwendung

Klicken Sie dann den Button Fertigstellen an und der Anwendungs-Assistent erstellt eine Anwendung die folgende Klassen enthält:

Die Klassen einer Dialoganwendung

Die Anwendung besteht jetzt nur noch aus einer Applikations-Klasse CLCEditApp und einer Dialogklasse CLCEditDlg. Sehen Sie sich nun einmal die Methode InitInstance(...) der Klasse CLCEDitApp an. Sie werden dort u.a. die Definition des Dialogobjektes sowie den Aufruf der Methode DoModal(...) zur Anzeige des Dialogs finden, die Sie schon aus der letzten Lektion her kennen.

Beginnen wir jetzt mit der Erstellung des Dialogs.

Wechseln Sie in die Ressourcen-Ansicht und öffnen dort den Dialog IDD_DIALOG1. Fügen Sie zunächst die oben dargestellten Controls zum Dialog hinzu. Lassen Sie dabei alle Eigenschaften der Controls bis auf weiteres noch auf ihren Default-Einstellungen stehen. Um ein Editfeld, eine Listbox oder eine Combobox zum Dialog hinzuzufügen, klicken Sie bitte die unten gekennzeichneten Symbole im Toolbar an:

Combo-,Listbox und Editfeld

Ändern Sie die Beschriftungen der statischen Textfelder (über den oberen Listboxen) und der Gruppenfelder wie oben dargestellt ab. Anschließend sind die Buttons noch anzupassen. Geben Sie den Buttons die nachfolgenden IDs und passen Sie ebenfalls deren Beschriftung an.

Button-Eigenschaften

Zum Schluss fügen Sie zur Dialogklasse die Nachrichtenbearbeiter für die BN_CLICKED Nachricht (wie in der letzten Lektion besprochen) für jeden der Buttons hinzu. Übernehmen Sie die jeweils vorgeschlagenen Methodennamen. Die Methoden werden nachher noch vervollständigt.

Sehen wir uns nun die Listboxen an. Listboxen werden in der MFC durch die Klasse CListBox repräsentiert. Um in einem Dialog sinnvoll mit einer Listbox arbeiten zu können, muss zunächst ein entsprechendes Objekt innerhalb der Dialogklasse erstellt werden. Anschließend können Sie in der Methode OnInitDialog(...) über die CListBox Methode AddString(...) der Listbox Einträge hinzufügen. Als Parameter erhält die Methode einen char-Zeiger auf den hinzuzufügenden Eintrag und liefert als Returnwert (bei erfolgreichem Aufruf) die Position innerhalb der Listbox, an die der Eintrag eingefügt wurde. Diese Position muss nicht unbedingt um eins höher sein als beim vorherigen Aufruf der Methode, da Listboxen über die Eigenschaft Sortieren die Einträge alphabetisch sortieren können. Das Gegenstück zur Methode AddString(...) ist die Methode GetText(...). Sie liest einen Eintrag aus einer Listbox aus. Die Methode erhält als Parameter u.a. die Position des auszulesenden Eintrags, wobei der erste Eintrag die Position '0' besitzt.

Außer Einträge hinzuzufügen und auszulesen, können Sie auch Einträge bei Bedarf wieder aus der Listbox löschen. Um einen Eintrag zu entfernen wird die Methode DeleteString(...) aufgerufen, der Sie die Position des zu löschenden Eintrags als Parameter mitgeben. Als Returnwert liefert die Methode die Anzahl der danach verbleibenden Einträge zurück.

Neben den sichtbaren (Text-)Einträgen kann eine Listbox auch pro Eintrag einen beliebigen 32-Bit Wert aufnehmen. Dieser 32-Bit Wert wird nachher im Beispiel dazu verwendet, um zwischen MFC und MFC-freien Kapiteln unterscheiden zu können. Über einen Button zwischen den Listboxen können dann z.B. alle MFC oder alle MFC-freien Kapitel auf einmal verschoben werden. Um für einen bestimmten Eintrag diesen 32-Bit Wert zu setzen wird die CListBox Methode SetItemData(...) eingesetzt. Auch Sie erhält wieder die Position des Listbox-Eintrags, dem der 32-Bit Wert zugewiesen soll sowie den 32-Bit Wert selbst. Der hiermit gesetzte Wert kann später dann mittels der Methode GetItemData(...) wieder ausgelesen werden.

Da die erwähnten Listbox-Funktionen im Fehlerfall die Returncode LB_ERR und LB_ERRSPACE zurückliefern, die intern durch die Werte -1 und -2 repräsentiert werden, sollten Sie -1 und -2 nicht als Zusatzinformation verwenden.

Gehen wir nun ans Bearbeiten der beiden Listboxen im Dialog oben. Die rechte Listbox soll mit den Kapiteln dieses Kurses belegt werden, wobei jeder Eintrag noch eine Zusatzkennung erhält, ob es sich um ein Kapitel mit oder ohne MFC handelt. Über die Buttons zwischen den Listboxen sollen die Kapitel von einer Listbox in die andere verschoben, nicht kopiert, werden. Die Buttons bewirken folgende Verschiebungen, von oben nach unten gesehen:

Buttons zum Verschieben von Einträgen Das in der linken Listbox markierte Kapitel wird nach rechts verschoben.
Alle Kapitel mit MFC nach rechts verschieben
Alle Kapitel ohne MFC nach rechts verschieben
Das in der rechten Listbox markierte Kapitel wird nach links verschoben
Alle Kapitel der rechten Listbox werden nach links verschoben

Die Auswahl von mehreren Einträgen gleichzeitig soll in beiden Listboxen nicht erlaubt sein. Die Einstellung der Auswahl-Möglichkeit erfolgt übrigens über die Listbox-Eigenschaft Auswahl. Durch anklicken des Fragezeichens oben links im Eigenschaftsdialog erhalten Sie wieder eine sehr gute Erklärung der einzelnen Möglichkeiten.

Auswahlmöglichkeiten einer Listbox

Da die Listboxen vorhin schon zum Dialog hinzugefügt wurden, müssen wir nun 'nur noch' deren Eigenschaften anpassen. Geben Sie der linken Listbox die ID IDC_NONSELECT und der rechten die ID IDC_SELECT. Die restlichen Eigenschaften können Sie auf ihrem Vorgabewert belassen. Standardmäßig werden Listboxen mit einfacher Auswahl erstellt deren Einträge alphabetisch sortiert werden.

Fügen Sie anschließend zur Dialogklasse für jede Listbox eine Membervariable über den Klassen-Assistenten hinzu. Da wir für die Listboxen entsprechende CListBox-Objekte erstellen müssen, stellen Sie im Feld Kategorie den Typ Control ein. Der Variablentyp wird daraufhin automatisch auf CListBox gesetzt. Geben Sie der Membervariable für die linke Listbox den Namen m_CNonSelect und der Membervariable für die rechte Listbox den Namen m_CSelect.

C++ Objekt mit einer Listbox verknüpfen

Als nächstes werden die vorgegebenen Einträge zur linken Listbox hinzugefügt. Dies erfolgt in der Methode OnInitDialog(...). Die rechte Listbox braucht nicht initialisiert werden, da sie am Anfang leer ist. Erweitern Sie OnInitDialog(...) wie folgt:

BOOL CLCEditDlg::OnInitDialog()
{
    CDialog::OnInitDialog();

    // Symbol für dieses Dialogfeld festlegen. Wird automatisch erledigt
    // wenn das Hauptfenster der Anwendung kein Dialogfeld ist
    SetIcon(m_hIcon, TRUE);             // Großes Symbol verwenden
    SetIcon(m_hIcon, FALSE);        // Kleines Symbol verwenden
   
    // ZU ERLEDIGEN: Hier zusätzliche Initialisierung einfügen
    // Index des hinzugefuegten Eintrags
    int    nIndex;
    // Listbox mit Eintraegen fuellen
    nIndex = m_CNonSelect.AddString("Einführung in WINDOWS");
    m_CNonSelect.SetItemData(nIndex,NONMFC);
    nIndex = m_CNonSelect.AddString("Einf. Progr. ohne MFC");
    m_CNonSelect.SetItemData(nIndex,NONMFC);
    nIndex = m_CNonSelect.AddString("Einführung MFC");
    m_CNonSelect.SetItemData(nIndex,MFC);
    nIndex = m_CNonSelect.AddString("Doc/View Modell");
    m_CNonSelect.SetItemData(nIndex,MFC);
    nIndex = m_CNonSelect.AddString("GDI");
    m_CNonSelect.SetItemData(nIndex,MFC);
    nIndex = m_CNonSelect.AddString("Eingaben");
    m_CNonSelect.SetItemData(nIndex,MFC);
    nIndex = m_CNonSelect.AddString("Ressourcen");
    m_CNonSelect.SetItemData(nIndex,MFC);
    return TRUE; // Geben Sie TRUE zurück, außer ein Steuerelement soll den Fokus erhalten
}

Legen Sie dann innerhalb der Dialogklasse noch die enum-Konstanten zur Kennzeichnung der nicht-MFC und der MFC Kapitel fest:

class CLCEditDlg : public CDialog
{
    ....
    virtual void DoDataExchange(CDataExchange* pDX);    // DDX/DDV-Unterstützung
    //}}AFX_VIRTUAL
private:
    enum {MFC, NONMFC};
// Implementierung
protected:
    HICON m_hIcon;
    ....
};

Wenn Sie jetzt das Programm übersetzen und starten, dann sollten in der linken Listbox die vorgegebenen Einträge erscheinen. Die Buttons sind allerdings noch ohne Funktion.

Jedem Eintrag in der Listbox wurde durch Aufruf der Methode SetItemData(...) zusätzlich noch die Kennung mitgegeben, ob es sich um ein Kapitel mit oder ohne MFC handelt. Wir brauchen diese Kennung gleich nachher beim Verschieben der Einträge. Beachten Sie bitte auch, dass die SetItemData(...) Methode mit der Positionsnummer aufgerufen wird, die AddString(...) zurückgeliefert hat. Die Positionsnummern werden, da die Listbox die Einträge alphabetisch sortiert, nicht aufsteigend sein.

So, gehen wir jetzt an die Kodierung der Button-Funktionen. Über die Buttons sollen Einträge zwischen den Listboxen verschoben werden. Dazu wird zuerst eine allgemeine Methode implementiert, die einen Eintrag zwischen zwei Listboxen kopiert (nicht löscht!).

Fügen Sie die Kopierfunktion CopyLBEntries(...) zur Dialogklasse hinzu. Definieren Sie die Methode wie folgt:
void CLCEditDlg::CopyLBEntries(CListBox &CFrom, CListBox &CTo, int nIndex)
{
    char *pszText;  
// Umzukopierender Text
    DWORD dwData;   
// und Datum
    int nNewIndex;  
// Neuer Index in der Ziel-Listbox
    int nSelTextLen;
// Laenge des zu kopierenden Textes

    // Textlaenge ermitteln (abschl. '0' dabei nicht vergessen!)
    nSelTextLen = CFrom.GetTextLen(nIndex)+1;
    // Speicher anfordern
    pszText = new char[nSelTextLen];
    // Text und 32-Bit Wert auslesen
    CFrom.GetText(nIndex,pszText);
    dwData = CFrom.GetItemData(nIndex);
    // und wieder einfuegen
    nNewIndex = CTo.AddString(pszText);
    CTo.SetItemData(nNewIndex,dwData);
    // Speicher freigeben!
    delete [] pszText;
}

Die Methode kopiert den Eintrag an der Position nIndex aus der Listbox CFrom in die Listbox CTo. Beachten Sie hier, dass nicht der Eintrag alleine sondern auch der dazugehörige 32-Bit Wert mit kopiert werden muss.

Als nächstes kann es ans kodieren der Button-Funktionen selbst gehen. Die Button-Methoden wurden bereits am Anfang der Lektion zur Dialogklasse hinzugefügt, so dass sie jetzt nur noch vervollständigt werden müssen. Zwei der Methoden sollen lediglich den in einer der Listboxen markierten Eintrag verschieben. Um den aktuell markierten Eintrag festzustellen, wird die CListBox Methode GetCurSel(...) aufgerufen. Sie liefert als Returnwert den Index des markierten Eintrags. Ist kein Eintrag markiert, so wird der Fehlercode LB_ERR zurückgegeben.

So, und jetzt dürfen Sie wieder rann! Versuchen Sie einmal die Button-Methoden OnSselect(...) und OnSdeselect(...) zu vervollständigen. Beim Anklicken des Buttons ID_SSELECT soll der in der linken Listbox markierte Eintrag in die rechte Listbox verschoben werden und beim Anklicken des Buttons ID_SDESELECT der in der rechten Listbox markierte Eintrag in die linke Listbox. Alle dazu notwendigen Methoden haben Sie bereits kennen gelernt.

HINWEIS: Kopieren Sie zuerst den markierten Eintrag mit Hilfe der vorhin implementierten Methode CopyLBEntries(...) und löschen Sie ihn erst danach.

Lösung zum Verschieben von Einträgen zwischen Listboxen

void CLCEditDlg::OnSdeselect()
{
    // TODO: Code für die Behandlungsroutine der Steuerelement-....
    // Ausgewaehlten Eintrag auslesen
    int nSelect = m_CSelect.GetCurSel();
    // Abpruefen, ob etwas ausgewaehlt ist
    if (nSelect != LB_ERR)
    {
        // Markierten Eintrag kopieren
        CopyLBEntries(m_CSelect, m_CNonSelect, nSelect);
        // und Eintrag loeschen
        m_CSelect.DeleteString(nSelect);
    }
    else
        MessageBox("Keine Auswahl getroffen","FEHLER",
                MB_OK|MB_ICONINFORMATION);
}
void CLCEditDlg::OnSselect()
{
    // TODO: Code für die Behandlungsroutine der Steuerelement-....
    // Ausgewaehlten Eintrag auslesen
    int nSelect = m_CNonSelect.GetCurSel();
    // Abpruefen, ob etwas ausgewaehlt ist
    if (nSelect != LB_ERR)
    {
        // Markierten Eintrag kopieren
        CopyLBEntries(m_CNonSelect, m_CSelect, nSelect);
        // und Eintrag loeschen
        m_CNonSelect.DeleteString(nSelect);
    }
    else
        MessageBox("Keine Auswahl getroffen","FEHLER",
                MB_OK|MB_ICONINFORMATION);
}

Haben Sie auch an die 'bösen' Anwender gedacht, die den Verschiebe-Button anklicken ohne dass ein Eintrag markiert ist?

Ende der Lösung

Übersetzen und starten Sie das Programm nun erneut. Sie können schon einzelne Einträge zwischen den Listboxen verschieben.

Beim Verschieben von mehreren Einträgen müssen Sie unbedingt beachten, dass sich nach dem Löschen eines Eintrags die Positionen aller nachfolgenden Einträge verschieben. Löschen Sie z.B. den Eintrag an der Position 0, so wird danach der Eintrag, der bisher auf der Position 1 stand, die Position 0 erhalten. Die beste und einfachste Vorgehensweise beim Löschen von mehreren Einträgen ist, die Listbox-Einträge von hinten nach vorne zu bearbeiten. Um die Anzahl der Einträge, und damit auch indirekt die letzte Position, auszulesen, wird die CListBox Methode GetCount(...) aufgerufen.

Erweitern Sie die Methoden zum Verschieben von mehreren Einträgen wie folgt. Beachten Sie dabei bitte, dass die Schleife zum Verschieben der Einträge von 'Anzahl der Einträge - 1' bis '0' bis läuft, da der erste Eintrag die Positionsnummer '0' besitzt!
void CLCEditDlg::OnMfcselect()
{
    // TODO: Code für die Behandlungsroutine der Steuerelement-....
    // Anzahl der Eintraege holen
    int nNoOfEntries = m_CNonSelect.GetCount();
    // Eintraege verschieben
    for (int iLoop=nNoOfEntries-1; iLoop>=0; iLoop--)
    {
        // Falls Kapitel zur MFC gehoert
        if(m_CNonSelect.GetItemData(iLoop) == MFC)
        {
            // zuerst kopieren, dann loeschen
            CopyLBEntries(m_CNonSelect,m_CSelect,iLoop);
            m_CNonSelect.DeleteString(iLoop);
        }
    }
}

void CLCEditDlg::OnNomfcselect()
{
    // TODO: Code für die Behandlungsroutine der Steuerelement-....
    // Anzahl der Eintraege holen
    int nNoOfEntries = m_CNonSelect.GetCount();
    // Eintraege verschieben
    for (int iLoop=nNoOfEntries-1; iLoop>=0; iLoop--)
    {
        // Falls Kapitel nicht zur MFC gehoert
        if(m_CNonSelect.GetItemData(iLoop) == NONMFC)
        {
            // zuerst kopieren, dann loeschen
            CopyLBEntries(m_CNonSelect,m_CSelect,iLoop);
            m_CNonSelect.DeleteString(iLoop);
        }
    }
}
void CLCEditDlg::OnAlldeselect()
{
    // TODO: Code für die Behandlungsroutine der Steuerelement-....
    // Anzahl der Eintraege holen
    int nNoOfEntries = m_CSelect.GetCount();
    // Abpruefen, ob ueberhaupt etwas zu kopieren ist
    if (nNoOfEntries != 0)
    {
        // Eintraege von hinten nach vorne verschieben
        for (int iLoop=nNoOfEntries-1; iLoop>=0; iLoop--)
        {
            CopyLBEntries(m_CSelect,m_CNonSelect,iLoop);
            m_CSelect.DeleteString(iLoop);
        }
    }
    else
        MessageBox("Keine Einträge vorhanden!","HINWEIS",
                MB_OK|MB_ICONINFORMATION);
   
}

Wenn Sie das Programm übersetzen und starten, so werden je nach Button die entsprechenden Einträge zwischen den Listboxen verschoben.

Und noch einmal, weil man's gerne vergisst: denken Sie immer daran, dass die Klasse CListBox, wie auch die anderen Controls, von der Klasse CWnd abgeleitet ist und damit auch über deren Funktionalität verfügt. So kann eine Listbox z.B. mittels EnableWindow(...) vom Programm aus gesperrt werden.

Wenden wir uns nun der Listbox in der Mitte des Dialogs zu. Diese Listbox weist zwei Besonderheiten auf: zum einen wird sie nicht mehr vom Dialog selbst initialisiert, wie die obige linke Listbox, sondern durch die Anwendung, und zum anderen können ihr Einträge über das darunter stehende Editfeld hinzugefügt werden. Außerdem werden nach dem Schließen des Dialogs alle in der Listbox vorhandenen Einträge, auch die neu hinzugefügten, im Ausgabefensters des Debuggers ausgegeben, d.h. diese Listbox verfügt nun über den DDX-Mechanismus.

Aber passen wir zuerst die Eigenschaften der Listbox und des Editfeldes an. Bis auf die IDs können Sie alle Eigenschaften der Controls auf ihren Defaultwerten belassen. Geben Sie den Controls die folgenden IDs:

Listbox und Editfeld ID

Als nächstes werden wir das Hinzufügen von Einträgen zur Listbox einbauen. Der hinzuzufügende Eintrag wird im Editfeld IDC_NEWENTRY eingegeben. Wird anschließend der Button Übernehmen angeklickt, so wird der eingegebenen Text in die Listbox übernommen und das Editfeld gelöscht. Die gesamte Funktionalität hierzu ist nachher im Nachrichtenbearbeiter des Buttons untergebracht.

Fangen wir mit dem Editfeld zur Eingabe an. Editfelder werden in der MFC durch die Klasse CEdit repräsentiert. Diese Klasse ist ebenfalls von der Klasse CWnd abgeleitet, so dass auch hier die gesamten Fensterfunktionen zur Verfügung stehen. Außerdem enthält die Klasse CEdit, genauso wie die vorhin schon behandelte Klasse CListBox, eine ganze Reihe von zusätzlichen Methoden.

WINDOWS kennt zwei Arten von Editfelder: Editfelder mit nur einer Zeile und Editfelder mit mehreren Zeilen. Im Beispiel wird ein Editfeld mit nur einer Zeile verwendet. Für Editfelder mit mehreren Zeilen stehen zusätzliche Methoden zur Verfügung wie z.B. GetLineCount(...) um die Anzahl der Zeilen im Editfeld zu bestimmen.

Bevor der Text aus dem Editfeld zur Listbox hinzugefügt wird muss erst einmal abgeprüft werden, ob überhaupt ein Text im Editfeld eingegeben wurde. Dies wird durch den Aufruf der CEdit-Methode LineLength(...) erreicht. Als Parameter erhält die Methode die Zeilennummer, für die die Zeilenlänge bestimmt werden soll. Da im Beispiel nur ein einzeiliges Editfeld eingesetzt wird, kann dieser Parameter auf seinem Defaultwert -1 belassen werden. Als Returnwert liefert die Methode die Anzahl der Zeichen in der entsprechenden Zeile. Um den Text des Editfeldes auszulesen wird (aber nur bei einzeiligen Editfelder!) die CWnd-Methode GetWindowText(...) verwendet.

Damit wäre der erste Teil, das Auslesen des eingegebenen Textes, vollständig beschrieben. Um diesen ausgelesenen Text anschließend zur Listbox hinzuzufügen, wird die vorhin eingeführte CListBox-Methode AddString(...) verwendet. Hierzu muss aber wiederum ein zur Listbox gehöriges CListBox-Objekt definiert sein.

Fügen Sie zur Dialogklasse zunächst die Membervariable m_CApiListbox vom Typ CListBox hinzu. Beachten Sie bitte bei der Definition der Membervariable über den Klassen-Assistent, dass im Feld Kategorie auch der Eintrag Control steht.

Erweitern Sie anschließend die Methode On2listbox(...) des Buttons Übernehmen wie nachfolgend angegeben. Beachten Sie bitte, dass das Editfeld nach dem Kopieren des Textes noch explizit gelöscht werden muss.

void CLCEditDlg::On2listbox()
{
    // TODO: Code für die Behandlungsroutine der Steuerelement-....
    // Zeiger auf das mit dem Edit-Control assoziierte
    // CEdit-Objekt holen
    CEdit *pEntry = static_cast<CEdit*>(GetDlgItem(IDC_NEWENTRY));
    // Abpruefen ob Eingabe im Editfeld vorhanden ist
    if (pEntry->LineLength() == 0)
        MessageBox("Kein Eintrag im Eingabefeld vorhanden!");
    else
    {
        // Text aus Editfeld auslesen
        CString      CEntryText;
        pEntry->GetWindowText(CEntryText);
        // und zur Listbox hinzufuegen
        m_CApiListbox.AddString(CEntryText);
        // Eingabefeld loeschen
        pEntry->SetWindowText("");
    }   
}

Übersetzen und starten Sie das Programm. Sie können nun der Listbox über das Editfeld und dem Button Übernehmen Einträge hinzufügen

Sehen wir uns jetzt den Datenaustausch der Listbox mit der Anwendung an. Hier wird's leider etwas komplizierter. Da eine Listbox mehrere Einträge aufnehmen kann, ist eine einfache Initialisierung, z.B. über ein CString-Objekt, nicht mehr möglich. Wir werden im Beispiel die Listbox-Einträge über ein CStringList-Objekt verwalten. Die Klasse CStringList ist eine MFC-Klasse, die beliebig viele Texte in einer verketteten Liste verwaltet. Auf eine detaillierte Beschreibung dieser Klasse soll hier verzichtet werden. Wir sehen uns nur die Methoden an, die für das Beispiel relevant sind. Um zu einer CStringList einen Text hinzuzufügen, kann u.a. deren Methode AddTail(...) eingesetzt werden. Sie erhält als Parameter einen char-Zeiger auf den zur Liste hinzuzufügenden Text.

Wenn Sie in der Online-Hilfe zu dieser Methode nachsehen werden Sie nur die Beschreibung der Methode zur Klasse CObjList finden. Die Klasse CStringList ist in manchen Versionen der Online-Hilfe leider nicht richtig beschrieben. Sie ist aber von CObject abgeleitet.
Bauen wir das Ausfüllen der Liste nun ins Beispiel ein. Zuerst wird zur Dialogklasse das für den Datenaustausch zuständige Objekt vom Typ CStringList hinzugefügt. Beachten Sie bitte, dass das Objekt public sein muss, da die Liste später von der Anwendung mit den Listbox-Einträgen gefüllt wird.

Definition der Membervariable für Listbox-DDX

Anschließend kann die Liste in der Methode InitInstance(...) des Anwendungsobjektes mit vorzugebenden Listbox-Einträgen gefüllt werden.

BOOL CLCEditApp::InitInstance()
{
    ....
    m_pMainWnd = &dlg;

    // Listbox-Eintraege in CStringList aufnehmen
    dlg.m_CListboxString.AddTail("Message API");
    dlg.m_CListboxString.AddTail("Telephone API");
    dlg.m_CListboxString.AddTail("Speech API");
    dlg.m_CListboxString.AddTail("Windows API");
    int nResponse = dlg.DoModal();
    ....
}

Nachdem die Liste mit den entsprechenden Einträgen gefüllt wurde, müssen die Einträge in einer Methode der Dialogklasse irgendwie in die Listbox kopiert werden. Ein erster Ansatz hierzu wäre, die Listbox wieder in der OnInitDialog(...) Methode zu initialisieren. Leider hat die Sache aber einen Haken. Die Listbox könnte zwar vor dem Darstellen des Dialogs mit den Einträgen initialisiert werden. Da die Listbox aber während des Programmlaufes um zusätzliche Einträge erweitert werden kann, müssen alle Listbox-Einträge beim Schließen des Dialogs auch wieder in die Liste zurück kopiert werden. Vielleicht fällt Ihnen jetzt spontan die Methode OnClose(...) ein um die Einträge zurück zu kopieren. Diese Methode wird ja bei einem Fenster immer dann aufgerufen, wenn das Fenster geschlossen werden soll. Doch leider funktioniert auch dies nicht, da Dialoge diese Methode nicht verwenden. Auch die Methode OnDestroy(...) kann hierfür nicht eingesetzt werden, da sie erst nach dem Zerstören des Dialogs, und damit auch nach dem Zerstören der Listbox, aufgerufen wird. Aber keine Panik, die Lösung naht in Form der vom Klassen-Assistent zum Dialog hinzugefügten Methode DoDataExchange(...). Innerhalb dieser Methode können Sie immer Daten von der Anwendung zum Dialog und umgekehrt transferieren. Das einzige Problem das hierbei zu lösen gilt ist: woher weiß die Methode, in welche Richtung der Datentransfer zu erfolgen hat. Nun, die Methode erhält als Parameter einen Zeiger auf ein Objekt vom Typ CDataExchange. Und innerhalb dieser Klasse gibt es eine public Membervariable m_bSaveAndValidate die den Wert TRUE annimmt, wenn Daten vom Dialog zur Anwendung zu transferieren sind und FALSE wenn der Datenaustausch in die andere Richtung erfolgt.

Nachdem wir wieder einmal auch dieses Problem 'gelöst' haben sehen wir uns jetzt an, wie die Dialogklasse mit dem CStringList-Objekt arbeitet. Als erstes muss innerhalb des Dialogs abgeprüft werden, ob überhaupt Einträge in der Liste vorhanden sind. Dazu wird die Methode IsEmpty(...) der CStringList Klasse aufgerufen. Sie liefert als Returnwert einen Wert ungleich 0 wenn die Liste Einträge enthält. Nachdem festgestellt wurde, dass die Liste Einträge enthält, muss das erste Listenelement adressiert werden. Hierfür wird die Methode GetHeadPosition(...) verwendet. Sie liefert als Returnwert die Position des ersten Listenelementes. Beachten Sie bitte, dass die Methode nur die Position und noch nicht den Eintrag selbst liefert. Um durch die Liste der Einträge durchzulaufen, rufen Sie die CStringList-Methode GetNext(...) auf. Sie erhält als Parameter die Position, dessen Eintrag Sie auslesen möchten. Als Returnwert liefert die Methode einen Zeiger auf eine Referenz auf den aktuellen Eintrag in der Liste (welche wunderbare Konstruktion!). Hört sich komplizierter an als es ist, Sie werden das nachher gleich sehen. Gleichzeitig setzt die Methode noch die Position in der Liste um einen Eintrag weiter, so dass Sie danach nur noch GetNext(...) aufrufen müssen um den jeweils nächsten Eintrag zu erhalten. Wurde das letzte Listenelement ausgelesen, so wird durch die Methode GetNext(...) die aktuelle Position auf NULL gesetzt. Damit können wir jetzt die Liste komplett auslesen.

Fügen wir jetzt den Code zum Auslesen der CStringList zu unserem Beispiel hinzu. Erweitern Sie dazu die Methode DoDataExchange(...) wie folgt:
void CLCEditDlg::DoDataExchange(CDataExchange* pDX)
{
    CDialog::DoDataExchange(pDX);
    //{{AFX_DATA_MAP(CLCEditDlg)
    ....
    //}}AFX_DATA_MAP
    if (pDX->m_bSaveAndValidate) // Datentransfer zur Anwendung
    {
        // das kommt noch
    }
    else
// Datentransfer von der Anwendung
    {
        // Abpruefen ob CStringList leer ist
        if (!m_CListboxString.IsEmpty())
        {
            // Position des ersten Listenelements holen
            POSITION ListPos = m_CListboxString.GetHeadPosition();
            // Liste solange durchlaufen, bis alle Eintraege
            // ausgelesen wurden
            while (ListPos != NULL)
            {
                // Listenelement auslesen
                CString      CText = m_CListboxString.GetNext(ListPos);
                // und zur Listbox hinzufuegen
                m_CApiListbox.AddString(CText);
            }
        }
    }
}

Wenn Sie nun das Beispiel übersetzen, so werden in der Listbox die von der Anwendung vorgegebenen Einträge dargestellt. Wenn Sie über den Button Übernehmen weitere Einträge zu Listbox hinzufügen, so erhält die Listbox bei Bedarf automatisch einen Scrollbar um durch die Einträge durchgehen zu können. Dies wird durch die standardmäßig gesetzte Eigenschaft Vertik. Bildlauflauf der Listbox erreicht.

Sehen wir uns jetzt den anderen Fall an, den Datentransfer vom Dialog zur Anwendung. Zuerst muss die Liste CStringList geleert werden; schließlich soll die Liste ja nur die Einträge enthalten, die aktuell in der Listbox vorhanden sind. Um eine CStringList Liste zu leeren wird deren Methode RemoveAll(...) aufgerufen. Danach sollte vor dem Transfer auch wieder abgeprüft werden, ob die Listbox überhaupt Einträge enthält. Dies erfolgt durch die bereits weiter oben beschriebene CListBox-Methode GetCount(...). Enthält die Listbox Einträge, kann mit der bereits bekannten CListBox-Methoden GetText(...) ein Eintrag nach dem anderen aus der Listbox auslesen und dann mittels der CStringList-Methode AddTail(...) zur Liste hinzufügen. Zum Schluss können Sie in der InitInstance(...) Methode der Anwendung alle aktuellen Einträge der Listbox noch im Debugger-Fenster ausgeben.

Versuchen nun wieder einmal selbst beim Schließen des Dialogs den Inhalt der Listbox in das CStringList Objekt zu kopieren und dann die darin enthaltenen Strings in der Methode InitInstance(...) des Anwendungsobjekts im Debuggerfenster mittels TRACE(...) auszugeben.

Lösung zum Listbox-DDX

Zuerst sehen wir uns an, wie der Dialog die aktuellen Listbox-Einträge in die CStringList umkopiert.

void CLCEditDlg::DoDataExchange(CDataExchange* pDX)
{
    ....
    if (pDX->m_bSaveAndValidate) // Datentransfer zur Anwendung
    {
        // CStringList leeren!
        m_CListboxString.RemoveAll();
        // Anzahl der Eintraege in der Listbox auslesen
        int  nNoOfEntries = m_CApiListbox.GetCount();
        // Falls Eintraege in der Listbox vorhanden sind
        if (nNoOfEntries != 0)
        {
            CString CText;
            // Eintraege in CStringList umkopieren
            for (int iLoop=0; iLoop<nNoOfEntries; iLoop++)
            {
                // Eintrag aus Listbox auslesen
                m_CApiListbox.GetText(iLoop,CText);
                // und zu CStringList hinzufuegen
                m_CListboxString.AddTail(CText);
            }
        }
    }
    else // Datentransfer von der Anwendung
    {
        ....
    }
}

Die Ausgabe der CStringList-Einträge in der Methode InitInstance(...) kann dann wie folgt erfolgen:

BOOL CLCEditApp::InitInstance()
{
    ....
    if (nResponse == IDOK)
    {
        // ZU ERLEDIGEN: Fügen Sie hier Code ein, um ein Schließen des
        //    Dialogfelds über OK zu steuern

        // Falls Listbox Eintraege enthalten hat
        if (!dlg.m_CListboxString.IsEmpty())
        {
            // Alle Eintraege ausgeben
            TRACE("Listbox Eintraege:\n");
            POSITION ListPos = dlg.m_CListboxString.GetHeadPosition();
            while (ListPos != NULL)
            {
                CString CText = dlg.m_CListboxString.GetNext(ListPos);
                TRACE("%s\n",CText);
            }
        }
        else
            TRACE("Listbox ist leer!\n");
    }
    ....
}

Ende der Lösung

Sie können das Beispiel nun übersetzen und starten. Die Listbox wird dann die gewünschte Funktionalität zeigen. Fügen Sie auch Einträge zur Listbox hinzu und sehen Sie sich dann die Ausgabe im Debugger-Fenster an.

Kommen wir nun zum letzten Element im Dialog, der Combobox. Comboboxen bestehen aus einem Edit-/Textfeld und einem Listenfeld, in dem die Einträge dargestellt werden. WINDOWS kennt drei Arten von Comboboxen:

Stil

Darstellung

Anmerkung

Einfach Einfache Combobox Das Listenfeld ist ständig sichtbar und das Editfeld ist mit diesem verbunden. Werden im Editfeld Eingaben vorgenommen, so wird das Listenfeld soweit durchgescrollt, das in diesem einer der Eingabe am nächsten kommende Eintrag sichtbar ist. Wird ein Eintrag ausgewählt, so wird er im Eingabefeld angezeigt. Neue Eingaben werden nicht automatisch ins Listenfeld übernommen.
Dropdown Dropdown-Combobox Wie einfache Combobox, nur dass das Listenfeld erst dann dargestellt wird, wenn der Dropdown-Pfeil (rechts im Eingabefeld) angeklickt wird. Wird ein Eintrag ausgewählt, so wird er im Editfeld angezeigt. Neue Eingaben werden nicht automatisch ins Listenfeld übernommen.
Dropdown-Liste Dropdown-Liste Das Listenfeld wird auch hier erst dann dargestellt, wenn der Dropdown-Pfeil angeklickt wird. Die Dropdown-Liste lässt keine Eingaben zu. Im Textfeld wird der aktuell ausgewählte Eintrag dargestellt.

Passen wir zunächst die Eigenschaften der am Anfang der Lektion erstellten Combobox unseren Anforderungen an. Im Beispiel soll innerhalb der Combobox aus einer vorgegebenen Anzahl von Dateitypen einer ausgewählt werden können. Hierzu wird eine Combobox vom Typ DropDown-Liste verwendet.

Geben Sie zuerst der Combobox die ID IDC_FILETYP.

Image3.gif (2350 Byte)

Als nächstes legen Sie die Größe des Listenfeldes fest. Öffnen Sie dazu den Eigenschaftsdialog der Combobox und wählen dann den Tabulator Formate aus. Stellen Sie als Typ zunächst Einfach ein. Nun können Sie durch ziehen am Rahmen die Größe der aufgeklappten Combobox einstellen.

Sie können die Größe des Listenfeldes nur dann einstellen, wenn Sie den Typ Einfach ausgewählt haben. Bei den beiden anderen Combobox-Typen ist dies nicht möglich!

Haben Sie Größe der Combobox entsprechend eingestellt, so stellen Sie dann den gewünschten Typ der Combobox ein, in unserem Beispiel als Dropdown-Liste. Wollen Sie nachher nochmals die Größe des Listenfeldes ändern, so müssen Sie zuerst wieder den Typ Einfach einstellen.

Um das Aussehen der Combobox testen zu können wechseln Sie in den Dialog Daten. Im Eingabefeld des Dialogs können Sie nun ein paar Einträge eingeben.

Combox-Einträge definieren

Achtung! Um einen neue Eintrag einzugeben drücken Sie auf keinen Fall die <RETURN>-Taste, dies würde den Eigenschaftsdialog schließen. Drücken Sie am Ende des Eintrags <STRG>-<RETURN> um zum nächsten Eintrag zu gelangen. Außerdem werden die hier angegebenen Einträge automatisch in die Combobox übernommen. Wollen Sie die Einträge per Programm erstellen, so wie im Beispiel, so müssen Sie die Einträge zum Schluss löschen!

Testen Sie nun das Aussehen der Combobox in dem Sie im Menü Layout den Menüpunkt Testen auswählen. Wenn alles zu Ihrer Zufriedenheit eingestellt ist, löschen Sie die Einträge im Dialog Daten und schließen dann den Eigenschaftsdialog.

Soweit zur Ressource-Eigenschaft unserer Combobox. Kommen wir nun zur programmtechnischen Handhabung. Comboboxen werden unter der MFC durch die Klasse CComboBox repräsentiert, die ebenfalls wieder von der Klasse CWnd abgeleitet ist. Wenn Sie in einem Dialog eine Combobox einsetzen, müssen Sie zur Dialogklasse ein entsprechendes CComboBox Objekt hinzufügen. Über dieses Objekt können Sie dann der Combobox Einträge hinzufügen. Rufen Sie dazu dessen Methode AddString(...) auf. Die Methode erhält als Parameter einen char-Zeiger auf den hinzuzufügenden Eintrag und liefert als Returnwert wieder die Position, an der der Eintrag eingefügt wurde. Soll ein bestimmter Eintrag der Combobox im Edit-/Textfeld dargestellt werden, so können Sie dies durch den Aufruf von SetCurSel(...) erreichen. Geben Sie der Methode die Position des Eintrags mit (0-basierend), die als Vorgabe verwendet werden soll.

Sie könnten jedem Combobox-Eintrag, genauso wie bei der Listbox, einen 32-Bit Wert zuordnen. Wenn Sie sich die Klasse CComboBox einmal ansehen werden Sie viele Methoden finden, die auch in der Klasse CListBox vorhanden sind. So liefert z.B. die Methode GetCount(...) auch die Anzahl der Einträge in der Combobox.

Aber initialisieren wir jetzt unsere Combobox.

Fügen Sie zunächst zur Dialogklasse das der Combobox zugeordnete CComboBox Objekt hinzu. Geben Sie dem Objekt den Namen m_CFileTyp. Beachten Sie dabei bitte, dass als Kategorie Control eingestellt ist.

Über dieses neu hinzugefügte Objekt kann die Combobox nun in der OnInitDialog(...) Methode initialisiert werden. Wir initialisieren hier die Combobox innerhalb des Dialogs und nicht vom Anwendungsobjekt aus, da der Dialog die anzuzeigenden Dateitypen vorgeben soll.

BOOL CLCEditDlg::OnInitDialog()
{
    ....
    m_CNonSelect.SetItemData(nIndex,MFC);

    // Combobox initialisieren
    m_CFileTyp.AddString("*.cpp");
    m_CFileTyp.AddString("*.h");
    m_CFileTyp.AddString("*.rc");
    m_CFileTyp.AddString("*.dsw");
    // Ersten Eintrag als Defaultwert vorgeben
    m_CFileTyp.SetCurSel(0);

    return TRUE; // Geben Sie TRUE zurück, außer ein Steuerelement soll....
}

Wenn Sie das Programm jetzt starten, so wird in der Combobox die Auswahlliste der möglichen Dateitypen angezeigt. Sollten Sie noch weitere Einträge sehen so liegt dies wahrscheinlich daran, dass Sie bei den Eigenschaften der Combobox die Daten noch nicht gelöscht haben.

Da wir im Beispiel keine Nachrichten von der Combobox verarbeiten müssen, werden wir auch gleich an den Datentausch mit der Anwendung gehen.

Bei Comboboxen der Typen Einfach und Dropdown könnten Sie z.B. die Nachricht CBN_EDITCHANGE verarbeiten die anzeigt, dass der Text im Editfeld der Combobox verändert wurde. Um für diese Nachrichten entsprechende Nachrichtenbearbeiter zu installieren, klicken Sie die Combobox mit der rechten Maustaste an und wählen aus dem Kontextmenü den Eintrag Ereignisse... aus.

Editfeld-Nachricht einer Combobox

Zum Schluss fehlt uns nur noch das Auslesen des aktuell eingestellten Datentyps innerhalb der Anwendung. Und hierbei lernen Sie auch gleich einen kleinen Kniff kennen, der Ihnen den Datentausch mit einer Combobox etwas erleichtert. Standardmäßig verwenden Comboboxen vom Typ Dropdown und Dropdown-Liste für den Datenaustausch eine int-Variable. Da uns aber nur der eingestellte Datentyp als String interessiert, werden wir uns um den Datenaustausch selbst kümmern. Um den aktuell eingestellten Combobox-Eintrag in einem CString-Objekt abzulegen, wird das Makro DDX_CBString(...) verwendet. Außer diesem DDX-Makro gibt es noch eine Vielzahl weiterer, die Sie der Online-Hilfe unter dem Stichwort DDX_xxx entnehmen können.

Bauen wir jetzt unseren DDX-Mechanismus für die Combobox jetzt ein.

Fügen Sie zunächst dem Dialog die public Membervariable m_CSelFiletyp mit dem Datentyp CString hinzu. Anschließend passen Sie die Methode DoDataExchange(...) noch wie folgt an:
void CLCEditDlg::DoDataExchange(CDataExchange* pDX)
{
    CDialog::DoDataExchange(pDX);
    //{{AFX_DATA_MAP(CLCEditDlg)
    ....
    //}}AFX_DATA_MAP
    // Unser eigener Datenaustausch!
    DDX_CBString(pDX, IDC_FILETYP, m_CFileString);
    if (pDX->m_bSaveAndValidate) // Datentransfer zur Anwendung
    ....
}

Zum Schluss kann die Anwendung dann nach dem Schließen des Dialogs diese Membervariable auslesen um den ausgewählten Datentyp auszuwerten. Erweitern Sie Methode InitInstance(...) der Anwendung dazu wie folgt.

BOOL CLCEditApp::InitInstance()
{
    int nResponse = dlg.DoModal();
    if (nResponse == IDOK)
    {
        ....
        TRACE("Ausgewählter Dateityp: %s\n",(LPCSTR)dlg.m_CFileString);
    }
    ....
}

Damit ist das Beispiel, und diese Lektion, (endlich) komplett und sie können es übersetzen. Das fertige Beispiel finden Sie unter 08Dialoge\LCEdit.

In der nächsten Lektion lernen Sie dann weitere Controls wie z.B. den Fortschrittsbalken oder auch den Schieberegler kennen.



Copyright © 2004

Senden Sie Emails mit Fragen oder Kommentaren zu dieser Website an: mailto:info@cpp-tutor.de
 Wolfgang Schröder, Lerchenweg 23, D-72805 Lichtenstein. Tel: +49 7129 6470