Windowsprogrammering for absolutte nybegyndere

Tags:    c++ windows win32 api gui
<< < 123 > >>
Skrevet af Bruger #8985 @ 13.09.2011


Anatomien bag et vindue


Det er meget lettere at memorere de forkellige trin, man skal i gennem for at oprette et vindue, hvis man ved præcis, hvad deres roller er og hvordan de spiller sammen. Grundlæggende består et vindue af en sender og en modtager. Senderen er ofte en while-løkke, der kører så længe beskeder bliver sendt til vinduet.
Modtageren er en brugerdefineret funktion (ligesom main) der foretager en eller flere handlinger baseret på den besked, den modtager.

WinMain


Idet vi går over til at programmere vores egne vinduer, vil vi naturligvis gerne skille os af med det sorte konsolvindue. Derfor kalder vi nu startpunktet i vores program 'WinMain' fremfor 'main'. Det er stadig muligt at oprette vinduer selvom du bruger main, men så vil både dit vindue og konsolvinduet opstå.

WinMain er en anelse mere kompleks end main. Hvor du med main kan vælge slet ikke at specificere en parameterliste, kræver WinMain hele fire argumenter. WinMain kan eksempelvis defineres således:

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR cmd_args, int wnd_style)
{
}

Måske hævede du øjenbrynene en smule, da du så et 'WINAPI' mellem returtypen og funktionens navn, eftersom de to normalt forekommer umiddelbart efter hinanden. 'WINAPI' er et alternativt navn til attributten __stdcall, en attribut påkrævet når man opretter sine egne Windows-funktioner. Vi kommer til at bruge den i et par andre sammenhænge, blandt andet på den funktion jeg tidligere kaldte programmets 'modtager'. Du kan studere __stdcall nærmere her: http://msdn.microsoft.com/en-us/library/zxk0tw93(v=vs.71).aspx

De første to parametre er af typen HINSTANCE, en type jeg vil forklare om lidt. Når programmet kører og WinMain bliver kaldt, vil operativsystemet levere de fire påkrævede argumenter.
Den første, hInstance, bruges blandt andet i sammenhænge såsom at finde ud af, hvor på computeren programmet ligger.

hPrevInstance bliver slet ikke brugt mere og er altid NULL. Oprindelig blev den brugt til at informere programmet om en allerede kørende instans af samme program. At parametren ikke er fjernet fra funktionen skyldes igen 'backward compatibility', eftersom alle tidligere versioner af Windows forventer fire parametre. Hvis Windows pludselig kun fandt tre ville programmet ikke virke.

cmd_args er af typen LPSTR, som i virkeligheden er en char- eller wchar_t-pointer. Den indeholder de argumenter, kommandolinjen (cmd) vedhæftede vores program, da den oprettede det.
wnd_style indeholder en værdi, vi senere kan bruge til at vise vinduet i den tilstand, brugeren efterspurgte. Det kunne for eksempel være, brugeren ønskede at åbne programmet i maksimeret tilstand.

Handles


En 'handle' er et emne man ikke kommer udenom, når man arbejder med Windows-API'et. Vi stødte endda på dem i det første kodeeksempel. I en nøddeskal er en handle ikke andet end pointer til en struct med kun ét medlem - en int, der overhovedet ikke bruges. En handle er defineret således:

typedef struct {
int unused;
} *HINSTANCE;

WndProc


Nu er der kun én sidste ting du skal have på plads, og så er vi klar til at gå i krig med at oprette et vindue. Denne sidste ting er programmets 'windows procedure' eller 'wndproc'. Som jeg var kort inde på tidligere, er vores wndproc den anden brugerdefinerede funktion - den såkaldte 'modtager' - vores program skal bruge. Den har visse ting tilfælles med WinMain. For det første tager den også fire argumenter, og for det andet er det også en Windows-funktion, hvilket vil sige vi udstyrer den med __stdcall-attributten. En måde at definere den på er således:

LRESULT CALLBACK WndProc(HWND window, UINT message, WPARAM wParam, LPARAM lParam)
{

}

