CTreeCtrl


Einleitung

Um diese Anleitung nicht zu lang werden zu lassen, schreibe ich immer nur die Dinge auf, die verändert werden müssen. Bei Codeabschnitten werden die Teile, die von mir verändert wurden fett geschrieben.

Beispiel

Aufgabe

Erstellen Sie ein Terminverwaltungsprogramm mit folgenden Eigenschaften:
  1. Ein Termin besteht aus einem Zeitpunkt (Tag, Monat, Jahr, Stunde, Minute) und einer Beschreibung.
  2. Die Terminverwaltung wird durch eine Anwendung, die beim Start einen Dialog startet, realisiert. Beim Verlassen des Dialoges ist die Anwendung zu beenden.
  3. Der zugehörige Dialog besteht aus einer Strukturansicht (TreeControl) und Feldern zur Darstellung eines Termins.
  4. Spezielle Textfelder für Datum und Uhrzeit lassen nur die Eingabe von speziellen Zeichenketten zu:
  5. Die Strukturansicht stellt Termine strukturiert dar:
  6. Bei Auswahl eines Termins durch "Anklicken" werden die Daten des Termins in den Textfeldern dargestellt.
  7. Ein Button "Eintragen" bewirkt, dass der in den Editfeldern beschriebene Termin in die Terminverwaltung übernommen wird. Bei Übernahme wird geprüft, ob die Eingabein den Textfeldern plausibel ist: Gegebenfalls wird eine Messagebox mit Fehlermeldung angezeigt.
  8. Die verwalteten Termine sollen auch serialisiert werden können.

