11.
CBitmap
Efter detta kapitel ska eleven känna till:
· Merlin.
· Bitmapsredigeraren.
· Initiering.
· Visa bitmap.
· Fönstrets storlek och placering.
· Knapparna.
· Musknapp och position.
· PC-högtalaren.
· Menyerna.
· Bitmappar som menyval.
Merlin.
Det går att använda VBX-kontroller i Visual C++ program. Vi ska dock i detta kapitel prova att använda bitmaps för att göra kontroller på en mera grundläggande nivå. Vi ritar knappar, och även bakgrundsbild, som bitmappsbilder, och känner av musklick samt muspekarens position, och på så sätt skapar vi våra egna knappar.
Programmet ska heta "Merlin", och det är tänkt att det så småningom ska se ut så här:
Idéen kommer från ett mycket gammalt fickdataspel, om inte rent av det första. Man skulle trycka på knapparna för att tända ett visst mönster: mitten och hörnen tända, och de övriga fyra släckta. När man trycker på en knapp påverkas dock flera knappar enligt ett visst system. Får man fram mönstret belönas man med ett typiskt dataspelsljud. Vi ska senare utveckla detta mer.
Börja med att skapa ett helt vanligt projekt typ single-document. Vi kommer visserligen att lägga våra skräddarsydda kontroller i programfönstrets client-area, men det föranleder inte att vi använder CFormView, eftersom vi inte kommer att skapa någon ny dialogruta till dessa kontroller.
Bitmapsredigeraren.
Developer Studio innehåller en Bitmapsredigerare. Man kan skapa en ny bitmap på samma sätt som vilken annan resurs som helst, och sedan redigera den. Observera att det inte går att redigera bitmappar som har för många färger, vilket till exempel Paint kan åstadkomma. (Det går dock att använda Paints bitmappar i våra C-program.)
Bitmapsredigeraren har ett extra verktygsfält med diverse ganska självförklarande ritfunktioner. Den knapp som ser ut som en pipett, är till för att välja färg genom att man pekar på bilden och klickar.
När man till exempel skapar en bakgrund till spelet Merlin kan det se ut så här (efter diverse anpassning, så att allting syns bra):
Skapa bitmappen IDB_MERLIN enligt nedanstående. De viktigaste koordinaterna finns angivna (x,y - visas nere till höger på statusraden i redigeraren):
Färgerna är tre nyanser av rött samt svart. Man måste redigera paletten lite för att få fram tre olika röda ljusstyrkor.
Fortsätt med att skapa bitmap för knapp i nedtryckt (tänt) respektive uppsläppt läge (släckt). De ska ha formatet 44 x 44 och heta IDB_NED resp. IDB_UPP:
Samma färgskala, men med följande tillägg: Den ljusa delen av den tända knappen är klargul och den släckta knappen är i motsvarande fält mörkröd.
Den gula färgen är definitivt en skillnad från originalspelet, eftersom detta hade röda lysdioder i knapparna. Vi kan dock inte få fram särskilt tydlig 'tänd'-effekt med röd färg.
När man skapar bitmappar som man avser använda till att visa tillsammans på skärmen, blir naturligtvis programmet helt beroende av bitmapparnas storlek och form. Om man ändrar storlek på knapparna ovan kommer man att få göra ändringar i programmet som flyttar knapparna till en position där de passar igen. Samma sak om man ändrar bakgrunden.
Initiering.
I vyklassen deklarerar vi tre objekt att använda med de tre bitmapparna vi skapat. De ska vara av typen CBitmap och börja med prefixet m_ (som vanligt):
CBitmap
m_Merlin;
CBitmap m_Upp;
CBitmap m_Ned;
Vi byter ut OnInitialUpdate() för diverse initieringar. Vi kommer att fylla i mer här efterhand, men det första vi ska göra är att initiera bitmap-objekten. Detta gör vi med hjälp av LoadBitmap(), som finns i klassen CBitmap. Slå upp CBitmap - members i MFC-hjälpen.
Använd denna funktion för var och en av bitmapparna, via den indirekta operatorn. Det står att man kan använda antingen en LPCSTR lpszResourceName eller en UINT nIDResource som argument till funktionen. Eftersom vi har ID till våra bitmappar är det bara att använda den sistnämnda formen:
// TODO: Add
your specialized code here and/or call the base
// class
m_Merlin.LoadBitmap(IDB_MERLIN);
m_Upp.LoadBitmap(IDB_UPP);
m_Ned.LoadBitmap(IDB_NED);
Vi kommer att fylla i mer kod här senare, efterhand som vi stöter på sådant som vi vill göra vid initiering.
Nu är det tid att prova på att visa bakgrundsbilden, och det gör vi i en annan funktion.
? CBitmap, CBitmap::
LoadBitmap().
Visa bitmap.
I CView finns en funktion som redan är utbytt i vår vy, vilken heter OnDraw(). Den ansvarar för att rita om vårt programfönster när programmet mottar meddelandet WM_DRAW. I den finns en ‘TODO’-kommentar, och där kan vi fylla i den extra kod som behövs för att våra bitmappar ska synas. Vi börjar med att lägga dit bakgrunden, vilket sker på följande sätt:
· Skapa ett objekt av typen class Device Context (Se klassen CDC i
MFC-hjälpen). Kalla objektet MemDC. Detta objekt ska användas som temporär
lagring för förberedelse och kopiering av bitmap till vyn.
· OnDraw har ett argument, en pekare pDC som pekar på vyns ‘Device
Context’. Använd CreateCompatibleContext() med pDC som argument för att anpassa
vårt CDC-objekt till den aktuella
bildskärmen. (Läs om funktionen under CDC:s 'members'.) Anropa funktionen för
det objekt som skapades i föregående punkt.
· Nästa förberedelse går ut på att låta objektet veta vilken bitmapp som
ska visas, i detta fall har vi objektet m_Merlin. Läs om SelectObject() och gör
som i föregående punkt vid anrop.
· Använd sedan funktionen BitBlt(), via pDC och den indirekta operatorn,
för att kopiera bilden från det temporära minnet till bilden. Slå upp BitBlt()
i hjälpen (CDC).
Så här ser koden ut:
// Skapa ett DeviceContext-objekt:
CDC MemDC;
// Anpassa objektet till aktuell bildskärm:
MemDC.CreateCompatibleDC(pDC);
// Ange vilken bitmap MemDC ska jobba med:
MemDC.SelectObject(&m_Merlin);
// ‘Blitta’ in bilden i aktuellt DC, d.v.s. det som
// pDC pekar på:
pDC->BitBlt(0,0,150,350,&MemDC,0,0,SRCCOPY);
Ordet ‘Blitta’ kräver en mer historisk förklaring. Funktionen heter BitBlt(), där ‘Blt’ är en förkortning för ‘Blit’, försvenskat: ‘Blitta’. Äldre hemdatorer hade inte kapacitet nog till grafisk bearbetning, utan man hade ibland en speciell processor som skötte om kopiering av data. Vanligt var att man behövde kopiera en bild från arbetsminnet till grafikminnet. Den processor som skötte om detta kallades för en ‘Blitter’. En ‘Blitter’ kunde implementeras för datakopiering på motsvarande sätt som en matematikprocessor implementerades för att utföra krävande beräkningar. Detta krävde naturligtvis speciell programmering, som kände av existensen av den extra processorn, så kallad co-processor. Därför implementerades speciella funktioner som gjorde det transparent för programmeraren att utföra operationen, vare sig det fanns en co-processor eller ej. I dag har vi till exempel matematikprocessorer i vissa datorer, och grafikkorten innehåller oftast speciell hårdvara för bildbearbetningen. Man måste installera en speciell drivrutin, eller använda de färdiga drivrutiner som följer med windows, för att få sin egen kod översatt till det som krävs för att utnyttja co-processorn i fråga. Så efter att du kört CreateCompatibleDC() kan du helt transparent anropa BitBlt() för att visa bilden på skärmen, oavsett skärmtyp. Det kan till och med vara så att pDC pekar på ett ‘Device Context’ för en skrivare, och då skrivs bilden ut på pappret i stället.
Kompilera, länka och testa. Tjusigt va? Men programfönstret har inte rätt storlek, och placerar sig inte på samma ställe varje gång. Det fixar vi strax.
? CDC, CreateCompatibleDC(),
SelectObject(), BitBlt(), dwRop.
Fönstrets storlek och placering.
Om vi letar bland klassen CWnd:s medlemmar finner vi en funktion som heter SetWindowPos(). Om vi använd den funktionen i OnInitialUpdate får vi möjlighet att påverka fönstrets pacering på skärmen i x, y och z-led, samt ändra fönstrets dimensioner. Med z-led avses vilka fönster som ligger ovanför varandra och därigenom skymmer varandra. Slå upp denna funktion och se efter vilka argument den vill ha.
Det första argumentet avser positionering i z-led, och dess användning kan verka lite oklar, men det enklaste är att säga att vårt programfönster ska ligga överst i z-ordningen, det vill säga synas framför allt annat på bildskärmen. Detta får man genom att ange ‘&wndTop’.
Övriga parametrar utom den sista är enligt hjälpen diverse koordinater. Vi vill att hela bilden syns, och då måste man räkna in ramarna. Efter diverse experimenterande fann jag att bredden borde vara 158 och höjden 396, det vill säga fönstrets yttermått måste anges.
Den sista parametern är flags, och det använder vi inga, utan vi sätter den till NULL:
SetWindowPos(&wndTop,
100, 50, 162, 400, NULL);
Kompilera, länka och testa detta. Fungerar det? Nej. Trist va?
Om man lusläser hjälpen ser man "mellan raderna" att funktionen i fråga är avsedd att positionera ett child-window av något slag, om vi nu betraktar pop-up och topmost windows som "barn" till vårt programobjekt, eller egentligen MainFrame. Man behöver alltså använda någon funktion som kan ge oss en pekare mot ägaren till vår fönsterklass. Det finns (naturligtvis) en sådan, och den heter GetOwner() och den tar inga argument alls. Om vi således använder själva funktionsanropet som pekare kan vi skriva så här:
GetOwner()->SetWindowPos(&wndTop,
100, 50, 162, 400, NULL);
Kompilera, länka och testa. Nu... funkar det!
Men, nu kan man ju faktiskt använda kanterna till att ändra storlek på fönstret. Det kan ju i och för sig vara OK, men eftersom det är helt meningslöst att ändra storlek på detta spel, så kan vi ju eventuellt göra något åt även detta.
Enkelt: När man ändrar storlek på ett fönster ritas det om, OnDraw. Vi har ju just lagt in kod i den funktionen. Kopiera det kodavsnitt som ställer in storleken på fönstret från OnInitialUpdate och lägg till här.
Det finns också en funktion som anger texten i titelraden. Den heter SetWindowText(). Slå upp den i hjälpen för att se hur den används. Det ska stå "‑=Merlin=-" i titelraden. Anledningen till att vi inte ändrar i stringtable0 för att ändra titeln är att man då även får med aktuellt dokuments filnamn automatiskt, och vi vill inte ha något sådant.
Knapparna.
Nu när vi vet hur vi ska visa en bitmap, är det dags att rita in knapparna. Men först behöver vi lite variabler.
Knapparnas tillstånd (uppe eller nere) vill vi kunna spara i en lista av typen BOOL. Vi kommer också att utöka spelet med olika lösningsmönster (så får vi nöjet att använda bitmappar i menyvalen också), så vi behöver en motsvarande lista med facit.
Mitt förslag är att kalla listorna 'm_Status' respektive 'm_Facit'. De ska ha 9 element, ett för varje knapp. Vi kommer att arbeta i vyklassen hela tiden, så det är bara att deklarera dem där, under 'public:'.
När spelet startas ska knapparna vara slumpvis inställda. Därför behöver vi en slumptalsgenerator. Slå upp rand() i hjälpen och använd den funktionen. Om du inte klarar att läsa dig till hur du ska använda den så får du diskutera det med någon, eller se i det exempel som står i hjälpen. Det står bland annat om hur man måste initiera den, och det är bara att kopiera rätt rad från hjälpen, och lägga in den i On InitialUpdate().
Vi kan arbeta helt i vy-klassen i detta program. Det betyder att vi deklarerar alla variabler och funktioner i klassen, så är alla variabler kända för alla funktioner. Vi börjar med en funktion void Blanda(). Deklarera prototypen i klassdeklarationen, och skriv funktionen i implementeringsfilen. Funktionen ska använda rand() till att tilldela varje knapp sitt tillstånd (status), vilket kan vara 0 eller 1.
En funktion vi kan kalla VisaKnappar(), kan användas att anropa en annan funktion, till exempel VisaKnapp(), för alla knappar. VisaKnapp() måste få ett argument som talar om vilken knapp som ska visas. Om du inte vill göra det på just detta sättet kan du lösa det på annat lämpligt sätt.
Börja med att deklarera funktionen. Kopiera den kod som visar bakgrunden i OnDraw(), och lägg in den i VisaKnapp(). Byt ut m_Merlin mot m_Ned eller m_Upp, så att vi kan testa att rita en knapp. Lägg in ett tillfälligt anrop till VisaKnapp i OnDraw(), bara för test.
Kompilera. Nu klagar kompilatorn på att den inte känner till pDC. Om vi tittar i funktionsheadern till OnDraw(), finner vi att den fanns serverad där vid anrop, men i VisaKnapp() har vi den inte tillgänglig. Alltså måste vi skaffa en pekare mot vårt Device Context på egen hand.
Titta i ‘hierarchy charts’. Under klassen för device context, CDC, finns en speciell klass för client-areans device context, CClientDC. Det är ett objekt av den klassen vi behöver. Dess constructor tar emot en pekare mot ett fönster, och där kan vi använda oss av 'this', så får vi ett ‘Device Context’ som därigenom verkar mot vårt programfönsters arbetsyta (‘Client Area’). Kalla objektet ‘ClientDC’.
CClientDC
ClientDC(this);
Byt sedan ut alla förekomster av pDC mot &ClientDC.
Nu... äntligen, ska vi kunna få upp en knapp mot bakgrundsbilden. Visserligen kommer den på helt fel ställe, uppe i vänstra hörnet, men det kan vi ju ordna genom att fixa till koordinaterna i BitBlt()-funktionen.
Nu kan vi göra färdigt VisaKnapp() och VisaKnappar(). Gör följande tillägg:
· VisaKnapp() ska ta ett heltal som argument. Använd detta för att beräkna var knappen ska ritas.
· Använd den även för att kontrollera om den ska vara nertryckt eller uppsläppt enligt Status[i], och välj rätt bitmap i enighet därmed.
· Skapa funktionen VisaKnappar, vilken anropar VisaKnapp en gång för varje knapp med hjälp av en for-slinga.
Ändra nu det tillfälligt anropet till VisaKnapp() i 'när vi ritar'-funktionen, så att vi i stället anropar VisaKnappar().
Kompilera, länka och testa.
Om vi får samma slumpvis valda mönster varje gång vi startar programmet har vi kanske glömt att initiera rand() i OnInitialUpdate(), se ovan.
Musknapp och position.
Innan vi rusar åstad för att finna allt om hur man använder musen i hjälpen bör vi kanske höra med Class Wizard vilka Window Messages som finns för mus-knapparna.
När vi gjort detta har vi antagligen lyckats hitta meddelandet WM_LBUTTONDOWN. Skapa en funktion som anropats när man trycker ner vänster musknapp. Om vi studerar funktionshuvudet finner vi att ett av argumenten är av klassen CPoint och heter point.
Slår man upp CPoint i hjälpen blir man snart klok på hur man kan få reda på vilka kooridinater muspekaren stod på när knappen trycktes ned. Tänk på att man kan använda elementoperatorn. Koordinaterna är förresten relaterade till klientarean, inte bildskärmen, vilket underlättar nedan föreslagna programmering.
Lägg in en if-sats som avgör om muspekaren stod på någon knapp när vänster musknapp trycktes ned. Det blir en ganska omfattande if-sats, så håll reda på paranteserna. Om någon knapp träffats kan vi testa med att skriva ut en AfxMessageBox(”Träff!”) bara för att se att något händer. Var noga med koordinaterna, inget får hända om man klickar utanför eller mellan knapparna.
När detta fungerar är det bara att skriva någon sorts algoritm som beräknar vilken knapp som tryckts ned. Det måste gå att få fram med hjälp av koordinaterna. Testa att byta status på den knapp som tryckts och sedan anropa VisaKnapp() med vald knapp som argument. Knapparna ska vara numrerad från noll till åtta, i samma ordning som man läser en text.
Kom i håg att vi nu hela tiden arbetar i det kodavsnitt som som tillhör if-satsen. Ta bort anropet till VisaKnapp(), och ta fram papper och penna. Det är dags för JSP.
Låt oss först definiera ett par olika typer av knappar: Det finns en mittknapp (den har nummer fyra). Det finns fyra knappar i hörnen, dem kallar vi hörnknappar. De övriga fyra kallar vi sidoknappar.
Vi behöver en logik som med utgångspunkt från vilken knapp man tryckt på gör ändringar i status för 2 eller 4 andra knappar, beroende av vilken metod som valts. Metod? Ja, det är ett av de tillägg jag utlovade till spelet. Man ska senare kunna välja metod via menyval, men just nu nöjer vi oss med att deklarera en variabel (i vyklassen naturligtvis) som vi kan kalla till exempel m_Metod. Den ska vara initierad i OnInitialUpdate till 0. Det ska finnas fyra metoder, 0 - 3, vilka ska fungera enligt följande:
OBS! Den knapp man trycker på ska ändra status i alla metoderna.
Metod 0: Mittknappen gör att alla andra knappar också ändrar status till motsatt
status mot vad de hade (tänd blir släckt och släkt blir tänd).
Hörnknapparna gör detsamma med den hörnknapp som följer medsols och den
sidoknapp som ligger på sidan mittemot den sida där bägge hörnknapparna just
bytt status.
Sidoknapparna gör detsamma med sidoknappen som följer medsols och den hörnknapp
som ligger i motstående hörn på samma sätt.
Metod 1: Mittknappen gör att alla knappar byter status med varandra tvärs över
knappsatsen. (0 byter med 8, 1 byter med 7 etc.)
Hörn- och sido-knapparna påverkar samma knappar som i metod 0 med den
skillnaden att de byter status med varandra i stället för att ändra till
motsatt status.
Metod 2: Mittknappen gör att hörnknapparna ändrar till motsatt status medan
sidoknapparna byter status med varandra tvärs över knappsatsen.
Hörnknapparna gör att de två hörnknappar som inte ligger mitt emot tryckt
hörnknapp ändrar till motsatt status. De två sidoknappar som omger motsatt hörn
byter status med varandra.
Sidoknapparna gör att de sidoknappar som inte ligger mitt emot tryckt sidoknapp
ändrar till motsatt status. De två hörnknappar som omger motsatt sidoknapp
byter status med varandra.
Metod 3: Mittknappen gör att alla andra knappar också ändrar till motsatt status
på samma sätt som vid metod 0.
Hörn- och sido-knapparna fungerar som i metod 1, med det tillägget att även
spegelvänt förhållande gäller, d.v.s. det som gäller för ‘medsols’ ska även
läggas till för motsols. Det blir alltså fyra knappar som parvis byter status
med varandra, sidoknapp mot hörnknapp.
Ett sätt att lösa detta är att använda en switch, med knappnummer som argument, som oavsett metod väljer ut de fyra andra knapparna som ska påverkas.
Därefter kan man använda en switch med m_Metod som argument till att ändra respektive byta status enligt ovanstående regler.
(Till att börja med kan man prova hur det fungerar med bara metod 0 ifylld. De andra metoderna kan man lägga till sedan, när själva grundlogiken fungerar.)
Sedan, fortfarande inom if-satsens kodavsnitt, jämför vi det nya tillståndet med facit. En ny, nästlad, if-sats delar upp i två kodavsnitt, ett för färdig lösning, ett för fortsättning.
Vi fortsätter med JSP-schemat:
Är lösningen klar ska vi spela en trudelutt medan vi blinkar med knapparna. Låt oss föra in ett block för detta, men vänta med att fundera över hur vi ska lösa det.
Därefter visar vi en meddelanderuta som talar om för oss hur många knapptryckningar vi behövde för att nå lösningen. Till detta behöver vi en räknare, som vi kan kalla t.ex. m_Count. Den ska deklareras och initieras och sedan räknas upp varje gång vi tryckte på en knapp, d.v.s. före den första switchen ovan.
Sedan ska vi blanda. Detaljerna fixar vi till sedan, men vi ska naturligtvis använda funktionen Blanda().
Till sist nollställs m_Count.
Är lösningen inte klar, ska vi pipa med högtalaren, s.k. knapp-beep, under förutsättning att ljudet inte är avstängt. För att kunna avgöra detta behöver vi en variabel som är deklarerad och initierad till TRUE. Vi ska senare lägga till ett menyval som aktiverar respektive stänger av ljudet. Variabeln kan heta t.ex. m_Beep. Denna ska bara påverka knapp-beepet, inte andra ljud.
Blev JSP-schemat fint? Skriv då den del av koden som vi hittills beskrivit, och testa, så ska vi strax lägga till ljud och discoeffekter.
PC-högtalaren.
I de flesta operativsystem, och där räknar jag nu även in Windows, kan man använda den inbyggda högtalaren till att åstadkomma pipljud av olika frekvens och varaktighet. Det finns en funktion som åstadkommer just detta, beep(), men när vi läser om den i hjälpen finner vi att Windows 95 inte bryr sig om argumenten för frekvens och varaktighet, utan spelar bara upp ett standardpip. För att kunna pipa med olika tonhöjd och tonlängd blir vi alltså tvugna att själva skriva en lågnivåfunktion som arbetar direkt mot hårdvaran. Vi ska inte bli experter på hårdvara, så ta detta bara som ett exempel på hur det kan se ut.
Att arbeta med hårdvaran direkt innebär att man måste använda mikroprocessorns in- respektive out- instruktioner. Efter diverse forskande fann jag att det fanns motsvarigheter till out i C vilken heter _outp (out port), samt en för in som heter _inp. Dessa instruktioner är till för att 'lämna meddelande' direkt till, respektive hämta information direkt från en hårdvaruadress, och PC-ns högtalare är ansluten till en krets som har en dylik, nämligen hex 61. För att få tillgång till detta måste vi inkludera filen conio.h Inställningar av frekvens etc. sker på adresserna hex 42 och 43. Dessutom finns en hårdvarutimer som vi kan nå med en funktion clock(). Vi utnyttjar dessa med hjälp av nedanstående ‘hack’, vilket inte följer Microsofts riktlinjer:
void
CMerlinView::Beep(unsigned int frekvens,
unsigned int langd)
{
// Variabel för att spara högtalarens
// ursprungliga kontrolldata.
int kontroll;
// Variabel för periodtid:
unsigned int period;
// Timern aktiveras genom att man skickar
// värdet 10111100 till port 43.
_outp(0x43, 0xb6);
// Omvandla frekvens i Hertz till periodtid i
// tusendels sekunder (räknaren tickar 1000 ggr/s):
period = (unsigned)(1193180L / frekvens);
// Lagra periodtiden i timerns periodregister (vilken
// högtalarens hårdvara använder för att pipa med rätt
// tonhöjd):
_outp(0x42, (char)period);
_outp(0x42, (char)(period >> 8));
// Spara högtalarens ursptungliga inställning:
kontroll = _inp(0x61);
// Aktivera pipet:
_outp(0x61, kontroll|0x3);
// Pip så länge som angivits:
Paus((clock_t) langd);
// Återställ högtalaren till ursprungsvärdet:
_outp(0x61, kontroll);
}
// Pausfunktion, väntar angivet antal millisekunder.
void
CMerlinView:: Paus(clock_t millisek)
{
// Variabel för slutvärde på timern.
clock_t sluttid;
// Timern räknar i millisekunder. clock() returnerar
// timerns värde. (Timern räknar antalet millisekunder
// från midnatt innevarande dygn.)
sluttid = millisek + clock();
// Ful pausloop som inte tillåter resursdelning i ett ‘non-
// preemptive’ operativsystem.
while(sluttid > clock());
}
Så här skulle man sedan kunna använda vår funktion Beep() till att åstadkomma en pip-och-blink-show att visa när man klarat att lösa spelet:
for(int k = 0;
k < 8; k++)
{
Beep(990, 15);
Beep(880, 15);
Beep(770, 15);
Beep(660, 15);
Beep(550, 15);
Beep(440, 15);
for(int j = 0; j < 9; j++)
{
Status[j] = !Status[j];
}
VisaKnappar();
}
När man rapporterat antalet knapptryckningar i en AfxMessageBox(), skulle man kunna blanda knapparna spektakulärt på följande sätt:
for(k = 440; k
< 990; k += 22)
{
Blanda();
VisaKnappar();
Beep(k,5);
}
Menyerna.
Använd Application Studio till att lägga upp menyerna enligt nedanstående. Det nya är menyval med undermenyer. Detta åstadkommer man genom att kryssa för egenskapen 'Popup'. Därigenom får man en tom lista med undermenyer. Så här ska strukturen se ut (Metod och Mönster är ‘pop-up’):
Arkiv
Metod
Lätt
Medel
Svår
Omöjlig
Mönster
Bild0
Bild1
Bild2
Bild3
Bild4
Beep
Avsluta
?
Om Merlin...
De olika valen under Mönster ska vi specialbehandla i avsnittet 'Bitmappar som menyval' nedan, lägg bara in dem så här.
Lägg till funktioner till menyerna.
· De olika menyvalen under Metod ska påverka variabeln m_Metod.
· De olika valen under Mönster ska vi titta på senare.
· Beep ska aktivera / avaktivera pipet, dvs m_Beep = TRUE / FALSE.
· Instruktioner lämnar vi till nästa kapitel.
Nu har vi några ställen där vi behöver markera menyval. Som vi sett tidigare kan det finnas en bock i kanten för ett menyval. När man använder Application Studio finner vi att man kan initiera ett menyval till att ha bock i kanten genom att kryssa för egenskapen 'checked'. Detta gör vi för menyvalet Beep och Lätt.
Sedan måste vi själva ändra detta status från våra ON_COMMAND-funktioner. Om vi till exempel stänger av pipet ska bocken försvinna. Sätter vi sedan på pipen ska bocken visas igen. Om vi väljer en metod ska det undermenyvalet bockas för, och alla övriga undermenyval ska lämnas utan bock, vilken man lättast åstadkommer genom att sätta dem alla till icke förbockade.
Och hur, åstadkommer man då förändring i egenskapen checked? Leta i hjälpen! Kom ihåg att 'sätta bock för' och 'kryssa i' heter 'check' på engelska. Ett menyval heter 'menu item'. Alltså borde det finnas något som heter CheckMenuItem(). Läs om den funktionen och använd i kommandohanterarna.
När man kompilerar blir det antagligen fel. Enligt hjälpen ska funktionen ha 2 argument, åtminstone om man får tro Class Library Reference, men kompilatorn klagar över att det inte ska vara två parametrar.
Om vi däremot konsulterar det avsnitt som finns för funktionen under boken Windows32 SDK, får vi veta att det finns en gammal version 2.0 av funktionen. Den vill ha tre parametrar. Skillnaden ligger i att den vill ha ett handtag, en pekare hmenu som talar om var menyobjektet finns. Det måste vara den funktionen som anropas. Detta beror av att vi inte arbetar i MainFrame(). Tillgången till funktionen har vi fått via filen windows.h. Hade vi i stället arbetat i MainFrame så ingår funktionen där i stället, och den funktionen tar två argument.
Hur gör vi om vi vill anropa den funktion som tar två argument då, det vill säga den funktion som ligger i vårt menyobjekt? Jo vi behöver en pekare att använda via den indirekta operatorn igen, så vi kan peka på objektets funktion.
Då får vi väl skaffa oss en då. Leta i hjälpen. I CView finns dock ingen metod som ger oss en pekare till menyerna, men enligt program-hierarkin har CView ärvt av CWnd, och där hittar vi bland en himla massa metoder en som heter GetMenu(). Är det inte den vi behöver så ska jag äta upp min gamla Amiga. Egentligen är det logiskt, menyobjektet tillhör inte vyn utan MainFrame. Dock kan alla fönster förses med en egen meny, så funktionen borde logiskt höra hemma i klassen CWnd.
Glada i hågen kompilerar vi, får inte något felmeddelande, länkar och testar. Det fungerar inte. Men vänta... vi har ju inte talat om vilket CWnd-objekt vi menar när vi anropar GetMenu(). Här behövs också en pekare och den indirekta operatorn. Men vad ska pekaren peka på? Jo, vi anropar från en funktion i vyn, och vill hitta något via en funktion i förälderobjektet, MainFrame. Från den klassen har vi ärvt en metod som heter GetParent(). Använd den så fungerar det.
CMerlinView::OnBeep()
{
if(m_Beep = !m_Beep)
{
GetParent()->GetMenu()->
CheckMenuItem(ID_ARKIV_BEEP,
MF_CHECKED);
}
else
{
GetParent()->GetMenu()->
CheckMenuItem(ID_ARKIV_BEEP,
MF_UNCHECKED);
}
}
När vi efter några liter blod, svett och tårar fått Beep-menyvalet att ändra status, tillsammans med m_Beep, går vi vidare, och fixar Metod på egen hand. Tänk bara på att du måste ta hänsyn till att du har fyra menyer varav en ska vara förkryssad, så koden blir litet annorlunda jämfört med OnBeep().
Bitmappar som menyval.
Tidigare lade vi in fem menyval under Mönster-menyn, vilka vi tillfälligt kallade Bild0 - Bild4. Dessa behövdes för att lura till oss ID till dessa menyer. Därigenom kan man använda Class Wizard för att lägga till de funktioner som ska anropas när man väljer respektive bild.
Vi ska dock inte se texten 'Bild0' etc. när vi öppnar Mönster-menyn. Meningen är att vi ska se fem olika lösningsmönster:
Vi borde kunna använda samma metod som ovan för att kunna 'komma åt' menyvalen. Detta går bra, men det första steget med GetParent() är onödigt om man gör det här på rätt ställe. Eftersom vi inte ska lägga in bilderna mer än en gång är det inget som säger att det måste göras i vyn. Det är ju så att menyerna hör till MainFrame, så varför inte se se efter om det finns något lämpligt ställe där. Ber vi Class Wizard om hjälp hittar vi snart ett meddelande WM_CREATE, till vilket vi kan få skapat en lämplig funktion.
En annan sak: Eftersom vi måste ange vilket menyval till menyn Arkiv vi menar, det vill säga Mönster, borde vi kunna göra det här med GetMenu() två gånger, en gång för att skaffa en pekare som pekar på Arkiv och en gång för att peka på Mönster. Det andra anropet måste då ske med en annan, liknande, funktion som skaffar fram en pekare mot en undermeny i stället. (Menyvalet Mönster betraktas som undermeny i och med att det är en popupmeny, och därigenom har egna menyval under sig.) Ett annat, och mycket enklare, alternativ är att använda de ID vi skapat till menyvalen.
I den nya funktionen OnCreate() är det meningen att vi ska byta ut de fem menyvalen mot menyval som använder bit-mappar i stället. Skapa först bitmapparna med Bitmap Editor.
Byta ut innebär att man först tar bort de gamla menyvalen, sedan sätter dit de nya, som ska vara av typen bitmap.
När vi letar bland CMenu:s metoder (eller hur?) finner vi en funktion som kan ta bort menyval, DeleteMenuItem(). Om man lusläser instruktionerna finner man att man kan ange vilket menyval man avser att ta bort på två sätt: efter position eller efter ID. Argument nummer två använder man för att ange vilken metod man använder. Observera att om man tar bort ett nummer, och börjar med den första (nummer 0), kommer de efterföljande att byta nummer.
Sedan är det 'bara' att lägga dit de nya menyvalen. CMenu har en metod som lägger till nya menyval sist i en meny, alltså måste man lägga till dem i rätt ordning. Dess första argument talar om vilken typ av meny det är. Det finns tre typer: text, ownerdraw och separator (ett vågrätt streck som delar upp menyn i logiskt sammanhängande delar). Vi behöver inte något speciellt här, så vi väljer att inte ange något alls, det vill säga vi skriver 'NULL'.
Andra argumentet är ID. Där använder vi naturligtvis samma som vi redan skapat när vi lade in våra 'dummys', som vi just tagit bort.
Tredje och sista argumentet är en pekare mot en bitmap. Hur vi skapar en sådan pekare är numera gammal skåpmat, så det klarar vi snabbt och enkelt. Glöm bara inte att 'ladda' bitmapen.
Gör detta för var och en av menyerna. För att de nya menyerna ska ritas upp anropar vi sedan funktionen DrawMenuBar(), som ligger i CWnd. Använd räckviddsoperatorn (scope resolution operator).
Ovanstående diskussion borde resultera i ungefär den kod som följer nedan:
int
CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
if (CFrameWnd::OnCreate(lpCreateStruct) ==
-1)
return -1;
// Skaffa en pekare till meny, undermeny samt menyval:
CMenu* pMenu = GetMenu();
CMenu* pArkivMenu =
pMenu->GetSubMenu(0);
CMenu* pMnsterMenu =
pArkivMenu->GetSubMenu(1);
// Ta bort alla menyvalen:
pMnsterMenu->DeleteMenu(4, MF_BYPOSITION);
pMnsterMenu->DeleteMenu(3, MF_BYPOSITION);
pMnsterMenu->DeleteMenu(2, MF_BYPOSITION);
pMnsterMenu->DeleteMenu(1, MF_BYPOSITION);
pMnsterMenu->DeleteMenu(0, MF_BYPOSITION);
// Skaffa pekare till bitmaparna, läs in bitmaparna och
// lägg till menyval av typen bitmap:
CBitmap *pBild0 = new CBitmap;
pBild0->LoadBitmap(IDB_BILD0);
pMnsterMenu->AppendMenu(MF_ENABLED,
ID_ARKIV_MNSTER_BILD0, pBild0);
CBitmap *pBild1 = new CBitmap;
pBild1->LoadBitmap(IDB_BILD1);
pMnsterMenu->AppendMenu(MF_ENABLED,
ID_ARKIV_MNSTER_BILD1, pBild1);
CBitmap *pBild2 = new CBitmap;
pBild2->LoadBitmap(IDB_BILD2);
pMnsterMenu->AppendMenu(MF_ENABLED,
ID_ARKIV_MNSTER_BILD2, pBild2);
CBitmap *pBild3 = new CBitmap;
pBild3->LoadBitmap(IDB_BILD3);
pMnsterMenu->AppendMenu(MF_ENABLED,
ID_ARKIV_MNSTER_BILD3, pBild3);
CBitmap *pBild4 = new CBitmap;
pBild4->LoadBitmap(IDB_BILD4);
pMnsterMenu->AppendMenu(MF_ENABLED,
ID_ARKIV_MNSTER_BILD4, pBild4);
// Rita om menylisten så att de nya menyvalen syns:
CWnd::DrawMenuBar();
return 0;
}
Nu kan vi kompilera, länka och testa.
Lägg sedan till funktioner till menyvalen. De ska skapa facit som motsvarar valt mönster. Glöm inte att bocka för valt mönster, och att ta bort bocken för alla andra.
Avsluta all kod som eventuellt inte är färdig. Anpassa dialogrutan Om Merlin... och gör en ny programikon med nedanstående utseende:
Kopiera programmet till skrivbordet och prova så att allt fungerar som det ska.
Programmet finns också under o:\ vcpp och heter merlin.exe. Prova detta och jämför!
Övningsuppgift:
· Nej, inte helt utantill! Avsikten med detta kapitel var huvudsakligen att vi skulle lära oss hur vi letar upp det vi behöver i hjälpen.
Överkursuppgift:
· Gör en bitmap-animering:
· Skapa med hjälp av Paintbrush ett antal bilder som i sekvens utgör en rörelse.
· Slå upp i hjälpen hur man skapar ett Timer-objekt. Metoder finns i CWnd.
· Man ska kunna starta och stoppa animeringen med hjälp av menyval.