'CALLBACK' og 'WINAPI' er nøjagtig det samme, altså alternative navne for attributten __stdcall. Vi kunne med andre ord også have valgt at skrive 'LRESULT WINAPI WndProc' eller 'int CALLBACK WinMain'. Rigtig mange elementer i API'et har mere end blot et navn, og det er som regel op til en selv at vælge, hvilket man vil anvende. Jeg vil dog på det kraftigste anbefale, du bruger de navne, der er stillet til rådighed af API'et, også selvom du er vant til at skrive 'unsigned int' fremfor 'UINT'. Ved at bruge API-navnene undgår du ikke kun mulige kompatibilitetsproblemer, du vil også forstå logikken og systematikken bag API'et langt hurtigere. Jeg vil lige forklare parametrene i WndProc, og så kaster vi os ud i skabelsen af vores første vindue.

HWND må siges at være den væsentligste handle, du kommer til at arbejde med i windowsapplikationer. De fire bogstaver står for 'Handle to Window', og det er i grunden et ret nøjagtigt navn. Denne handle repræsenterer dit vindue. Den er nødvendig for mere eller mindre alt, du har i sinde at gøre med dit vindue, om det så er at skifte dets titel, ændre dets størrelse, gøre det usynligt eller skifte dets baggrundsfarve, og så videre.

UINT forklarede jeg tidligere, men jeg gør det gerne igen. Det er en sammentrækning af navnet på datatypen unsigned int.

WPARAM og LPARAM er to lidt specielle typer. 'W' står for 'wide' og 'L' står for 'long'. De indeholder begge informationer eller detaljer relateret til en hændelse. Eksempelvis indeholder de vinduets dimensioner ved hændelsen WM_SIZE, som sendes når vinduet ændrer størrelse, og de indeholder musens position ved hændelsen WM_LBUTTONDOWN, som sendes når der klikkes med venstre museknap på vinduet. 'WPARAM' er synonymt med 'UINT_PTR,' som igen er synonymt med 'int __w64'. 'LPARAM' er synonymt med 'LONG_PTR,' som er synonymt med 'long __w64,' men eftersom du ikke behøver holde styr på alt dette, vil jeg igen anbefale dig blot at bruge de navne, API'et tildeler typerne. På den måde slipper du for at huske, hvad hver eneste type er et synonym for.

Det særlige ved WPARAM og LPARAM er måden data er gemt i dem på. Hvis vi går tilbage til WM_SIZE-eksemplet, kunne man godt tro, wParam indeholdt den horisontale dimension og lParam den vertikale, men det er ikke tilfældet. Ved WM_SIZE indeholder lParam begge værdier, mens wParam er tom. Ved andre hændelser er det modsatte tilfældet. De to værdier isoleres fra enten wParam eller lParam ved hjælp af makroerne LOWORD og HIWORD. Hvordan disse fungerer hører ikke hjemme i denne artikel, men du kan læse mere om dem ved at slå dem op i MSDN.

Vores første vindue


Du burde nu være udrustet med den fornødne viden for at oprette et vindue. Jeg vil servere dig hele koden på en gang, og så evaluerer vi den bagefter.

Fold kodeboks ind/udC++ kode 

Resultat



Disse 48 linjer kode skulle gerne oprette et tomt vindue på din skærm. Jeg har gjort mit yderste for at gøre denne introducerende kode så simpel og logisk som muligt. Lad os tage den fra en ende af.

Vi starter med at inkludere windows.h, en header-fil der indeholder et væld af andre header-filer. Dernæst opretter vi vores windowsprocedure, WndProc, der nu reagerer på hændelsen WM_DESTROY, som sendes når vinduet lukker. Via et kald til PostQuitMessage sender vi beskeden WM_QUIT til beskedløkken, som holder op med at lede efter beskeder, og enden af WinMain nås.

I fald beskeden ikke er WM_DESTROY, skal programmet reagere på en normal, ikke-brugerdefineret måde, og dette formås med DefWindowProc, som står for 'Default Windows Procedure'. Vi fodrer DefWindowProc med præcis de samme argumenter vores windowsprocedure modtog, så funktionen kan beregne, hvilken reaktion er passende. Mere behøver vi ikke fra vores wndproc i denne omgang, og vi går i gang med WinMain.

