9. Att arbeta med listor

 

 

·     Telefonlistan.

 

·     Klassen 'CTeleObjekt'.

 

·     Klassen 'CObList'.

 

·     DeleteContents().

 

·     Vyklassens variabler.

 

·     Koppla uppdateringar.

·     Variablerna - en överblick.

·     Knapparna '<' och '>'.

·     Knapparna 'Ny' och 'Ta Bort'.

·     Serialisering.

·     Kundanpassa.

 


Telefonlistan.

 

Programexemplet i detta kapitel kallar vi Telefonlistan. Vi förkortar namnet till 'Tele'. Programmet ska hantera en privat telefonlista där själva datat hanteras som en lista. Vi kommer inte att arbeta med en vanlig C-lista, till exempel 'Namn[25][100]', utan med en lista med objekt, och objekten bygger på en klass som vi själva skapar, klassen CTeleObjekt.

 

Objekten kommer vi att administrera med hjälp av ett speciellt listobjekt. Detta listob­j­ekt deriverar (skapar och ärver) vi från en klass som heter CObList. Denna klass innehåller funktioner för att hantera en lista av en typ liknande den vi med så stor möda ska­p­ade själva i kapitel 3 av C++ kursen.

 

Detta projekt kommer att kräva en hel del skrivarbete, och det är bara bra att kompilera och länka litet då och då, även om det vi lagt till inte kan testas för stunden. Det är som sagt lättare att hitta felen när man just skrivit koden, och inte har så mycket ny kod att leta i.

 

När vi så småningom är (nästan) klara ska det se ut så här:

 

           

 

Övningsuppgift:

·      Skapa projektet Tele.

·      Ange ‘Single document’ men byt ut vyklassen till CFormView.

·      Anpassa filnamnsillägget till ‘tel’ och ändra filtret till ‘Tele Filer (*.tel)’.

·      Behåll fyra ‘senaste filer’ (recent file list).

·      Anpassa dialogrutan enligt ovan.

·      Använd vårt vanliga sätt att ange namn på variabler och identifika­tioner. Text­ru­torna har medlemsvariabler knutna till sig. Knapparna '<' och '>' ska heta IDC_PREV respektive IDC_NEXT.

·      Kompilera, länka och kontrollera att det ser ut som det ska. Det fungerar dock inte än, det fixar vi i de kommande avsnitten.

·      Anpassa programfönstrets storlek på samma sätt som i föregående kapitlet.

 


Klassen 'CTeleObjekt'.

 

Vi deklarerar själva en klass som kan hålla ett namn och ett telefonnummer, 'CTeleObjekt'. På sedvanligt sätt används en headerfil TeleObj.h och en imple­mente­r­ings­fil kallad TeleObj.cpp. Vi skapar dem med 'new' i 'file'-menyn. I teleobj.h skriver vi in följ­ande:

 

// TeleObj.h

//

// Headerfil för klassen CTeleObjekt.

// Ett objekt av typen CTeleObjekt kan hålla namn och telefon-

// nummer för en person.

 

class CTeleObjekt : public CObject

{

public:

    CTeleObjekt();

    CString m_Namn;

    CString m_Nummer;

};

 

Observera att vi inte skapar klassen helt själva. Den bygger på den grundläg­gan­de klassen CObject. Detta för att vi ska få tillgång till bl.a. serialisering. Slå upp CObject i hjälpen och ta reda på vad vi får mer.

 

En viktig sak: Vi får inte ge konstruktorn något argument. Det skulle enligt vad som står i hjälpen ge en konflikt när man använder serialisering. Slå upp detta i hjälpen.

 

Som synes består våra tillägg till CObject av två saker: vi byter ut konstruktorn, och vi lägger till två variabler. Variablerna behöver vi till vårt data, och i kon­struk­torn vill vi initiera dem. Vi fortsätter i filen TeleObj.cpp:

 

// TeleObj.cpp

