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 listobjekt 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 skapade 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 identifikationer. Textrutorna 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 implementeringsfil kallad TeleObj.cpp. Vi skapar dem med 'new' i 'file'-menyn. I teleobj.h skriver vi in följande:
// 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äggande 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 konstruktorn 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öljande 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_TeleLista 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 objekt, utan att vi i stället använder en pekare, här kallad m_pTeleObjekt. Denna 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: direkt 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_TeleLista 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örklaring: Varför både 'delete' och 'RemoveHead'?
RemoveHead() är en metod i objektet m_TeleLista. Den tar bort det första objektet i listan. Returvärde är adressen till objektet. Slå upp CObject::RemoveHead 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'. Eftersom '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.
Serialiseringen 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_TeleLista (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-klassen, 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 'OnInitialUpdate', 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_TeleLista. När vi sedan satte '&' framför det hela så fick vi adressen till m_TeleLista, 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 medlemsfunktion 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_Nummer 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 dokumentets 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ådliggö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 respektive 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örsta objektet. Därför arbetar vi med en temporär pekare, också den av typen POSITION.
Därefter kopieras objektets data till vy-variablernas data och vidare till bildskärmen 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 dylika 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 klassen CObList anropar serialisering av ett objekt i taget. Den anropar alltså objektets egen serialiseringsfunktion. Teleobjekten måste vara förberedda att serialiseras 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 serialiseringsfunktionen i objektet m_TeleLista från dokumentets serialiseringsfunktion.
Eftersom CTeleObjekt avgör huruvida vi läser eller skriver i vår serialiseringsfunktion 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.