Man opretter vinduer med funktionen CreateWindow, kort og godt. Problemet er, at der ikke eksisterer en skabelon eller windowsclass (WNDCLASS) for et tomt vindue på forhånd, og vi er derfor nødt til at oprette en selv. Det er sådan set dette ene trin, den mest møjsommelige del af koden går ud på. Vi er nemlig nødt til at udfylde en struktur med ti medlemmer, bare for at opnå dette. Det kan være ren tortur at forsøge at memorere navnene på alle disse medlemmer, og det er her en god IDE eller MSDN er guld værd.

Hvis du sidder med Visual C++, vil jeg anbefale dig at vælge View > Other Windows > Web Browser (Ctrl+Alt+R), gå ind på msdn.microsoft.com og søge på 'WNDCLASS'. En anden god idé er at bruge din IDEs indbyggede 'IntelliSense,' såfremt den altså har en sådan. Denne funktion viser dig en liste over en types medlemmer i det øjeblik, du indtaster navnet på en variabel af denne type efterfulgt af '.' eller '->'. På den måde slipper du for at fra start af skulle huske alt dette, og det hjælper en del på motivationen og indlæringen at kunne bevæge sig videre fra dette stadium hurtigt og fokusere på andre ting. Når du har udfyldt en WNDCLASS-struktur 10-20 gange, sidder dette trin heldigvis i fingrene. For en god ordens skyld vil jeg dog lige hurtigt gennemgå dem med dig, da nogle af dem faktisk er ret væsentlige.

cbClsExtra og cbWndExtra sættes til den mængde hukommelse målt i bytes, du vil have allokeret for henholdsvis vinduesklassen og hvert vindue tilknyttet vinduesklassen. Nu om dage bruger man dem nærmest aldrig, og de sættes derfor til 0. De er der mest for bagudgående kompatibilitet, så dit program også kan køre på ældre versioner af windows.

hbrBackground indeholder dit vindues baggrundsfarve. Jeg plejer at sætte den til systemfarven angivet for et vindues baggrundsfarve, som er at finde i konstanten COLOR_BACKGROUND.

hCursor og hIcon repræsenterer henholdsvis vinduets markør og ikon. Ikonet finder du til venstre for teksten i titelbaren. 'IDC' står for 'ID of Cursor' og 'IDI' for 'ID of Icon'. IDC_ARROW er standardmarkøren - pilen, der peger mod nordvest. IDI_APPLICATION er standardikonet, som afhænger lidt af, hvilken version af Windows, du bruger.

lpfnWndProc er en funktionspointer af typen WNDPROC. Vi tildeler dette medlem WndProc. Således er det WndProc, der kommer til at stå for behandlingen af alle beskeder sendt til vores vindue.

lpszClassName tildeles en streng, der skal tilknyttes vores vinduesklasse. På den måde kan vi specificere, hvilken slags vindue vi ønsker at oprette, når vi om lidt kalder CreateWindow. 'lpsz' står for 'Long Pointer to String terminated with a Zero,' altså en pointer til en streng, der afgrænses med tegnet '\0'.

lpszMenuName kan tildeles navnet på en menu, hvis man har en sådan. Dette er en af to mulige måder at oprette menuer på, men i denne artikel bruger vi den anden måde. Jeg går i dybden omkring dette senere.

style kan sættes til en kombination af nogle integral værdier, og på den måde kan man få vinduet til at opføre sig en smule anderledes på visse punkter. Eksempelvis bliver der som standard ikke sendt beskeder om dobbeltklik til et vindue. Ved at sætte dette medlem til CS_DBLCLKS ('Class Style Double Clicks') kan man lave om på dette.