// Implementering av medlemsfunktioner i klassen CTeleObjekt.

 

#include "stdafx.h"

#include "teleobj.h"

 

// Konstruktor:

CTeleObjekt::CTeleObjekt()

{

    m_Namn = "";

    m_Nummer = "";

}

 

Glöm inte att TeleObj.cpp ska vara med i projektet, Tele.


Klassen 'CObList'.

 

Förkortningen står för Class Object List. Tanken är att vi ska skapa en lista med de objekt vi skapar från klassen CTeleObjekt. (Dessa objekt kommer att vara namnlösa och refereras till via medlemmar i CObList samt vanliga pekare.)

 

Till att börja med skapar vi ett objekt som vi kallar m_TeleLista. Detta objekt ska vara medlem av dokumentklassen, alltså öppnar vi TeleDoc.h och skriver in följ­ande under public (eller lägg in med hjälp av ClassView):

 

CObList m_TeleLista;

 

När vi skapar ett nytt dokument vill vi börja med ett tomt TeleObjekt i den m_Tele­Lista vi precis skapade. Vi använder 'new' för att reservera minne till ett nytt objekt av klassen CTeleObjekt. Observera att vi inte skapar ett namngivet obj­ekt, utan att vi i stället använder en pekare, här kallad m_pTeleObjekt. Den­na pekare kommer vi att använda mer senare.

 

Därefter initierar vi det nya objektets m_Namn och m_Nummer.

 

Listobjektet har ärvt ett antal metoder, en av dem heter AddHead() och den kan vi använda för att lägga till det nya teleobjektet i början av telelistan.

 

Det gör vi genom lägga till följande i funktionen CTeleDoc::OnNewDocument:

 

// Skapa ett objekt av klassen CTeleObjekt till vilket vi

// kan referera med pekaren pTeleObjekt:

CTeleObjekt* pTeleObjekt = new CTeleObjekt;

 

// Initiera det nya objektets medlemsvariabler:

pTeleObjekt->m_Namn = "";

pTeleObjekt->m_Nummer = "";

 

// Lägg till objektet i början av objektlistan:

m_TeleLista.AddHead(pTeleObjekt);

 

Vi har refererat till klassen 'CTeleObjekt', vilken deklarerats i filen teleobj.h. Därför skriver vi i början av filen TeleDoc.cpp, före #include "tele.h":

 

#include "TeleObj.h"

 

Har du inte kompilerat tidigare, så gör det nu. Leta upp och rätta de fel som du lyckats åstadkomma så här långt.

 

? CObject, CObject::CObject(), CObList, AddHead().


DeleteContents().

 

I dokumentklassen finns en funktion som heter DeleteContents(). Den måste vi byta ut (override) om vi ska kunna lägga till vår egen kod.

 

Slå upp 'CDocument' i hjälpen för MFC. Titta på 'Members'. Under rubriken 'Overridables - Public Members' finner vi 'DeleteContents'. Klicka på namnet och läs vad som står under 'Remarks'.

 

I dokumentklassen lägger vi till följande funktionsprototyp under protected: dir­ekt efter OnNewDocument() (du kan också använda ClassView):

 

virtual void DeleteContents();

 

Vi skriver in själva funktionen i slutet av filen teledoc.cpp. Objektet m_TeleLis­ta har ärvt en metod som heter IsEmpty() vilken vi kan anropa medan vi tar bort ett TeleObjekt i taget.

 

void CTeleDoc::DeleteContents()

{

    // Ta bort alla objekt tills IsEmpty() svarar TRUE:

    while(!m_TeleLista.IsEmpty())

    {

        delete m_TeleLista.RemoveHead();

    }

}

 

Den sista raden "delete m_TeleLista.RemoveHead()" kräver en närmare förklar­ing: Varför både 'delete' och 'RemoveHead'?

 

