8.
CArchive
· Notisar.
· Variabler i dokumentklassen.
· Initiera vyvariabler.
· Uppdatera dokumentvariabler.
· Spara / Öppna.
· Kundanpassning.
· CFileDialog.
· CFile.
· Initiera dokumentet.
Notisar.
I detta kapitel ska vi lära oss spara och hämta data från filer. Tidigare har vi använt oss av diverse variabler, men de har alla haft en sak gemensamt: när man stänger programmet så glöms alla variabler bort. Detta är helt ok för t.ex. 'kalkylatorn', som du finner under 'tillbehör', men de flesta program behöver spara sina data på disk.
Vi ska prova att göra ett litet program för noteringar, kallat Notisar. Meningen är att varje notis ska sparas i en egen fil med filnamnstillägget 'not'. Lämpligt vore det att ha ett dialogrutebaserat projekt, eftersom vi inte behöver mer, och vill slippa ha ett oanvänt MainFrame i bakgrunden. När vi väljer ‘Dialog based’ på AppWizards första sida får vi dock ett projekt som varken har dokument eller arkivmeny.
Om vi i stället provar den metod som går ut på att beställa ett vanligt ‘single document’-projekt, och byta vyklass på sjätte sidan, får vi ju såväl dokument som arkivmeny. Men vi ska först anpassa litet till på sidan fyra, där vi hittar en knapp ‘Advanced...’, vilken leder till dialogrutan ‘Advanced Options’:
Här kan vi i fältet ‘File extension:’ ange att vi vill ha filnamnstillägget ‘not’. Vi måste även se upp med en annan sak: kompatibiliteten med tidigare filhantering med 8 tecken i filnamnet kräver att vissa namn fortfarande är tillräckligt korta. Man reserverade två tecken för olika märkningar av filnamnet, och kvar blev bara sex tecken. Detta slår igenom på de s.k. ‘korta’ namnen, vilket vi i de flesta fall kan acceptera, men inte när detta sedan används till att generera filterbeskrivningen till filvalsdialogrutan. Lägg alltså märke till att det står ‘Notisa Files (*.not)’ i fältet ‘Filter name:’. Rätta detta skönhetsfel enligt ovanstående, och översätt samtidigt ordet ‘Files’ till svenska.
Skulle du vilja ändra programnamnet i namnlisten, så är det här man gör det, i fältet ‘Mainframe caption’, men ‘Notisar’ passar utmärkt för vårt lilla program.
Glöm inte att byta vyklass till CFormView på sista sidan! Redigera sedan dialogrutan IDD_NOTISAR_FORM enligt nedanstående. Vi behöver fyra textrutor och tre rubriker:
Observera att den stora rutan är en vanlig textruta, som vi ändrar i egenskaperna till att vara flerradig. Du kan själv bestämma hur du vill att den ska reagera på att man skriver utanför rutan, se egenskaperna:
· Multiline - ger oss en textruta där man kan byta rad med hjälp av <Enter>, om ‘Want Return’ förbockats.
· Want Return - gör att <Enter> inte betyder tryckning på aktuell knapp. Det blir i stället ny rad.
· Vert Scroll - ger oss en vertikal rullningslist.
· Auto HScroll - Scrollar automatiskt i horisontalled när texten inte får plats, så att markören alltid är synlig.
? Tryck på egenskapsrutans hjälpknapp och läs om de olika
egenskaperna:
Använd följande id för de olika textrutorna:
· IDC_DATUM
· IDC_TID
· IDC_AMNE
· IDC_TEXT
Nu går det bra att kompilera, länka och testa så att textrutan fungerar som den ska. Tänk bara på att vi inte skrivit någon kod för att läsa eller skriva på disken ännu, så använd inte de funktionerna. Så här ser det ut:
Variabler i dokumentklassen.
Vi har flera gånger deklarerat variabler i vy-klassen. I kapitel 2 lärde vi oss dock att deklarera en variabel i dokumentklassen. God programmering kräver att var sak har sin plats, och detta gäller även variablerna.
· Vyn är det vi ser på skärmen. Variabler som ska visas här deklareras i vy-klassen. Dessa variabler ska dock endast betraktas som 'det vi kan se på skärmen'. Här har vi nytta av UpdateData().
· Objektet är det vi arbetar med. Här ska vi egentligen ha våra variabler. När vi nu ska börja spara dem på disk, är det lika bra att vi betraktar våra variabler gemensamt som ett dokument, så att vi kan utnyttja dokumentklassens funktioner direkt på dem.
I detta fall har vi alltså variabler som hör hemma på båda ställen. Detta är fullt normalt för många program, dock är det inte lika vanligt att hela dokumentet syns i vyn samtidigt, och det är just det vi har nu. Därför deklarerar vi exakt samma uppsättning variabler för dokumentet som för vyn.
Vy-klassens variabler fixar Class Wizard till oss. Lägg in följande variabler knutna till motsvarande kontroller (alla ska vara av typen CString):
· m_Datum
· m_Tid
· m_Amne
· m_Text
Dokument-klassens variabler får vi skriva själva, eftersom de inte är knutna till några kontroller. Skriv in följande i NotisarDoc.h under public (eller kopiera dem från NotisarView.h):
CString m_Datum;
CString m_Tid;
CString m_Amne;
CString m_Text;
(Vi kan mycket väl använda samma namn i bägge klasserna, de kvalificeras ju av att de deklareras i olika klasser.)
Dessa nya variabler i dokumentet måste naturligtvis initieras när man skapar ett nytt dokument. Detta sker dels när vi startar programmet, dels när vi väljer [File - New]. Det finns redan en funktion som gör detta: CNotDoc::OnNewDocument. Vi det här laget kan du säkert hitta den på egen hand. Initiera variablerna till tomma strängar ("").
Initiera vyvariabler.
Nu behöver vi kopiera över dokumentets variabler till vyn, direkt vid initiering. Vi ska alltså inte bara initiera dem till tomma strängar, för då är det möjligt att vi får en missanpassning när vi bestämmer oss för att initiera till något annat i framtiden.
När vi emellertid letar efter meddelandet WM_INITIALUPDATE upptäcker vi att ClassWizard inte riktigt var med på noterna när vi bytte vyklassen till CFormView. Meddelande saknas. I stället finns WM_INITDIALOG. ClassWizard har alltså lagt upp vår vy som en dialogruta i sin databas. Det är naturligtvis inte helt bra, men i detta enkla exempel påverkar det bara det här med initieringen, och det löser vi här. Vi blir tvungna att själva byta ut CView:s OnInitialUpdate(). Den som inte minns exakt hur detta går till slår naturligtvis upp det i hjälpen. Sedan kan man kopiera funktionsprototypen till vyns klassheader:
// Operations
public:
// Overrides
virtual
void OnInitialUpdate( );
...
Vi skriver själva funktionen i filen NotisarView.cpp under kommentaren 'CNotView message handlers'. I denna skaffar vi oss en pekare mot dokumentet, kopierar variablerna samt anropar UpdateData(), så att de kopierade värdena syns på skärmen. Ovanstående bör du vid det här laget kunna skriva på egen hand, men om det skulle bli fel kan du tjuvtitta här nedan:
void CNotisarView::OnInitialUpdate( )
{
CNotisarDoc* pDoc = (CNotisarDoc*) GetDocument();
m_Datum
= pDoc->m_Datum;
m_Tid = pDoc->m_Tid;
m_Amne = pDoc->m_Amne;
m_Text = pDoc->m_Text;
UpdateData(FALSE);
}
Nu har vi kod som initierar dokument-klassens variabler samt kod som kopierar dessa till vy-klassens motsvarande variabler. Skulle vi vilja ändra initieringen, så kan vi göra detta i dokumentet, och ändringarna slår igenom hela vägen automatiskt.
Uppdatera dokumentvariabler.
Varje gång vi ändrar något på skärmen kommer motsvarande textrutas medlemsvariabel att ändra innehåll. Detta måste vi kopiera över till dokumet-klassens motsvarande variabel.
Det finns ett meddelande som heter EN_CHANGE för var och en av textrutorna. Skapa en funktion för varje textrutekontroll med hjälp av Class Wizard. I funktionerna kopieras kontrollens variabel till dokumentets motsvarande variabel. Det ska bli fyra funktioner. Som exempel ser vi här hur vi kopierar över variabeln m_Datum:
void CNotisarView::OnChangeDatum()
{
// Hämta data från skärmen till vy-variabeln:
UpdateData(TRUE);
// Skapa en pekare mot dokumentet:
CNotisarDoc* pDoc = (CNotisarDoc*) GetDocument();
// Kopiera variabelns innehåll till dokumentvariabeln:
pDoc->m_Datum = m_Datum;
// Tala om för dokumentet att variabeln har ändrats:
pDoc->SetModifiedFlag();
}
De tre första stegen är sådant som vi diskuterat tidigare, men det fjärde är en nyhet: Dokumentklassen har en flagga som håller reda på om någonting ändrats i dess variabler. Man måste själv sätta flaggan med hjälp av SetModifiedFlag(), men det vi får är värt det: När man försöker stänga dokumentet påpekar programmet automatiskt att vi inte sparat våra ändringar:
Känns den igen?
Spara / Öppna.
MFC innehåller en speciell klass för hantering av skrivning/läsning av data på disk eller annat lagringsmedia: CArchive. Man kallar denna typen av databehandling för serialisering (serialization). Det handlar om att lagra data som en sekvens av tecken.
När man skapar projekt med hjälp av App Wizard, så som vi gjort hela tiden, får man automatiskt menyval i File-menyn som ansluter till CArchive. Dessa menyval är inte skuggade, alltså finns det redan en funktion i vårt program för läsning och skrivning.
Det kan vara på sin plats att redigera menyerna nu, när vi vet vad vi vill ha kvar. Ta bort ‘Edit’-menyn och översätt övriga. Alt-snabbtangenterna skiljer sig naturligtvis från de engelska, och man bör titta i ett program som följer standard, till exempel MS-Word 6.0, och göra likadant:
När du redigerar menyval som har Ctrl-snabbtangenter (‘accelerators’), så högetjusteras dessa med hjälp av ansikoden för tabulator, ‘\t’, vilket du kan se i original ‘caption’ för de menyval som redan har sådana.
Åter till spara och öppna. Det finns som sagt en funktion i dokumentet som hanterar detta och den heter Serialize(). Det är bara en funktion, men en funktion i CArchive, IsStoring(), om för Serialize() huruvida den ska läsa eller skriva data. Grundvalen för detta sattes automatiskt när vi gjorde vårt menyval.
Funktionen ser ut så här:
///////////////////////////////////////////////////////////////
// CNotisarDoc serialization
void CNotisarDoc::Serialize(CArchive& ar)
{
if
(ar.IsStoring())
{
//
TODO: add storing code here
}
else
{
//
TODO: add loading code here
}
}
Som synes är det lätt att hitta var vi ska skriva in vår kod.
Själva filen representeras av ett objekt 'ar' vilket skapats automatiskt åt oss. Den ärver från klassen CArchive och har likheter med klassen ios: man kan använda strömningsoperatorerna för att skicka data mellan objektet och våra variabler. Så här lätt skriver vi våra variabler till filen:
// TODO: add storing code here
ar << m_Datum;
etc...
Lika lätt är det att läsa: man bara vänder på strömningsoperatorn. Observera att man måste läsa och skriva variablerna i samma ordning (inte omvänt). Så här ser det ut när det är färdigt:
void CNotisarDoc::Serialize(CArchive& ar)
{
if (ar.IsStoring())
{
ar << m_Datum;
ar << m_Tid;
ar << m_Amne;
ar << m_Text;
}
else
{
ar >> m_Datum;
ar >> m_Tid;
ar >> m_Amne;
ar >> m_Text;
}
}
? CArchive.
Övningsuppgift:
· Skapa projektet Notisar.
· Programmet ska vara ‘single document’.
· Filnamnstillägg för dokument ska vara ´not’ och filter ska heta ‘Notisar Filer’.
· Välj vyklass ‘CFormView’.
· Anpassa dialogrutan såsom angivits ovan.
· Redigera menyerna enligt ovan.
· Skriv all kod som diskuterats ovan.
· Testa att spara och hämta ett par notisar i olika filer.
Kundanpassning.
Nu fungerar ‘Nytt’, ‘Spara’, ‘Spara som...’ och ‘Öppna’. Men när vi vill spara upptäcker vi att inte allt är översatt. Detta beror av att vi inte fått svenska textsträngar med AppWizard. Dialogrutans titelrad är på engelska, och det föreslagna filnamnet är ‘Untitled’:
Det är också litet trist att Microsoft levererar AppWizard utan svenska texttabeller. Det innebär en del extra arbete att översätta menyval etc.
Om vi tittar bland CDocument:s medlemmar finner vi en som heter SetTitle(). Den kan vi anropa när vi har ett nytt dokument, det vill säga på samma ställe där vi initierade variablerna till tomma strängar. Svensk de factostandard anger att man kallar ett ännu icke namngivet dokument ‘Namnlös’.
När vi använder filvalsdialogrutan finner vi att den har engelska titlar, olika beroende om vi sparar, sparar som eller öppnar. Denna dialogruta finns färdigkompilerad på CD-skivan, och vi kan inte göra mycket åt den.
Däremot kan man själv skapa ett objekt av typen CFileDialog, och där kan man komma åt att ändra titeln via OpenFileName.lpstrTitle, men det går utanför detta avsnitts målsättning.
När vi testar programmet upptäcker vi även att programfönstret inte är anpassat efter de kontroller vi lade in i dialogrutan. Detta skedde ju automatiskt när man genererade ett projekt av typen ‘Dialog based’. Vi kan dock själva ställa in en lämplig storlek och eventuellt anpassa var på skärmen fönstret ska visas vid programstart. Det finns en funktion i CWnd som kan anpassa fönstrets storlek, placering på skärmen samt placering i z-ordning, det vill säga om det ligger överst, underst eller mellan andra fönster på skärmen. Vi vill naturligtvis inte ändra på att det ligger överst. Funktionen heter SetWindowPos(), men den har en liten egenhet: den kan inte påverka fönstret för det objekt som funktionen kallas för. Den kan endast anropas för att påverka ett fönsterobjekt som är ‘child’ till, eller som ägs av ett annat objekt. Om det ägande objektet är ett fönster, så är detta fönster ett ‘child’ annars är det ‘ägt’ av förälderobjektet.
Det är emellertid lämpligt att anropa SetWindowPos() i OnInitialUpdate(), och då anropas funktionen från samma objekt som den avser att påverka, vilket den inte kan. Därför måste vi fråga efter adressen till föräldern eller ägaren för att anropa SetWindowPos() för det objektet i stället, så kan den ändra storleken på vårt fönster. I detta fall är föräldern även ägare, så vi kan välja vilken funktion som helst av de två, vilka heter GetParent() respektive GetOwner(). (Vad hade du väntat dig, det är ju bara att översätta till engelska så har man funktionsnamnen.)
Således kan vi komplettera OnInitialUpdate() med följande rad:
GetOwner()->SetWindowPos(&wndTop, 50, 50, 419, 318, NULL);
Siffrorna kan naturligtvis variera beroende på hur stor du gjorde dialogrutan som visas i vyn, så ovanstående resultat är baserat på diverse experimenterande. Angående &wndTop så rekommenderar jag ett mera ingående studium av hjälpen, där funktionen SetWindowPos i klassen CWnd diskuteras mer i detalj.
Övningsuppgift:
· Öppna projektet Notisar om det inte redan är öppet.
· Ändra så att filnamnet för ett nytt dokument är ‘Namnlös’.
· Anpassa storleken på programfönstret så att det passar lagom till kontrollerna.
? SetWindowPos().
CFileDialog.
Man kan som nämnts ovan skapa ett eget objekt av typen CFileDialog. Lämpligt vore i vårt fall att skapa egna funktioner till de tre filhanteringsmenyvalen i arkivmenyn. Dessa kan heta olika beroende av om vi återanvänt originalmenyvalen eller om vi skapat nya menyval. I till exempel OnFileSaveAs() kan det se ut så här när vi öppnar CFileDialog:
void CNotisarDoc::OnFileSaveAs()
{
// Skapa ett CFileDialog-objekt som heter FileDialog.
// Slå upp CFileDialog i hjälpen och läs om argumenten!
CFileDialog FileDialog(FALSE,
"not",
NULL,
OFN_OVERWRITEPROMPT|
OFN_HIDEREADONLY|
OFN_PATHMUSTEXIST,
"Notisar Filer (*.not)|*.not|");
// Översätt titelraden i dialogrutan (se hjälpen!):
FileDialog.m_ofn.lpstrTitle = "Spara Som";
// Hämta dokumentnamnet:
CString Title = GetTitle() + ".not";
// Ge dialogrutan tillgång till filnamnet:
FileDialog.m_ofn.lpstrFile = Title.GetBuffer(FILENAME_MAX);
// Öppna dialogrutan:
FileDialog.DoModal();
// Släpp Title:s buffert:
Title.ReleaseBuffer(-1);
// Hämta fullständigt filnamn:
Title = FileDialog.GetPathName();
// Visa det på skärmen för tillfällig kontroll:
AfxMessageBox(Title);
}
Mycket av detta kräver mera ingående förklaring, och den får du i hjälpen. Den sista raden handlar bara om att testa så att det hela fungerar innan vi lägger in kod för att spara datat på disken. Den ska vi ta bort så fort vi testat. Slå nu upp alla inblandade medlemmar från CFileDialog och CString i hjälpen. Förklara varför vi behöver ange FILENAME_MAX.
? CFileDialog
members,m_ofn, OPENFILENAME, Get/ReleaseBuffer().
Övningsuppgift:
· Öppna projektet Notisar om det inte redan är öppet.
· Lägg till en CFileDialog för Spara Som.
· Vänta med övriga filhanteringskommandon.
· Testa med en AfxMessageBox att det blir korrekt filnamn oavsett om du anger filnamnstillägg eller ej.
CFile.
Sedan kan man spara. Vi har ju tagit över kontrollen av ‘Spara Som’, så nu får vi även spara vårt data själva. Detta gör man med hjälp av CArchive, tillsammans med klassen CFile. Börja med att ta reda på om användaren tryckt på ‘OK’ i filvalsdialogrutan, fortsätt sedan enligt nedanstående:
// Öppna dialogrutan:
int iSvar = FileDialog.DoModal();
...
// Testa om vi tryckt på OK:
if(iSvar == IDOK)
{
// Testa att skapa eller öppna filen:
CFile File;
if(File.Open(Title,
CFile::modeCreate|CFile::modeWrite))
{
// Skapa ett arkivobjekt:
CArchive Archive(&File, CArchive::store);
// Serialisera från variablerna:
Archive << m_Datum;
Archive << m_Tid;
Archive << m_Amne;
Archive << m_Text;
// Stäng arkivet:
Archive.Close();
// Stäng filen:
File.Close();
// Ta bort 'modified'-flaggan:
SetModifiedFlag(FALSE);
}
// Det gick inte att öppna filen:
else
{
AfxMessageBox("Det gick inte att spara!",
MB_ICONEXCLAMATION);
}
}
}
Övningsuppgift:
· Öppna projektet Notisar om det inte redan är öppet.
· Lägg till ovanstående filhantering.
· Testa att spara en notis, och sedan att öppna den igen (vi har ju kvar den gamla öppningsmetoden).
Övningsuppgift:
· Öppna projektet Notisar om det inte redan är öppet.
· Testa att på egen hand byta ut ‘Öppna’ på samma sätt som ‘Spara som’.
· Fortsätt med ‘Spara’.
· ‘Avsluta’ varnar om man inte har sparat sin notis, men det leder till den gamla dialogrutan, byt även den rutinen. Programmet stoppas med PostQuitMessage(0).
Initiera dokumentet.
Om vi nu vill initiera dokumentets variabler skulle det ju slå igenom till vyn och bildskärmen automatiskt. Låt oss testa detta. Men vad ska vi initiera i vårt lilla dokument till en notis? Varför inte datum och tid?
Det finns en funktion ‘time’ som hämtar systemtid och lagrar denna i packat format i en ‘long int’. Datum och tid representeras här som antalet sekunder sedan 1970/01/01 kl 00:00.
En annan funktion localtime räknar om detta till en struct tm, vilken innehåller alla uppgifter uppdelat på olika variabler.
Vi kan även utnyttja funktionerna _strtime() och _strdate() till att formattera klockslag och datum till våra variabler.
void CNotisarDoc::AktuellTid()
{
time_t ttTid;
struct tm *tmTid;
char cTid[10];
char cDatum[10];
// Hämta tid och konvertera till struct tm:
time(&ttTid );
tmTid = localtime(&ttTid );
// Formattera klockslag och datum:
_strtime( cTid );
_strdate( cDatum );
// Tilldela våra variabler:
m_Tid = cTid;
m_Datum = cDatum;
}
För den som är intresserad finns det ganska många funktioner för tid och datum. De vi nämnt ovan finns delvis inbakade i klassen CTime.
Övningsuppgift:
· Öppna projektet Notisar om det inte redan är öppet.
· Initiera tid och datum enligt ovanstående modell. Funktionen anropas från OnNewDocument() i stället för att initiera m_Tid och m_Datum till tomma strängar.
· Slå upp funktioner och strukturer i hjälpen för att ta reda på vilka #include-filer som gehövs.
Övningsuppgift:
· Sudda projektet Notisar.
· Skapa ett nytt projekt Notisar, som baseras på en dialogruta.
· Vi får inga menyer, lägg därför till tryckknappar för alla funktioner vi behöver, det kan se ut så här:
· Lägg till samma filhantering som i tidigare version.
· Lägg till en variabel av typen BOOL som du använder till att hålla reda på om användaren ha ändrat i ‘dokumentet’ så att han behöver tillfrågas om huruvida han vill spara sina ändringar när han stänger programmet, öppnar en ny fil eller skapar nytt ‘dokument’.
· Lägg till samma initiering av datum och tid som i tidigere exempel.