Nu da vinduesklassen er oprettet, mangler vi bare at registrere den med et kald til RegisterClass. Så skal vi til oprettelsen af selve vinduet. Funktionen CreateWindow er en anden ting, der kræver en velfungerende hukommelse, men når du har gjort det et par gange, kan du det i søvne. Det første argument er en streng, der identificerer vores vinduesklasse, eller skabelon som jeg foretrækker at kalde det. Herefter kommer titlen. Så skal vi specificere, hvilke elementer vores vindue skal 'fødes' med. Dette gøres ved at kombinere en række værdier. Vi bruger konstanten WS_OVERLAPPEDWINDOW, som er en kombination af de mest normale værdier. Ved at bruge denne konstant, får vores vindue en ramme, en titelbar, et ikon, en minimér- og maksimérknap og en lukknap.

De næste fire argumenter er henholdsvis antal pixel fra venstre, antal pixel fra toppen, bredde angivet i pixel og højde angivet i pixel. Jeg har valgt 100px fra venstre, 100px fra toppen, 600px bred og 500px høj.

Så kommer vi til to parametre, der lader os angive henholdsvis et forældervindue til vores vindue og en menu. Vinduer kan have både forældre, søskende og børn (ingen kæledyr desværre). Et vindues forælder er det vindue, et undervindue sidder på. Tag for eksempel programmet Notesblok. Det store skrivefelt er barn til selve vinduet med med rammen og titelbaren og så videre. Med andre ord er vinduet forælder til skrivefeltet. Eftersom vores vindue ikke sidder på et andet vindue, det vil sige ingen forælder har, giver vi parametren værdien 0. Da vi heller ingen menu har, angiver vi endnu et 0.

Så er vi næsten ved vejs ende. Vi bruger den HINSTANCE, vi fik af Windows gennem WinMain, som argument. På denne måde kan vi også tilgå denne i funktionen wndproc, hvis det skulle blive nødvendigt. Den sidste parameter kan tildeles hvad som helst, da den er af typen LPVOID, også kendt som void*. Den bruges til at tilknytte noget data, vi selv vælger, til vores vindue. Det kan være meget nyttigt, hvis du har flere vinduer af samme slags, og du har brug for at kunne skelne mellem dem i WndProc, men da vi jo kun har et vindue i denne omgang, specificerer vi ingen brugerdefineret data ved at angive et 0.

Nu er vinduet også oprettet, men vi kan endnu ikke se det. Vi er nødt til at kalde ShowWindow, som tager to argumenter: en handle til det vindue, vi gerne vil vise, samt en værdi, der angiver, hvordan vinduet skal vises. Med det mener jeg, om det skal vises i maksimeret tilstand, i minimeret tilstand, eller bare i normal tilstand. Du kan selv vælge en af de tre ved enten at specificere SW_SHOWMINIMIZED, SW_SHOWMAXIMIZED eller SW_SHOWNORMAL, eller du kan lade Windows afgøre det ved at bruge den værdi, du fik i WinMains sidste parameter, nCmdShow.

Så mangler vi én sidste ting, nemlig beskedløkken. Du kan se denne løkke som programmets 'hjerte,' der bliver ved med at pumpe så længe programmet kører. Løkken kører så længe funktionen GetMessage får lov til at hente beskeder fra den såkaldte 'message queue' eller beskedkø. Funktionen ved, at når den henter en WM_QUIT vil der ikke blive afsendt flere beskeder, og først da vil løkken og programmet stoppe. Ved brug af de sidste tre parametre i GetMessage kan man ignorere en håndfuld beskeder sendt til et bestemt vindue, hvis disse beskeder er under eller over en vis værdi, men vi modtager med glæde alle mulige beskeder.

Hver gang løkken kører, det vil sige hver gang GetMessage har fundet en ny hændelse eller besked, skal vi have sendt denne besked videre til vores wndproc. Dette tager DispatchMessage sig af. I visse tilfælde vil virtuelle tastekoder ('virtual key codes') blive sendt, og disse skal oversættes af funktionen TranslateMessage, før programmet forstår dem. Dette er blandt andet tilfældet, når man indtaster noget i en tekstboks.

Nu har vi gennemgået programmet fra top til tå. Prøv lige at rode lidt med disse opgaver, inden vi går videre.