RemoveHead() är en metod i objektet m_TeleLista. Den tar bort det första obj­ek­tet i listan. Returvärde är adressen till objektet. Slå upp CObject::Remove­Head i hjälpen. Objektet frigörs alltså från listan, men förstörs inte. Nu hör objektet inte längre till listan, men det man skapat med 'new' ska tas bort med hjälp av 'delete'. Efter­som 'RemoveHead' returnerar adressen till objektet kan vi ta bort det med 'delete' på ovanstående sätt.

 

Än så länge så har vi bara tittat på dokumentets variabler, det vill säga där vi lagrar vårt data i minnet, respektive skriver det på eller läser från disken. Serialisering­en kommer vi till sist i detta kapitel. Nu är det dags att titta på de variabler som kommer att synas på bildskärmen, och sedan hur vi flyttar data fram och tillbaka mellan dokumentet och vyn.

 

? CDocument ® Public Members ® DeleteContents. CObject::RemoveHead.


Vyklassens variabler.

 

Använd Class Wizard till att deklarera medlemsvariablerna m_Namn och m_Nummer till de två textrutorna, om du inte redan gjort detta. (Har du
använt sedvanlig nomenklatur borde dessa heta IDC_NAMN respektive IDC_NUMMER.)

 

Förutom dessa två variabler behöver vi en pekare som vi kan använda att peka på m_TeleLista i dokumentet, och en variabel som kan ange aktuell position i m_Tele­Lista (vi ska ju kunna bläddra bland uppgifterna, objekten). m_Position ska vara av en speciell typ, POSITION, vilken redan finns deklarerad i CObList.

 

Deklarera variablerna i vy-klassen, under 'protected:' efter 'CTeleView();'.

Varför under 'protected:'? Vi kommer endast att använda dessa inifrån vy-klas­sen, och det som inte behöver vara publikt ska helst inte vara det.

 

Precis som i föregående kapitel behöver vi också byta ut funktionen 'OnInitial­Update', och det kan ClassWizard fixa åt oss. Lägg även till de två variablerna under ‘attributes - protected:’

 

POSITION m_Position;

CObList* m_pTeleLista;

 

Därefter öppnar vi vyns implementeringsfil och lägger till kod i OnInitialUpdate(). Så här ska den se ut när vi är färdiga:

 

void CTeleView::OnInitialUpdate()

{

    CFormView::OnInitialUpdate();

        

    // TODO: Add your specialized code here and/or

    // call the base class

    // Ställ in fönstrets storlek:

    GetOwner()->SetWindowPos(&wndTop, 50, 50, 290, 136, NULL);

 

    // Skapa en pekare som pekar på dokumentet:

    CTeleDoc* pDoc = (CTeleDoc*)GetDocument();

 

    // Hämta adressen till objektlistan TeleLista i dokumentet:

    m_pTeleLista = &(pDoc->m_TeleLista);

 

    // Ställ in m_Position till att peka på första objektet.

    m_Position = m_pTeleLista->GetHeadPosition();

 

    // Hämta namn och nummer från dokumentet och lagra i vyn:

    CTeleObjekt* pTeleObjekt = (CTeleObjekt*)m_pTeleLista->

                               GetAt(m_Position);

    m_Namn = pTeleObjekt->m_Namn;

    m_Nummer = pTeleObjekt->m_Nummer;

 

    // Visa de nya värdena på skärmen:

    UpdateData(FALSE);

 

    // Positionera markören till namnfältet:

    ((CDialog*) this)->GotoDlgCtrl(GetDlgItem(IDC_NAMN));

}

 

Det som kommer efter SetWindowPos() kräver kanske en närmare förklaring:

 

Först skaffade vi oss en pekare 'pDoc' till dokumentet på sedvanligt sätt. Sedan använde vi denna till att peka på CObList-objektet m_TeleLista: pDoc->m_Te­le­Lista. När vi sedan satte '&' framför det hela så fick vi adressen till m_Tele­Lis­ta, vilken vi lagrade i m_pTeleLista, som sig bör.

 