Lösung in Prosa

  1. Zuerst einmal ein neues Projekt anlegen:
  2. Zeichnen des Dialogs:

    Um im CTreeCtrl die verbindenden Linien und die Symbole + und - zum Öffnen und Schließen eines Asts des Baumes müssen nur in den Eigenschaften des CTreeCtrls folgende Formate angekreuzt werden:
  3. Noch bevor man die Membervariablen für die Dialogelemente anlegt, müssen die speziellen Eingabefelder, die nur entweder ein Datum oder eine Zeit eingeben lassen erstellt werden. Dazu werden 2 neue Klassen erstellt, die von CEdit abgeleitet werden.
  4. Nun können die Membervariablen für die Elemente des Dialogs angelegt werden:
    Dazu müssen noch die Klassen für die speziellen Eingabefelder in die View inkludiert werden:

    termineView.h
    #include "DatumEdit.h"
    #include "ZeitEdit.h"
    


    Ich lege die Controlvariablen für das Datums- und das Zeiteingabefeld nur deshalb zusätzlich zu den Wertvariablen an, weil sonst Windows nicht weiß, dass es das spezielle Eingabefeld nutzen soll. Ich brauche sie sonst im ganzen Programm nicht mehr.
  5. Weil ich gern möchte, dass der Add Button nur dann aktiv ist, wenn die beiden Datums- und Zeiteingabefeldern gültige Werte enthalten, überschreibe ich 2 Methoden von termineView. Es sind jeweils Handler für die Nachricht EN_CHANGE der jeweiligen Eingabefelder.
    void CTermineView::OnChangeDatum() 
    {
    	// Sind die Inhalte der Eingabefelder gültig ?
    	if (m_datumctrl.isContentValid() && m_zeitctrl.isContentValid())
    		// Aktiviere Button
    		m_Add.EnableWindow(true);
    	else
    		// Deaktiviere Button
    		m_Add.EnableWindow(false);
    }
    
    void CTermineView::OnChangeZeit()
    {
    	// Sind die Inhalte der Eingabefelder gültig ?
    	if (m_datumctrl.isContentValid() && m_zeitctrl.isContentValid())
    		// Aktiviere Button
    		m_Add.EnableWindow(true);
    	else
    		// Deaktiviere Button
    		m_Add.EnableWindow(false);
    }
    
    Der Button muss zu Anfang des Programms deaktiviert sein. Dies kann in der Methode OnInitialUpdate der Klasser der View erreicht werden.
    void CTermineView::OnInitialUpdate()
    {
    	CFormView::OnInitialUpdate();
    	GetParentFrame()->RecalcLayout();
    	ResizeParentToFit();
    
    	// Button am Anfang deaktivieren
    	m_Add.EnableWindow(false);
    }
    
  6. Nun erstellt man am Besten gleich die Klasse, in der man einen Termin speichern kann und die man auch gleich serialisieren kann.
  7. Nun muss der Termin noch zum CTreeCtrl hinzugefügt werden. Da man aber bei jedem Einfügevorgang eines jeden einzelnen Items in das CListCtrl prüfen müsste, ob es das Element schon gibt, habe ich mir eine Methode geschrieben, die nach einem child mit dem angegebenen text von den angegebenen parent sucht. Wird es gefunden, kann man es einfach zurückgeben. Wenn nicht, erstelle ich es und gebe dann das neue Item zurück.

    CTermineView.cpp
    HTREEITEM CTermineView::getItem(CString text, HTREEITEM parent)
    {
    	// Diese Methode sucht nach einem Child des übergebenen Parent, das
    	// als angezeigten Namen text besitzt. Wenn es das Item findet wir es zurück
    	// gegeben. Sonst wird es neu angelegt.
    	// Die Methode dient dazu, zu verhindern dass Einträge, wie zb ein Jahr
    	// oder ein Monat etc. doppelt vorhanden sind.
    
    	// Hole erstes Child
    	HTREEITEM child = m_tree.GetChildItem(parent);
    
    	// Solange Child nicht gefunden wurde..
    	while (child != NULL)
    	{
    		// Ist es das gesuchte Item ?
    		if (m_tree.GetItemText(child) == text)
    			// Ja, dann zurückgeben
    			return child;
    		// Nächstes Child vom übergebenen parent holen,
    		// also Bruderelement von child.
    		child = m_tree.GetNextSiblingItem(child);
    	}
    	// Child nicht gefunden, also neu erstellen
    	child = m_tree.InsertItem(text,0,0,parent);
    	// lparam des Childs auf NULL setzen.
    	m_tree.SetItemData(child,(DWORD) NULL);
    	// Neues Child zurückgeben
    	return child;
    }
    
  8. Es folgt eine Methode, die einen gesamten Termin mittels der Methode getItem in CTreeCtrl einfügt.
    void CTermineView::AddTermin(int jahr, int monat, int tag, int stunde, int minute, CString text,DWORD data,bool show)
    {
    	// Diese Methode fügt einen Eintrag zum CTreeCtrl hinzu
    	CString zwsp;
    
    	// Zahl in String umwandeln
    	zwsp.Format("%4d",jahr);
    	// Element als Rootelement einfügen und merken
    	// TVI_ROOT .. Als Rootelement einfügen
    	HTREEITEM hjahr = getItem(zwsp,TVI_ROOT);
    
    	zwsp.Format("%2d",monat);
    	// als Child von hjahr einfügen
    	HTREEITEM hmonat = getItem(zwsp,hjahr);
    
    	zwsp.Format("%2d",tag);
    	HTREEITEM htag = getItem(zwsp,hmonat);
    
    	zwsp.Format("%2d",stunde);
    	HTREEITEM hstunde = getItem(zwsp,htag);
    
    	zwsp.Format("%2d",minute);
    	HTREEITEM hminute = getItem(zwsp,hstunde);
    
    	HTREEITEM htext = m_tree.InsertItem(text,0,0,hminute);
    	// im Datenfeld von htext, das bei jedem Item dazu gespeichert werden kann, die Adresse
    	// des Elements in der CObList merken.
    	m_tree.SetItemData(htext,(DWORD) data);
    	// Wenn es sichtbar werden soll..
    	if (show)
    		// .. dann anzeigen
    		m_tree.EnsureVisible(htext);
    }
    
  9. Nun kann man den Handler für den Button Add schreiben:
    void CTermineView::OnAdd() 
    {
    	// Dokument holen
    	CTermineDoc * doc = GetDocument();
    	// Neues Element für Termin erstellen
    	Element *elem = new Element();
    
    	// Einfügen in die Liste
    	// Holen der Daten der Dialogelemente in die Membervariable
    	UpdateData();
    	// Die Strings zerlegen, und in das neue Element schreiben
    	// Richtigkeit wird schon in den Eingabefeldern geprüft
    	sscanf(m_datum,"%2d.%2d.%4d",&elem->tag,&elem->monat,&elem->jahr);
    	sscanf(m_zeit,"%2d:%2d",&elem->stunde,&elem->minute);
    	elem->Text = m_text;
    
    	// Zur CObList hinzufügen
    	doc->list.AddTail(elem);
    
    	// Einfügen in den Dialog
    	AddTermin(elem->jahr,elem->monat,elem->tag,elem->stunde,elem->minute,elem->Text,(DWORD)elem,true);
    }
    
  10. Bei Klick auf den Beschreibungstext eines Termins im CTreeCtrl soll er die Daten in den Eingabefeldern anzeigen. Dazu muss nur der Handler für das Ändern der Auswahl der CTreeCtrl überschrieben werden.
    Ich wollte zuerst, den Handler On_Click verwenden, doch konnte man mit den Parametern, die diesem Handler von Windows übergeben nichts anfangen.
    Die Nachricht, für die der Handler geschrieben werden soll heißt TVN_SELCHANGED.
    void CTermineView::OnSelchangedTree(NMHDR* pNMHDR, LRESULT* pResult) 
    {
    	// Angeklicktes Item
    	NM_TREEVIEW* item = (NM_TREEVIEW*) pNMHDR;
    
    	// Existiert Item..
    	if (item->itemNew.hItem != NULL)
    	{
    		// Ist es das Element, das den Beschreibungstext enthält..
    		// Beim Hinzufügen eines Termins zu CTreeCtrl habe ich mir mit
    		// SetItemData den Wert von lParam gesetzt, aber nur beim Item,
    		// das den Beschreibungstext enthält.
    		if (item->itemNew.lParam != NULL)
    		{
    			CString zwsp;
    
    			// Zugehöriges Element holen
    			Element* elem = (Element*)item->itemNew.lParam;
    			// Variable der Dialogelemente setzen
    			m_text = elem->Text;
    			// String Datum aus den Einzelkomponenten zusammenstellen
    			zwsp.Format("%02d.%02d.%04d",elem->tag,elem->monat,elem->jahr);
    			m_datum = zwsp;
    			// String Zeit aus den Einzelkomponenten zusammenstellen
    			zwsp.Format("%02d:%02d",elem->stunde,elem->minute);
    			m_zeit = zwsp;
    			// Variable anzeigen
    			UpdateData(false);
    		}
    	}
    	
    	*pResult = 0;
    }
    
  11. Bei jedem Laden, müssen die Termin in die CTreeCtrl eingetragen werden.
    void CTermineView::OnInitialUpdate()
    {
    	CFormView::OnInitialUpdate();
    	GetParentFrame()->RecalcLayout();
    	ResizeParentToFit();
    
    	// Einfügen der Elemente in der CObjectList in das CTreeCtrl
    	// Wird beim Starten des Programms und beim Laden aufgerufen
    	CTermineDoc * doc = GetDocument();
    	Element *elem;
    	POSITION pos = doc->list.GetHeadPosition();
    
    	// Alle Elemente der CTreeCtrl löschen
    	m_tree.DeleteAllItems();
    	// Hinzufügen der geladenen Elemente in der CObList zum CTreeCtrl
    	while (pos != NULL)
    	{
    		elem = (Element*)doc->list.GetNext(pos);
    		AddTermin(elem->jahr,elem->monat,elem->tag,elem->stunde,elem->minute,elem->Text,(DWORD)elem,false);
    	}
    
    	// Button am Anfang deaktivieren
    	m_Add.EnableWindow(false);
    }
    
  12. Es fehlen noch die Bildchen vor den Einträgen der Termine:
  13. Fertig