[1] Deaktiver vinduets maksimérknap. Hint: brug IntelliSense til at se værdien af WS_OVERLAPPEDWINDOW eller slå CreateWindow op på MSDN.
[2] Opret vinduet i centrum af skærmen. Hint: slå GetSystemMetrics op på MSDN.

Udvidet funktionalitet


Når først man har skrevet koden for sit vindue, er det utroligt nemt at udbygge det og tilføje funktionalitet. Vi behøver blot tilføje en 'case' i WndProc for hver ny besked, vi ønsker en reaktion på.

I følgende program har jeg tilføjet funktionalitet for både venstre og højre museknap. Hvis du klikker med venstre museknap, får du at vide, hvor på computeren programmet ligger. Klikker du med højre museknap, får du musens koordinater oplyst.

Fold kodeboks ind/udC++ kode 

Resultat



Vi starter med at definere fire variabler. I princippet behøver de ikke være statiske, men der er ingen grund til at oprette nye, hver gang WndProc bliver kaldt.
MAX_PATH indeholder en værdi, der reflekterer hvor mange tegn en sti må bestå af. Vi kunne også have specificeret vores egen værdi, for eksempel 50, men vi kan ligeså vel benytte konstanten.

Som du kan se, har jeg tilføjet to ny cases, WM_LBUTTONDOWN og WM_RBUTTONDOWN, som afvikles når henholdsvis venstre museknap og højre museknap trykkes. Vi starter med at kigge på WM_LBUTTONDOWN.

Det er GetModuleFileName, der finder stien til programmet. Den første parameter er en HINSTANCE til det program, den skal finde stien på. Ved at bruge 0 som argument, finder den stien på vores program. Man kan altså med andre ord også bruge den HINSTANCE, man får gennem WinMain. Det næste argument er et array af TCHARs, stien skal lægges i. Det sidste argument er, hvor mange tegn der skal lægges i szPath. Vi vil have hele stien med, men ønsker naturligvis ikke, at stien fylder mere end der kan være i szPath, så den får også en MAX_PATH.

Nu har vi stien, men vi vil gerne formulere den ind i en besked. Til det bruger vi wsprintf, som er nøjagtig mage til C's printf, bortset fra at den ikke udskriver den formatterede streng, men i stedet gemmer den i en variabel.

Så når vi til MessageBox, en yderst nyttig funktion, som jeg har dedikeret et helt afsnit til i denne artikel. Den opretter en lille dialogboks, hvor du kan vælge, hvilke knapper og hvilket ikon, der skal stilles til rådighed. Det første argument er en handle til et valgfrit forældervindue. Den eneste effekt dette har, er hvor på skærmen dialogboksen dukker op. Argument nummer to og tre er besked og titel. Det sidste argument er en kombination af konstanter, der bestemmer, hvilke knapper og hvilket ikon, der skal være i dialogboksen.

I WM_RBUTTONDOWN er fremgangsmåden stortset den samme: vi samler informationerne, formulerer dem i en brugervenlig besked og viser denne besked i en dialogboks. Her gør vi for første gang brug af en af de to *PARAM-parametre. Koordinater, dimensioner og deslige er altid i LPARAM. Lad os se at få noget indhold på vores vindue.

På næste side kigger vi på brugerkontroller.





<< < 123 > >>

Hvad synes du om denne artikel? Giv din mening til kende ved at stemme via pilene til venstre og/eller lægge en kommentar herunder.

Del også gerne artiklen med dine Facebook venner:  

Kommentarer (3)

User
Bruger #8985 @ 14.09.11 15:09
I må meget gerne rate og kommentere artiklen :)
User
Bruger #15301 @ 02.10.11 11:00
En meget behagelig tilgang til GUI i c/c++.. Det var godt, at der var lidt flere eksempler end bare lige at vise et vindue, da der også var hvordan man kunne få noget tekst input, samt hvordan man kunne bruge dette test input i sit program :)

User
Bruger #8985 @ 04.10.11 23:06
Endelig en kommentar! Mange tak, Kevin :)
Du skal være logget ind for at skrive en kommentar.
t