Tredje steget gick ut på att använda m_pTeleLista för att peka på en medlems­funktion i m_TeleLista: GetHeadPosition(). Därigenom ställer vi in värdet i m_Position.

 

På samma sätt använder vi sedan medlemsfunktionen GetAt() med m_Position som argument, för att hämta värdet till en pekare 'pTeleObjekt', vilken samtidigt deklareras.

 

Sedan använder vi denna pekare till att hämta värdena till m_Namn och m_Num­mer från dokumentet.

 

När vi visat data på skärmen kommer den sista nyheten: man kan positionera sig bland textrutorna genom att använda funktionen GoToDlgCtrl(). Vi skaffar oss rätt kontroll med hjälp av GetDlgItem() som vi känner till från tidigare.

 

Nu har vi använt referenser till klasserna CTeleObjekt och CTeleDoc. Därför måste vi inkludera dem i början av filen. Efter satsen #include "Tele.h" skriver vi in följande:

 

#include "TeleObj.h"

#include "TeleDoc.h"

 

Det vore inte helt fel att kompilera och länka, bara för att se hur många stavfel som smugit sig in i koden (eller grövre fel, om nu sådant kan förekomma). Testa inte än, det finns en hel del kvar.

 

 

? GetHeadPosition(), GetAt(),GoToDlgCtrl(),GetDlgItem().


Koppla uppdateringar.

 

På samma sätt som i föregående kapitel måste vi nu se till att de ändringar som görs i vyn återspeglas i dokumentet. När man ändrar något på skärmen ska dok­u­mentets variabler också ändras.

 

Vi skapar en funktion med hjälp av Class Wizard som reagerar på EN_CHANGE för respektive textruta. Glöm inte att välja vy-klassen först. Till exempel IDC_NAMN får en funktion enligt nedan:

 

void CTeleView::OnChangeNamn()

{

    // Hämta data från skärmen:

    UpdateData(TRUE);

 

    // Skapa en pekare som pekar på dokumentet:

    CTeleDoc* pDoc = (CTeleDoc*)GetDocument();

    pDoc->SetModifiedFlag();

 

    // m_pTeleLista är ifylld från initieringen.
    // Skapa en pekare mot rätt objekt i listan:

    CTeleObjekt* pTeleObjekt =

           (CTeleObjekt*)m_pTeleLista->GetAt(m_Position);

 

    // Överför variabelns data till dokumentet:

    pTeleObjekt->m_Namn = m_Namn;

}

 

Gör på samma sätt för nummer-fältet.

 

Nu kan det vara lämpligt att kompilera och länka igen, bara för att kontrollera vilka stavfel vi gjort. Starta inte programmet, det är ändå ingenting nytt som fungerar än. Först ska vi fixa till knapparna.

 

Innan vi gör det skall vi dock försöka överblicka det vi åstadkommit.


Variablerna - en överblick.

 

De variabler vi skapat hittils, och deras respektive sammanhang kan åskådlig­göras enligt nedanstående skiss:

 

 

 

 

 

 

 


Dokumentet                        Vyn

CTeleObjekt

 
 

 

 


m_Namn

m_Nummer

 

m_pTeleLista

m_Position

 
                                   

 

   

m_Namn

m_Nummer

 
 

 

 


 

 

                                          

 

 


                                          

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 


Leta upp de funktioner i källkoden som utför respektive koppling och initierar pekarna i ovanstående bild, och skriv in dem i bilden vid respektive pil.


Knapparna '<' och '>'.

 

Som du kanske minns kallade vi knapparna '<' och '>' för IDC_PREV respek­tive IDC_NEXT. Nu ska vi lägga till kod som utförs när man klickar på dessa. Som vanligt skapar vi funktioner med hjälp av Class Wizard.

 

Föregående objekt (<):

 

Man använder en metod som heter GetPrev() för att hitta föregående objekt i listan. Denna returnerar en pekare till objektet. Om man redan står på första objektet kan man inte komma längre. Då returnerar GetPrev() värdet noll. Detta kan man testa på och till exempel skriva ut en meddelanderuta.

 

När nu GetPrev returnerar värdet noll får vi inte tappa bort pekaren till det förs­ta objektet. Därför arbetar vi med en temporär pekare, också den av typen POSI­TION.

 

Därefter kopieras objektets data till vy-variablernas data och vidare till bild­skär­men enligt tidigare modell. Till sist sätter vi markören i namnfältet.

 

Detta borde resultera i följande funktion:

 

void CTeleView::OnPrev()

{

    POSITION temp_Position;

    temp_Position = m_Position;

    m_pTeleLista->GetPrev(temp_Position);

 

    if(temp_Position == NULL)

    {

        AfxMessageBox("Detta är första namnet i listan!",

            MB_ICONEXCLAMATION);

    }

    else

    {

        m_Position = temp_Position;

        CTeleObjekt* pTeleObjekt =
            (CTeleObjekt*)m_pTeleLista->GetAt(m_Position);

        m_Namn   = pTeleObjekt-> m_Namn;

        m_Nummer = pTeleObjekt-> m_Nummer;

        UpdateData(FALSE);

    }

    ((CDialog*)this)->GotoDlgCtrl(GetDlgItem(IDC_NAMN));

}

 

Gör på motsvarande sätt för '>'-knappen. Kompilera och länka bara för att hitta var du skrivit fel. Testa inte programmet än, vi måste fixa lite till.


Knapparna 'Ny' och 'Ta Bort'.

 

När man trycker på 'Ny' ska ett nytt TeleObjekt skapas och läggas sist i listan. Ta Bort ska ta bort aktuell post. Använd Class Wizard:

 

void CTeleView::OnNy()

{

    // Töm variablerna och skärmen:

    m_Namn = "";

    m_Nummer = "";

    UpdateData(FALSE);

 

    // Skapa ett nytt tomt TeleObjekt:

    CTeleObjekt* pTeleObjekt = new CTeleObjekt;

    pTeleObjekt->m_Namn = m_Namn;

    pTeleObjekt->m_Nummer = m_Nummer;

 

    // Lägg det nya objektet sist i listan och lagra
    // dess position i m_Position:

    m_Position = m_pTeleLista->AddTail(pTeleObjekt);

 

    // Sätt Modified-flaggan i dokumentet till TRUE:

    CTeleDoc* pDoc = (CTeleDoc*)GetDocument();

    pDoc->SetModifiedFlag();

 

    // Sätt markören i namnfältet:

    ((CDialog*)this)->GotoDlgCtrl(GetDlgItem(IDC_NAMN));

}

 

void CTeleView::OnTabort()

{

    // Skaffa en temporär pekare. Originalet förstörs:

    CObject* pTemp = m_pTeleLista->GetAt(m_Position);

 

    // Tag bort objektet ur listan (m_Position förstörs):

    m_pTeleLista->RemoveAt(m_Position);

 

    // Tag bort objektet ur minnet:

    delete pTemp;

 

    // Om listan är helt tom lägger vi in ett blankt objekt:

    if(m_pTeleLista->IsEmpty()) OnNy(); // Eller hur?

 

    // Sätt Modified-flaggan i dokumentet till TRUE:

    CTeleDoc* pDoc = (CTeleDoc*)GetDocument();

    pDoc->SetModifiedFlag();

 

    // Var ska vi nu positionera oss? Egentligen skulle vi

    // först letat upp föregående objekt. Nu nöjer vi oss med

    // att hoppa till första objektet i listan:

    OnInitialUpdate(); // Samma här: Eller hur?

}


Nu, kan vi testa. Men först ska vi kompilera och länka (och rätta alla fel). Om allt står rätt till ska vi kunna skriva in namn och telefonnummer, lägga till dy­lika objekt, samt ta bort dem. Så här kan det se ut när man skriver in ett namn och telefonnummer:

 

 

 

           

 

Övningsuppgift:

·      Öppna projektet Tele om det inte redan är öppet.

·      Gör ovanstående tillägg.

·      Testa att lägga in några poster.

·      Testa att bläddra. Bläddra förbi gränserna och kontrollera så att det fungerar.

·      Testa att ta bort poster.

 

 

 

Vi kan dock inte lagra våra uppgifter på disken ännu, men det tar vi i sista avsnittet.


Serialisering.

 

Klassen CObList är ju en klass som hanterar en lista med objekt. Den har även stöd för att serialisera alla objekt i listan, ett i taget. Det går till så att ett objekt av klas­sen CObList anropar serialisering av ett objekt i taget. Den anropar alltså ob­jektets egen serialiseringsfunktion. Teleobjekten måste vara förberedda att seri­aliseras via sitt listobjekt, i stället för att vi serialiserar på vanligt sätt.

 

Vi gör följande tillägg i klassen CTeleObjekt:

 

Använd makrot DECLARE_SERIAL för att implementera serialiseringskod i klassen, och byt ut funktionen Serialize();

 

­// TeleObj.h

//

// Headerfil för klassen CTeleObjekt.

// Ett objekt av typen CTeleObjekt kan hålla namn och telefon-

// nummer för en person.

 

class CTeleObjekt : public CObject

{

 

    // Tillägg som genererar kod för serialisering:

    DECLARE_SERIAL(CTeleObjekt);

 

public:

    CTeleObjekt();

    CString m_Namn;

    CString m_Nummer;

 

    // Byt ut funktionen Serialize():

    virtual void Serialize(CArchive& ar);

};

 

 

 

 

 

 

 

 

 

 

 

 

? DECLARE_SERIAL.


Ändra i TeleObj.cpp så att det ser ut så här:

 

// TeleObj.cpp

// Implementering av medlemsfunktioner i klassen CTeleObjekt.

 

#include "stdafx.h"

#include "teleobj.h"

 

// Tillägg som genererar kod för serialisering:

IMPLEMENT_SERIAL(CTeleObjekt, CObject, 0);

 

// Constructor:

CTeleObjekt::CTeleObjekt()

{

    m_Namn = "";

    m_Nummer = "";

}

 

// CTeleObjekts Serialize()-funktion:

void CTeleObjekt::Serialize(CArchive& ar)

{

    if(ar.IsStoring())

    {

        ar << m_Namn << m_Nummer;

    }

    else

    {

        ar >> m_Namn >> m_Nummer;

    }

}

 

Detta var den kod som serialiserar ett objekt. Vi behöver anropa den för alla objekt i listan. Detta gör vi genom att helt enkelt anropa serialiserings­funk­tionen i objektet m_TeleLista från dokumentets serialiseringsfunktion.

 

Eftersom CTeleObjekt avgör huruvida vi läser eller skriver i vår serialiserings­funk­tion ska vi här lägga vår kod utanför if-satsen:

 

void CTeleDoc::Serialize(CArchive& ar)

{

    if(ar.IsStoring())

    {

        // TODO: add storing code here

    }

    else

    {

        // TODO: add loading code here

    }

 

    // Serialisera hela m_TeleLista:

    m_TeleLista.Serialize(ar);

}


Kundanpassa.

 

Som vi såg i förra kapitlet så har AppWizard fixat det mesta åt oss. Vi kanske vill att ett nytt dokument heter ‘Namnlös’, och det vet vi ju hur man gör. Vi struntar i att fixa till filvalsdialogrutan denna gång, men testa att dubbelklicka på en fil du skapat med Tele. Man får automatiskt upp programmet.

 

Så här kan det se ut om vi till exempel skapat en telefonlista för firmor, stäng Tele, och senare dubbelklickar på denna ikon:

 

 

 

 


           

 

 

 

 

 

 

 

Övningsuppgift:

·      Lägg till detta sista och testa.