* yksinkertaiset Windows-ohjelmat: .C, .H, .DEF -tiedostot
* peruskäsitteet: viestisilmukka, ikkunafunktio, viestit, kahvat, esiintymät, laiteyhteys
* 16- ja 32-bittinen Windows
* viestien käsittely taulukon avulla
* perustoiminnot: tekstit, viivat, kuviot
* keskeiset ongelmat: näytön päivitys ja moniajon mahdollistaminen
Yksinkertainenkin "Hello World" - tulostava Windows-ohjelma tarvitsee lähes A4:sen verran C-ohjelmakoodia. Aluksi täytyy kertoa käytettävän ikkunan ominaisuuksia yms. Sitten täytyy pitää huolta viestisilmukasta ja lopuksi huolehtia ohjelman siististä lopettamisesta.
Linker warning: No module definition file specified; using defaults
Tästä varoituksesta ei tässä tapauksessa tarvitse välittää, vaan jatketaan kääntämistä. Palaamme .DEF -tiedostoihin myöhemmin.
Voimme tehdä myös "oikean" Windows-ohjelman, jossa tulostukseen käytetään printf -funktiota:
/**************************************************************************** PROGRAM: Ehello.c PURPOSE: Esimerkki EasyWin-ikkunan käytöstä: ****************************************************************************/ #include <windows.h> /* Tarvitaan kaikissa Windows C-ohjelmissa */ #include <stdio.h> #pragma argsused /* Jottei parametreistä valiteta */ int PASCAL WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) { _InitEasyWin(); printf("HelloWorld!"); return 0; }
Muuten Windows-ohjelmissa EI SAA KÄYTTÄÄ printf -funktiota! _InitEasyWin -kutsu alustaa ja luo ikkunan, johon voidaan kirjoittaa printf -funktiolla. Oikeassa Windows-ohjelmassa ominaisuutta tulee välttää ja parhaiten se sopii ehkä debuggaus-tarkoituksiin. Esimerkiksi Borlandin EasyWin ei toimi 32-bittisessä Windowsissa, mutta tiedostossa ALI\EASYW.C on alkeellinen vastike, jonka pitäisi toimia myös 32-bittisessä ympäristössä.
Yksinkertainenkin Windows-ohjelma tarvitsee projektin, jossa on tarvittavat C-ohjelmat sekä .DEF -tiedosto.
; module-definition file for WHELLO NAME WHELLO ; application's module name , OPTIONAL DESCRIPTION 'Hello World for Windows' ; , OPTIONAL EXETYPE WINDOWS ; required for all Windows applications ;CODE can be moved in memory and discarded/reloaded CODE PRELOAD MOVEABLE DISCARDABLE ;DATA must be MULTIPLE if program can be invoked more than once DATA PRELOAD MOVEABLE MULTIPLE HEAPSIZE 1024 STACKSIZE 5120 ; recommended minimum for Windows applications
Tiedostossa ali\def.def on valmis pohja, josta optionaaliset parametrit puuttuvat. Yksinkertaisissa ohjelmissa DEF.DEF voidaan linkittää omaan ohjelmaan.
/***************************************************************************** PROGRAM: Mhello.c PURPOSE: "Pienin Windows-ohjelma". Tulostaa näyttöön tekstin Hello World Editor: Vesa Lappalainen typistänyt malliohjelmista. Project: mhello.c, mhello.def *****************************************************************************/ #include <windows.h> /* Tarvitaan kaikissa Windows C-ohjelmissa */ LONG CALLBACK _export MainWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { PAINTSTRUCT ps; switch (message) { case WM_PAINT: /* Viesti: Piirrä ikkuna uudelleen */ if ( BeginPaint(hWnd,&ps) ) TextOut(ps.hdc, 10, 10, "Hello World!",12); EndPaint(hWnd,&ps); return NULL; case WM_DESTROY: /* Viesti: ikkuna hävitetään */ PostQuitMessage(0); return NULL; default: /* Antaa Windowsin käsitellä muut */ break; } return DefWindowProc(hWnd, message, wParam, lParam); } int PASCAL WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) { WNDCLASS wc; /* Ikkunaluokka */ HWND hWnd; /* Pääikkunan kahva */ MSG msg; /* Viesti */ (void)lpCmdLine; /* Hämäystä, jottei valitusta param. käytt. */ if (!hPrevInstance) { /* Onko muita esiintymiä käynnisssä? */ wc.style = NULL; wc.lpfnWndProc = MainWndProc; wc.cbClsExtra = 0; wc.cbWndExtra = 0; wc.hInstance = hInstance; wc.hIcon = LoadIcon(NULL, IDI_APPLICATION); wc.hCursor = LoadCursor(NULL, IDC_ARROW); wc.hbrBackground = GetStockObject(WHITE_BRUSH); wc.lpszMenuName = NULL; wc.lpszClassName = "WHelloWClass"; if (!RegisterClass(&wc)) return 1; } hWnd = CreateWindow("WHelloWClass","Windows Hello",WS_OVERLAPPEDWINDOW, CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT, NULL,NULL,hInstance,NULL); if ( !hWnd ) return 1; ShowWindow(hWnd, nCmdShow); /* Näytetään ikkuna */ UpdateWindow(hWnd); /* Lähetetään WM_PAINT viesti */ while (GetMessage(&msg,NULL,NULL,NULL)) { TranslateMessage(&msg); /* Tulkitaan virtuaaliset näp. koodit */ DispatchMessage(&msg); /* Lähetetään viesti ikkunalle */ } return msg.wParam; /* Palautetaan PostQuitMessage-funktion arvo */ }
Nyt tarvitsemme hetken aikaa eri ominaisuuksien opiskeluun.
Tavassa yritetään yhdistää lyhennettyjen nimien yksinkertainen kirjoittaminen ja pitkien nimien kuvaavuus. Nimen eteen laitetaan lyhyt lyhennys kuvaamaan tunnisteen tyyppiä ja/tai tapaa, jolla tunnistetta käytetään. Seuraavassa esitämme listan yleisistä lyhenteistä. Listan lyhenteet eivät ole minkään sovitun standardin mukaisia, mutta niitä on kerätty yleisistä ohjelmista.
h - alkuiset : kahvoja (Handle), kahva on kokonaisluku, jonka perusteella systeemi osaa valita jollekin toiminnalle oikean kohteen. wnd : ikkuna (WiNDow) dc : laiteyhteys (Device Context) ch : merkki (CHar) sz : NUL-loppuinen merkkijono (String Zero) p : osoitin (pointer) lp : pitkä (far) osoitin (Long Pointer) fn : funktio (FuNction) st : merkkijono (STring, yleensä sz) a : taulukko (Array) n : lukumäärä (couNt) c : lukumäärä (Count) b : tavu (Byte) f : lippu (Flag) WM_ : ikkunan viesti (Window Message) CM_ : komento viesti (Command Message) WC_ : ikkunan luonti (Window Create) WS_ : ikkunan tyyli (Window Style) msg : viesti (MeSsaGe) cmd : komento (CoMmaD) def : oletus (DEFault) br : sivellin, tyyli (BRush) cls : luokka (CLaSs) Esim: hDC : kahva ikkunan näyttöyhteyteen cbClsExtra : luokan ylimääräisten tavujen lkm hbrBackground : kahva taustan värin siveltimeen lpszMenuName : far-osoitin Menun nimi-merkkijonoon (C-tyyli)
Funktio esitellään Pascal-tyyppiseksi, koska alkuperäinen Windows on todennäköisesti kirjoitettu Pascal-kielellä. Lisäksi 8086-perustaisissa prosessoreissa Pascal-tyylinen parametrin välitys on aavistuksen nopeampaa kuin C-tyylinen (parametrit pinossa päinvastaisessa järjestyksessä ja Pascalissa aliohjelma siivoaa pinon), mutta toisaalta muuttuvia parametrilistoja ei voida tehdä (kuten esim. printf).
Pääfunktiolle tuodaan 4 parametria:
HINSTNACE hInstance, - nykyisen esiintymän kahva HINSTANCE hPrevInstance, - edellisen esiintymän kahva NULL jos ohjelma 1. kertaa käynnissä LPSTR lpCmdLine, - osoitin ohjelman kutsussa olleeseen merkkijonoon int nCmdShow - kertoo miten ikkuna pitää näyttää, mm: minimoituna koko ruudun täyttävänä (maksimoitu) normaalisti
Pääohjelman tehtävänä on tietenkin pyörittää koko sovellusta. Useimmiten pääohjelma alkaa ikkunaluokan (luokkien) rekisteröinnillä RegisterClass, jolla rekisteröidään tämän ohjelman ikkunaluokka (tai luokat). Kutsua ei tehdä, mikäli luokat on jo rekisteröity. Näin on esimerkiksi jos ohjelmasta jo on esiintymä (instance) käynnissä.
Lopuksi pääohjelmassa luodaan ikkuna ja pyöritetään viestisilmukkaa kunnes systeemi haluaa lopettaa ohjelman.
Esimerkki väärin nimeämisestä:
Ohjelman nimi .DEF-tiedostossa ---------------------------------- WHELLO.EXE NAME THELLO VÄÄRIN! THELLO.EXE NAME FHELLO 1. Käynnistetään WHELLO.EXE -> ajetaan WHELLO.EXE 2. Käynnistetään THELLO.EXE -> muistista löytyy NAME THELLO ohjelmasta WHELLO.EXE -> ajetaan muistissa oleva WHELLO.EXE uudelleen
Siis helpointa on pitää aina ohjelman .EXE (yleensä projektin nimi) ja .DEF -tiedoston NAME -nimi samoina, niin ei pääse tulemaan yllätyksiä. Tai koko NAME-parametri voidaan jättää pois, jolloin ohjelman nimenä käytetään sen .EXE-tiedoston nimeä.
Näiden eri objektien (älä sekoita liikaa olio-ohjelmointiin) erottamiseen tarvitaan jokin tunniste. Eräs tapa olisi määritellä kullekin objektille oma tietotyyppinsä ja käyttää sitten osoitinta tähän tyyppiin. Windowsin suunnittelijat ovat kuitenkin halunneet kätkeä ohjelmoijilta osan objektien ominaisuuksista, joten nämä tyypit säilytetään Windowsin sisäisesti. Ohjelmoija saa kutakin objektia varten yksikäsitteisen tunnisteen tai viitteen, josta käytetään jatkossa nimitystä kahva (handle).
Kahvat ovat yksinkertaisesti kokonaislukuja, joille Windows antaa arvon. Samaa tapaa käytettiin ennen myös MS-DOSin tiedostojen käsittelyssä. Tiedoston avauksen yhteydessä käyttöjärjestelmä antoi tiedostolle tunnuksen, tiedostokahvan, jonka perusteella ohjelmoija saattoi sitten myöhemmin viitata tähän tiedostoon.
Asiaa voitaisiin havainnollistaa vaikkapa siten, että kirjahyllyssä on useita kirjoja. Pyydämme kirjastonhoitajaa antamaan meille hyllystä kirjan "C-language". Antaessaan kirjan hoitaja ilmoittaa meille, että kirjan numero on sitten 123. Pyydämme toisen kirjan "C-ohjelmointikieli". Tälle kirjalle kirjastonhoitaja antaa tunnuksen 452 (usein aika mielivaltaisia). Palautamme "C-language" -kirjan tilapäisesti. Heti kohta tarvitsemme kirjaa uudelleen ja pyydämmekin nyt kirjaa 123 (ei tarvitse esitellä huonoa englannin ääntämistään).
Jos palauttaisimme kirjan pysyvästi (suljemme Windowsin ikkunan) ei kirjastossa numerot ehkä katoaisi, mutta Windowsissa suljettua ikkunaa (tai muuta vapautettua objektia) ei saa enää kutsua vanhalla kahvalla (eikä saman ikkunan uusi esiintymä mahdollisesti saa enää samaa kahvaa)!
if (!hPrevInstance) { /* Onko muita esiintymiä käynnisssä? */
Erityisesti kustakin eri ohjelmasta on käynnissä vain yksi koodi (80?86-prosessoreissa yksi koodisegmentti). Sen sijaan ohjelman toiselle esiintymälle luodaan uusi oma data-alue (80?86-prosessoreissa oma data-segmentti).
Joissakin tapauksissa voi olla järkevää kieltää ohjelman useampikertaiset esiintymät. Mikäli ohjelma yritetään käynnistää uudelleen, voidaan antaa varoitus tai siirtyä suoraan jo käynnissä olevaan ohjelmaan.
Esimerkkiohjelma ei alusta ikkunaluokkaa kuin ohjelman 1. esiintymälle, muuten ohjelma voi olla käynnissä useita kertoja.
hWnd = CreateWindow( "WHelloWClass", /* Tunnus jota RegisterClass() käytti 1 */ "Windows Hello", /* Ikkunan otsikoksi tulostuva teksti 2 */ WS_OVERLAPPEDWINDOW, /* Ikkunan tyyli (tavallinen) 3 */ CW_USEDEFAULT, /* Oletus x-paikka 4 */ CW_USEDEFAULT, /* Oletus y-paikka 5 */ CW_USEDEFAULT, /* Oletus leveys 6 */ CW_USEDEFAULT, /* Oletus korkeus 7 */ NULL, /* Pääikkunalla ei ole isä-ikkunaa 8 */ NULL, /* Mitä menua käytetään 9 */ hInstance, /* Tämä esiintymä omistaa ikkunan 10 */ NULL /* Ikkunanluontiparametrien osoite 11 */ );
Ensimmäinen parametri ilmoittaa luotavan ikkunaluokan. Tämä voi olla itse rekisteröity luokka tai esimerkiksi jokin seuraavista valmiista luokista:
BUTTON nappulan näköinen ikkuna COMBOBOX edit-ikkuna + valikkolista EDIT ikkuna, johon käyttäjä voi kirjoittaa tekstiä LISTBOX valikkolista SCROLLBAR liuku STATIC ikkuna, jossa voi olla jotakin vakiotekstiä, ei ota eikä lähetä viestiä
Ikkunan tyyli (3. param.) määritellään kokonaisluvulla, jossa päällä olevien bittien arvot määräävät ikkunan ominaisuuksia. Esimerkin vakio on määritelty:
#define WS_OVERLAPPEDWINDOW (WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | \ WS_THICKFRAME | WS_MINIMIZEBOX | WS_MAXIMIZEBOX)
Tässä bittikentät ovat vastaavasti (eri ikkunaluokille on lisäksi vielä muita luokkakohtaisia tyylejä):
/* Main window styles */ #define WS_CAPTION 0x00C00000L #define WS_BORDER 0x00800000L #define WS_DLGFRAME 0x00400000L #define WS_VSCROLL 0x00200000L #define WS_HSCROLL 0x00100000L #define WS_SYSMENU 0x00080000L #define WS_THICKFRAME 0x00040000L #define WS_MINIMIZEBOX 0x00020000L #define WS_MAXIMIZEBOX 0x00010000L #define WS_OVERLAPPED 0x00000000L #define WS_POPUP 0x80000000L #define WS_CHILD 0x40000000L
Seuraavassa kuva on leikattu näytöstä kun MHELLO -ohjelmaa on muutettu siten, että kaikki ikkunan ominaisuudet tulevat kerralla päälle (eli malliin vielä WS_VSCROLL ja WS_HSCROLL päälle):
Mikäli WS_CHILD ilmoitetaan päälle, on ikkunan paikkaa ilmoittavat koordinaatit suhteessa isä-ikkunaan, ei näytön nurkkaan. Tällöin on kuitenkin ilmoitettava isä-ikkunan kahva CreateWindow -kutsussa.
ShowWindow(hWnd, nCmdShow); /* Näytetään ikkuna */
Kutsun ensimmäinen parametri on näytettävän ikkunan kahva ja toinen parametri on näyttämistapa. Ensimmäisen kerran jälkeen tapana voi olla jokin seuraavista:
SW_HIDE ikkuna piilotettuna ja jokin muu ikkuna aktiiviseksi SW_MINIMIZE ikkuna ikonina ja listan ylin ikkuna aktiiviseksi SW_RESTORE aktivoi ja näyttää ikkunan (sama kuin SW_SHOWNORMAL) SW_SHOW aktivoi ikkunan ja näyttää sen nykyisessä koossa ja paikassa SW_SHOWMAXIMIZED aktivoi ikkunan ja näyttää kokoruudun kokoisena SW_SHOWMINIMIZED aktivoi ikkunan ja näyttää ikonina SW_SHOWMINNOACTIVE ikkuna ikonina ja aktiivisena oleva ikkuna jatkaa SW_SHOWNA ikkuna valitussa koossa ja aktiivisena oleva jatkaa SW_SHOWNOACTIVATE ikkunan viimeisimmässä koossaan ja aktiivinen jatkaa SW_SHOWNORMAL aktivoi ja näyttää ikkunan ja palauttaa sen alkup. koon
Tämä viesti voidaan lähettää ikkunalle esimerkiksi UpdateWindow kutsulla.
Tätä viestin "vetämistä" jonosta kutsutaan pull-model processing. Samaa menetelmää käytetään Apple MacIntoshissa.
Kun viesti on saatu, muutetaan viestissä mahdolliset näppäinkoodit vastaaviksi ASCII-koodeiksi kutsulla TranslateMessage. Malliohjelmassa näppäinkoodeja ei tarvita (itse asiassa hyvin harvassa Windows-ohjelmassa tarvitaan), joten TranslateMessage on oikeastaan tarpeeton.
Viestin käsittely suoritetaan kutsulla DispatchMessage, jolla viesti lähetetään sille ikkunalle, jolle viesti kuului. Nyt Windows ikkunaluokassa määritellyn funktion osoitteen perusteella kutsuu tätä funktiota. Esimerkissämme siis funktiota MainWndProc. Tällaista "väkisin" kutsumista viestin avulla nimitetään push-model processing.
Itse asiassa ikkuna voi saada viestejä jo ennen viestisilmukkaan saapumista. Esimerkiksi CreateWindow aiheuttaa ikkunalle WM_CREATE -viestin, jonka käsittelyssä voitaisiin - ja kannattaakin - tehdä kaikki ikkunan alustamiseen kuuluvat toimenpiteet.
Viestisilmukka päättyy kun GetMessage palauttaa arvon 0. Tällöin myös pääohjelman on syytä loppua. Pääohjelman palauttamaa arvoa ei nykyisessä Windowsin versiossa käytetä hyväksi, mutta esim. debuggerin kanssa voi olla hyödyllistä palauttaa Windowsin viimeiseen viestiin jättämä arvo.
Windowsin tapauksessa nämä funktiot voitaisiin esitellä myös FAR PASCAL -tyyppisiksi (FAR = ohjelmakoodi voi olla toisessa segmentissä), mutta CALLBACK on siirrettävämpi:
LONG CALLBACK _export MainWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
Jos _export jätetään pois, pitää esittely suorittaa .DEF -tiedostossa:
EXPORTS MainWndProc @1
Funktio saa parametrinaan käsiteltävän ikkunan kahvan, viestin numeron ja kaksi lisäparametria viestin mahdollisesta lisäarvosta (hiiren paikka, näppäimen koodi yms.).
Funktion pitää osata käsitellä ainakin viesti WM_DESTROY, jolla Windows toivoo koko ohjelman lopettamista. Mikäli ennen ohjelman lopettamista pitää tallettaa tiedostoja tai muuta sellaista, on nyt jo myöhäistä. Tällöin pitäisi jo WM_CLOSE viestin yhteydessä antaa viimeinen varoitus tiedostojen tallettamisesta. Kun ikkunan sulkeminen hyväksytään, kutsutaan PostQuitMessage -funktiota.
Kaikki muut viestit voidaan jättää Windowsin oletuskäsittelijälle DefWindowProc.
Esimerkissä haluamme kuitenkin piirtääkin näyttöön jotakin, eli tekstin "Hello World". Yksinkertaisissa Windows-ohjelmissa koko kuvaruudun piirto kannattaa tehdä WM_PAINT-viestin saapuessa:
case WM_PAINT: /* Viesti: Piirrä ikkuna uudelleen */ if ( BeginPaint(hWnd,&ps) ) TextOut(ps.hdc, 10, 10, "Hello World!",12); EndPaint(hWnd,&ps); return NULL;
tai (huonompi):
case WM_PAINT: /* Viesti: Piirrä ikkuna uudelleen */ if ( ( hDC = GetDC(hWnd) ) != NULL ) { TextOut(hDC, 10, 10, "Hello World!",12); ReleaseDC(hWnd,hDC); } break; /* Koska oletuskäsittely täytyy nyt tehdä! */
hDC = GetDC(hWnd);
Tällöin piirtoattribuuteiksi tulee oletusarvot. WM_PAINT -viestin käsittelyssä laiteyhteys on syytä ottaa BeginPaint -kutsun avulla. Laiteyhteyden luomisen jälkeen voidaan oletusarvoja muuttaa esimerkiksi kutsuilla:
SetROP2(hdc,R2_NOT); /* Set Raster OPeration TO ... Raster op to NOT */ SetTextColor(hDC,tcolor); SetBkColor(hDC,bcolor); SetTextAlign(hDC, textalign);
HUOM! Monesti "jenkit" intoutuvat käyttämään nimissä sanaleikkejä "to = two = 2" ja "for = four = 4" (esim. Car 4 sale tai 2 fast 4 you!).
Itse piirtämistä varten on joukko kutsuja kuten esimerkiksi:
TextOut(hDC, 10, 10, "Hello World!",12); Ellipse(hDC, 40, 40, 80, 80); MoveToEx(hDC, 60, 80, NULL); LineTo(hDC, 60,180); SetPixel(hDC, 15, 20, RGB(0,0,0));
Piirtämisen lopuksi pitää GetDC:llä luotu laiteyhteys poistaa kutsulla
ReleaseDC(hWnd,hDC);
muuten muistiin jää varaus laiteyhteydestä vaikka sen kahva on jo hukattu aliohjelmasta poistuttaessa. BeginPaintiä täytyy vastata aina EndPaint -pari. EndPaint ilmoittaa Windowsille, että WM_PAINT -viesti on käsitelty. Ilman EndPaint -ilmoitusta Windows lähettää ikkunalle kohta uuden WM_PAINT -viestin.
Piirtämisen oletuskoordinaatit ovat fyysisiä pikseleitä origon ollessa ikkunan vasemmassa yläkulmassa ja y-koordinaatin arvon kasvaessa alaspäin (client area coordinates). Hiiri palauttaa myös näitä koordinaatteja.
Koordinaatisto voidaan tarvittaessa muuttaa paremmin sovellusta vastaavaksi.
WM_PAINT -viesti voi tulla joko koko ikkunaa varten tai siinä voi olla koordinaatit suorakaiteelle, joka on pilaantunut ja pitää näin ollen piirtää uudelleen. Usein yksinkertaisissa Windows ohjelmissa on helpointa piirtää aina koko näyttö uudelleen. Joskus on jopa mahdotonta laskea mikä osa piirtoalgoritmista piirtäisi vain pilaantuneeseen suorakaiteeseen.
Oikeissa Windows-ohjelmissa näyttöön piirretyistä objekteista täytyy pitää listaa, jotta koko näyttö (tai sen osa) mahdollisesti osataan piirtää uudelleen.
Toinen tapa olisi lukea aina piirtämisen jälkeen ikkunan alla oleva pikselikuvio muistiin ja tulostaa tämä kuvio uudelleenpiirtoviestin yhteydessä. Menetelmä ei kuitenkaan toimi, mikäli ikkunaa isonnetaan. Tällöin piirtäminen jouduttaisiin tekemään omaan pikselikarttaan, josta aina tarvittava osa tulostettaisiin näyttöön.
Ikkunan uudelleen piirtäminen onkin eräs Windows-ohjelmoinnin kaikkein vaikeimpia ongelmia.
Piirtämistä voidaan tietenkin tehdä muuallakin kuin WM_PAINT viestissä. Esimerkiksi hiiren napin painalluksella voidaan piirtää viiva edellisestä pisteestä uuteen jne. Tällöin täytyy kuitenkin päivittää myös tietorakennetta josta WM_PAINT osaa sitten piirtää myös lisätyt osat ruudun uudelleen piirron yhteydessä.
WM_PAINT -viestissä pitää aluksi "avata" piirtäminen kutsulla:
BeginPaint(hWnd,&ps)
lopuksi piirtäminen pitää sulkea kutsulla:
EndPaint(hWnd,&ps);
Tässä ps on piirtotyyppi, jossa on:
typedef struct tagPAINTSTRUCT { /* ps */ HDC hdc; /* avattu laiteyhteys piirtämistä varten */ BOOL fErase; /* Pitääkö tausta piirtää uudelleen */ RECT rcPaint; /* Alue, joka pitää piirtää uudelleen */ BOOL fRestore; /* varattu Windowsin sis. */ BOOL fIncUpdate; /* varattu Windowsin sis. */ BYTE rgbReserved[16]; /* varattu Windowsin sis. */ } PAINTSTRUCT;
Laiteyhteys voitaisiin tietysti avata myös GetDC -kutsulla, mutta tällöin WM_PAINT viesti ei poistuisi jonosta. Siis WM_PAINT viestiin pitää aina liittyä BeginPaint - EndPaint -kutsupari! Joissakin ohjelmissa laiteyhteys avataan kuitenkin GetDC -kutsulla, mutta tällöin kutsutaan piirtämisen jälkeen oletuskäsittelijää, joka lopulta sisältää mainitun kutsuparin.
Kun ohjelman ensimmäinen esiintymä käynnistetään, luodaan tarvittava ikkunaluokka. Seuraavat esiintymät käyttävät tätä samaa ikkunaluokkaa. Ikkunaluokka pysyy Windowsin muistissa niin kauan kuin yksikin saman ohjelman esiintymä on käynnissä. Siis esiintymiä ei tarvitse sulkea pinomaisesti käynnistämisjärjestyksessä!
Näin meidän tarvitsee uutta ohjelmaa varten kirjoittaa vain tiedosto, johon tulee ikkunan sisälle piirrettävä koodi. Tätä ohjelmaa kutsutaan WM_PAINT -viestin saapuessa.
#include <windows.h> char *WindowName = "Hello World"; void MyDraw(HWND hWnd,HDC hDC) { TextOut(hDC, /* Laiteyhteys, johon tulostetaan */ 10,10, /* x ja y-koordinaatti suhteessa ikkunaan */ "Hello World!",12 /* Tulostettavan tekstin osoite ja tekstin */ /* pituus */ ); }
Nyt olisi Windows-ohjelmointi helppoa! Ongelmat selviävät myöhemmissä esimerkeissä. Kääntämistä varten täytyy tehdä projekti, jossa on mukana pääohjelma (simplew.c), piirtämisen suorittava aliohjelma (esimerkissä swhello.c) ja moduulin määrittelytiedosto (simplew.def).
Laiteyhteys (hDC) tuodaan kutsuvalta ohjelmalta parametrina, samoin kahva ikkunaan (hWnd).
TextOut -funktio vaatii parametrikseen laiteyhteyden kahvan, tekstin aloittamiskoordinaatin (suhteessa ikkunaan), tulostettavan tekstin (osoitteen, LPSTR) ja sen pituuden. Mikäli tulostettava teksti on muuttujassa, voidaan tekstin pituus tietysti selvittää strlen-funktiolla.
#include <windows.h> char *WindowName = "Tikku-ukko"; void MyDraw(HWND hWnd,HDC hDC) { Ellipse(hDC,40,40,80,80); /* Pää */ MoveToEx(hDC, 60, 80, NULL); LineTo(hDC, 60,180); /* Keskivartalo */ LineTo(hDC, 20,260); /* Vasen jalka */ MoveTo(hDC, 60,180); LineTo(hDC,100,260); /* Oikea jalka */ MoveTo(hDC, 20,170); LineTo(hDC, 60, 90); /* Vasen käsi */ LineTo(hDC,120, 40); /* Oikea käsi */ }
Ympyrä piirretään siis ellipsinä. Ellipsistä annetaan ympäröivän suorakaiteen nurkkakoordinaatit. Viivat piirretään siirtämällä "kynä" viivan aloituspisteeseen ja vedetään sitten viiva. Viivoja voidaan piirtää myös useampia samalla kertaa (vrt. vasen ja oikea käsi).
pt = MAKEPOINT(lParam);
Tarvittaessa voidaan käyttää myös LOWORD ja HIWORD -makroja:
x = LOWORD(lParam); y = HIWORD(lParam);
Koska piirtofunktiot haluavat yksittäisiä koordinaatteja, ei pistetyyppisiä pareja, voidaan tehdä myös käänteinen makro:
#define P2(pt) (pt).x,(pt).y /* Pisteestä 2 erillistä lukua */
jota voidaan tarvittaessa kutsua:
MoveToEx(hDC,P2(alkupiste),NULL); LineTo(hDC,P2(loppupiste));
Jos oletamme, että laiteyhteyden nimenä käytetään "aina" hDC, voitaisiin edelliset kirjoittaa myös muotoon:
#define P2(pt) hDC,(pt).x,(pt).y /* Pisteestä 2 erillistä lukua */
jota voidaan tarvittaessa kutsua:
MoveToEx(P2(alkupiste),NULL); LineTo(P2(loppupiste));
Mikäli teemme edellistä yksinkertaista runkoa vastaavan rungon, jossa ikkunan funktio käsittelee myös hiiren viestejä, voimme kirjoittaa vastaavia yksinkertaisia ohjelmia, joissa jotenkin reagoidaan hiiren liikkeisiin.
Mikäli hiiren liikkeiden mukaan halutaan piirtää jotakin näyttöön, pitää nyt muistaa avata laiteyhteys GetDC ja sulkea ReleaseDC -kutsuilla.
Esimerkiksi POINT -tyyppi koostuu kahdesta kokonaisluvusta. WIN32:ssa siis kahdesta 32-bittisestä osasta. Koska lParam on 32-bittinen, on tästä vaikea saada kahta 32-bittistä osaa, kun taas kaksi 16-bittistä osaa (= MAKEPOINT) saadaan varsin helposti. Jatkossa tyydymmekin ehkä laitekoordinaattien riittäessä käyttämään POINTS (point short) -tyyppiä. Windows 3.1:een on helppo määritellä makro (ALI\PORTABLE.H)
#define POINTS POINT
Vastaavasti WIN32:sta löytyy valmis MAKEPOINTS -makro, ja Windows 3.1:een sen tekeminen on jälleen helppoa.
WIN32:sta ei löydy funktiota MoveTo(x,y), vaan on käytettävä funktiota MoveToEx, joka siirtämisen lisäksi palauttaa paikan edelliseen nykypisteeseen. Tämän paluuarvon osoitin voi olla myös NULL, joten funktiota (joka löytyy molemmista liittymistä) voidaan kutsua:
MoveToEx(x,y,NULL);
HUOM! Tarkista aina ennenkuin teet mitään, mitä toimenpiteestä sanotaan:
1. Avustuksessa (Help),
1. WINDOWSX.H:ssa ja
1. ALI\PORTABLE.H:ssa!
LONG CALLBACK _export MainWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { PAINTSTRUCT ps; switch (message) { case WM_PAINT: /* Viesti: Piirrä ikkuna uudelleen */ if ( BeginPaint(hWnd,&ps) ) MyDraw(hWnd,ps.hdc); EndPaint(hWnd,&ps); return (NULL); case WM_LBUTTONDOWN: /* Hiiren vasen nappi alas: */ MyDown(hWnd,wParam,lParam); return (NULL); case WM_LBUTTONUP: /* Hiiren vasen nappi ylös: */ MyUp(hWnd,wParam,lParam); return (NULL); case WM_MOUSEMOVE : /* Hiirtä siirretty: */ MyMove(hWnd,wParam,lParam); return (NULL); case WM_DESTROY: /* Viesti: ikkuna hävitetään */ PostQuitMessage(0); return (NULL); default: /* Antaa Windowsin käsitellä muut */ break; } return (DefWindowProc(hWnd, message, wParam, lParam)); }
Ratkaisun huono puoli on tietysti se, että käsiteltävien viestien määrän lisääntyessä joudutaan aina muuttamaan myös "runko"-ohjelmaa.
/**************/ /* swhello.c */ /**************/ #include <windows.h> #include "simplet.h" /****************************************************************************/ LONG MyDraw(tMSGParam *msg) { TextOut(msg->hDC, 10,10, "Hello World!",12); return NULL; } /****************************************************************************/ LONG MyCreate(tMSGParam *msg) /* Aliohjelmaa kutsutaan kun ikkuna on luotu, muttei vielä näytössä. */ { SetWindowText(msg->hWnd,"Hello World for Windows"); MoveWindow(msg->hWnd,10,10,300,200,FALSE); return NULL; } /****************************************************************************/ /* Viestien käsittelytaulukko */ /****************************************************************************/ tMSGEntry MsgTbl[] = { { WM_PAINT ,DoC, DoC, MyDraw , 1 }, { WM_CREATE ,DoC, DoC, MyCreate, 0 }, { 0 } }; /****************************************************************************/
Esimerkiksi ukonpiirto-ohjelma kokonaisuudessaan taulukkokäsittelijän avulla olisi seuraavanlainen:
/**************/ /* draw_man.c */ /**************/ /* Project: drawman.c, simplet.def, ALI\tabhand.c */ #include <windows.h> #include "tabhand.h" /***************************************************************************/ TblClassSWindowMAIN("StickManClass",0,"Tikku-ukko",MsgTbl,0); /***************************************************************************/ /****************************************************************************/ static EVENT WM_paint(tMSGParam *msg) /* # MAKE_DC # */ { Ellipse( msg->hDC,40,40,80,80); /* Pää */ MoveToEx(msg->hDC, 60, 80,NULL); LineTo( msg->hDC, 60,180); /* Keskivartalo */ LineTo( msg->hDC, 20,260); /* Vasen jalka */ MoveToEx(msg->hDC, 60,180,NULL); LineTo( msg->hDC,100,260); /* Oikea jalka */ MoveToEx(msg->hDC, 20,170,NULL); LineTo( msg->hDC, 60, 90); /* Vasen käsi */ LineTo( msg->hDC,120, 40); /* Oikea käsi */ return 0; } /****************************************************************************/ static EVENT WM_create(tMSGParam *msg) /* Aliohjelmaa kutsutaan kun ikkuna on luotu, muttei vielä näytössä. */ { MoveWindow(msg->hWnd,10,10,200,300,FALSE); return 0; } /****************************************************************************/ /* Viestien käsittelytaulukko */ /****************************************************************************/ #define DoC DONT_CARE tMSGEntry MsgTbl[] = { EV_HANDLE_WM_DESTROY, { WM_PAINT , DoC , DoC , WM_paint, MAKE_DC }, /*a*/ { WM_CREATE , DoC , DoC , WM_create }, /*a*/ { 0 } }; /****************************************************************************/
Makro TblClassSWindowMain (taulukkokäsittelijän Tbl luokallinen Class yksinkertainen Simple ikkuna Window ja pääohjelma Main) tekee kaikki pääohjelmassa tarvittavat toimenpiteet (mm. ikkunaluokan rekisteröinti, ikkunan luominen ja viestisilmukka). Muita tapauksia varten löytyy ALI\TABHAND.H:sta lisää makroja.
Nimeämällä käsittelijäfunktiot sopivasti (esim. WM_paint käsittelemään piirtoviesti), voidaan automatisoida käsittelytaulukon luominen ja sopivassa ympäristössä ohjelmoijan tarvitseekin kirjoittaa edelliseen ohjelmaan vain ko. kaksi WM_ -alkuista funktiota (ks. \BAT\RESP.BAT , \BAT\SED\RESTABLE.SED ja OLDTABL.SED). Jos edellä ikkunan oletuskoko kelpaisi, voitaisiin myös WM_create jättää pois.
Taulukon DoC (= Don't Care) sarakkeet tulevat käyttöön myöhemmin. Käsittelijäfunktiolle tuodaan parametrina vain yksi tietue, josta löytyy mm. kaikki normaalin ikkunafunktion saamat parametrit. Lisäksi tietueessa on valmiina eri tilanteisiin sopivasti käsiteltyjä arvoja. Esimerkiksi valmis laiteyhteys - jos sitä halutaan -, tiettyjä omia lisäparametreja sekä myös liukujen (scroll bars) käyttöä helpottavia tietoja.. Käsittelijään voidaan haudata myös valmiita oletuskäsittelyjä. Esimerkissä EV_HANDLE_WM_DESTROY tekee ikkunan hävittämisessä tarvittavat toimenpiteet. Mallin mukaan ohjelmoija voi rakentaa itselleen omat oletuskäsittelijät, jotka on sitten helppo liittää jokaiseen ohjelmaan.
int muuta_kaden_suunta(HDC hDC, LONG lParam, kasi_tyyppi *kasi) { int dx,dy; POINTS pt = MAKEPOINTS(lParam); PiirraNotKasi(hDC,kasi); /* Pyyhitään vanha käsi pois */ dx = pt.x - kasi->alku.x; dy = pt.y - kasi->alku.y; if ( dx || dy ) kasi->suunta = atan2(-dy,dx); PiirraNotKasi(hDC,kasi); /* Piirretään uusi oikea käsi tilalle */ return 1; }
Tässä PiirraNotKasi on vastaavasti:
#define POINT_TO_2(pt) pt.x,pt.y /* Pisteestä 2 erillistä lukua */ #define POINT_PLUS_RA(pt,r,a) \ pt.x+r*cos(a)+0.5,pt.y-r*sin(a)+0.5 /* Piste + r*suunta */ void PiirraNotKasi(HDC hdc,kasi_tyyppi *kasi) { SetROP2(hdc,R2_NOT); MoveToEx(hdc,POINT_TO_2(kasi->alku),NULL); LineTo(hdc,POINT_PLUS_RA(kasi->alku,kasi->pituus,kasi->suunta)); }
SetROP2 -funktio (Set Raster OPeration TO) valitsee käytettävän piirtotavan (drawing mode). Valittavissa on:
R2_BLACK Pisteestä tulee musta (Raster operation TO BLACK) R2_WHITE Pisteestä tulee valkea R2_NOP Pisteen väri ei muutu R2_NOT Pisteen väri muuttuu päinvastaiseksi (~screen pixel) R2_COPYPEN Pisteestä tulee kynän värinen (pen) R2_NOTCOPYPEN Pisteestä tulee kynän väriä päinvastainen (~pen) R2_MERGEPENNOT final pixel = (~screen pixel) | pen R2_MASKPENNOT final pixel = (~screen pixel) & pen R2_MERGENOTPEN final pixel = (~pen) | screen pixel R2_MASKNOTPEN final pixel = (~pen) & screen pixel R2_MERGEPEN final pixel = pen | screen pixel R2_NOTMERGEPEN final pixel = ~(pen | screen pixel) R2_MASKPEN final pixel = pen & screen pixel R2_NOTMASKPEN final pixel = ~(pen & screen pixel) R2_XORPEN final pixel = pen ^ screen pixel R2_NOTXORPEN final pixel = ~(pen ^ screen pixel)
Viisi ensimmäistä ovat ehkä yleiskäyttöisempiä. Valittu tapa on käytössä kunnes laiteyhteys vapautetaan tai tapa muutetaan uudelleen. Mikäli aliohjelman ei ole tarkoitus muuttaa tapaa pysyvästi, pitäisi vanha tyyli tallettaa ennen piirtämistä ja palauttaa piirtämisen jälkeen:
vanha = SetROP2(...); ... piirtäminen ... SetROP2(vanha);
Malliohjelmassa SHOWMAN laiteyhteys vapautetaan aina käden piirtämisen jälkeen (tabhand.c hoitaa varaamisen ja vapauttamisen), joten siksi vanhaa tapaa ei ole tarvinnut tallettaa (hyvä ohjelmointitapa olisi kuitenkin ollut varalta tallettaa vanhakin piirtotapa).
Kynän väri voidaan muuttaa vaikkapa siniseksi seuraavasti:
HPEN hpen, hpenOld; hpen = CreatePen(PS_SOLID, 6, RGB(0, 0, 255)); hpenOld = SelectObject(hDC, hpen); Rectangle(hDC, 10, 10, 100, 100); SelectObject(hDC, hpenOld); DeleteObject(hpen);
CreatePen -kutsussa ensimmäinen parametri määrää kynän tyylin, joka voi olla jokin seuraavista:
PS_SOLID PS_DASH PS_DOT PS_DASHDOT PS_DASHDOTDOT (vain jos leveys on 1) PS_NULL PS_INSIDEFRAME (esim. suorakaide piirretään leveänäkin viivana raamin kokonaan sisäpuolelle)
Kutsun toinen parametri on viivan leveys ja viimeinen kynän väriä ilmaiseva luku, joka usein muodostetaan RGB -makrolla (Red-Green-Blue[1], kunkin värin suhteellinen osuus 0-255). Jokainen luotu objekti on lopulta hävitettävä ennen ohjelmasta poistumista. Eräs yleinen tapa on tehdä (globaalien) objektien luominen pääikkunan funktiossa WM_CREATE -viestin saapuessa ja poistaa ne WM_DESTROY -viestin tullessa:
static HPEN sininen,vihrea,punainen,turkoosi,violetti,keltainen; ... static EVENT WM_create(... sininen = CreatePen(PS_SOLID,1,RGB(0,0,255)); vihrea = CreatePen(PS_SOLID,1,RGB(0,255,0)); punainen = CreatePen(PS_SOLID,1,RGB(255,0,0)); turkoosi = CreatePen(PS_SOLID,1,RGB(0,255,255)); violetti = CreatePen(PS_SOLID,1,RGB(255,0,255)); keltainen = CreatePen(PS_SOLID,1,RGB(255,255,0)); ... static EVENT WM_destroy(... DeleteObject(sininen); DeleteObject(vihrea); Delete... ... }
Muutamia valmiitakin kyniä on saatavilla, esimerkiksi:
musta = GetStockObject(BLACK_PEN); valkoinen = GetStockObject(WHITE_PEN); ... vanha = SelectObject(hDC,musta); ... piirto ... SelectObject(hDC,vanha);
Vastaavasti valitaan myös alueen täyttöihin vaikuttava sivellin (brush). Esimerkiksi "ukon" päästä tulisi musta jos piirtofunktio alkaisi:
... SelectObject(hDC,GetStockObject(BLACK_BRUSH)); Ellipse(...); ...
Punainen pää piirrettäisiin seuraavasti:
... HBRUSH hBrRed,vanha; hBrRed = CreateSolidBrush(RGB(255,0,0)); vanha = SelectObject(hDC,hBrRed); Ellipse(hDC,40,40,80,80); /* Pää */ SelectObject(hDC,vanha); DeleteObject(hBrRed); ...
void InitMy(HWND hWnd) { SetTimer(hWnd, /* Ikkunan kahva, johon ajastimen toim. kohd. */ 1, /* Ajastimen numero (voi olla monta eri ajast. */ AIKA_VALI, /* Aikaväli millisekunteina */ NULL); /* Käsittelevä CALLBACK funktio tai NULL viest.*/ }
static EVENT WM_timer_1(tMSGParam *msg) /* # MAKE_DC # */ /* Aliohjelmaa kutsutaan kun valittu aika on tullut täyteen */ { ... muuta_raajan_suunta_rad(msg->hDC,Vasen_kasi->suunta-M_PI/45.0,Vasen_kasi); ... } ... tMSGEntry MsgTbl[] = { ... { WM_TIMER , 1 , DoC , WM_timer_1, MAKE_DC }, /*a*/ ...
Kuva piirtyy vaikkapa seuraavalla koodilla:
#define PIENIN_KOLMIO 0.5 /* Säädä tällä kauanko koko kuvan piirt. kest. */ #define I(x,y) hdc,((int)(x)),((int)(y)) void kolmio(HDC hdc, double x, double y, double h) { double s2 = h / (sqrt(3)); MoveToEx(I(x,y),NULL); LineTo(I(x-s2,y-h)); LineTo(I(x+s2,y-h)); LineTo(I(x,y)); if ( h < PIENIN_KOLMIO ) return; kolmio(hdc,x-s2,y,h/2); /* Pienempi kolmio vasemmalle */ kolmio(hdc,x+s2,y,h/2); /* Pienempi kolmio oikealle */ kolmio(hdc,x,y-h,h/2); /* Pienempi kolmio yläpuolelle*/ } /****************************************************************************/ static EVENT WM_paint(tMSGParam *msg) /* # MAKE_DC # */ { char s[100]; SetWindowText(msg->hWnd,"Kolmio: Piirretään..."); start_timer(1); kolmio(msg->hDC,300,400,200); sprintf(s,"Kolmio: %5.2lf s.",stop_timer(1)); SetWindowText(msg->hWnd,s); return NULL; }
Alkeellinen tapa hoitaa kauan kestävä piirtäminen on laittaa näyttöön tiimalasi piirtämisen ajaksi:
SetCapture(hWnd); /* Estetään hiiren toiminta muissa íkkunoissa */ hSaveCursor = SetCursor(hHourGlass); /* ... pitkään kestävä piirtäminen ... */ SetCursor(hSaveCursor); ReleaseCapture(); /* Sallitaan hiiren toiminta muissa ikkunoissa*/
Käyttäjää tämä ei kuitenkaan tee iloiseksi, jos vahingossa siirtää ikkunoitaan niin, että minuutin kestävä peruuttamaton piirtäminen käynnistyy uudelleen. Palaamme ongelmaan kohta uudelleen.
Malliohjelmassa uudelleen piirtäminen ei aina vie samaa aikaa. Piirtämisen aika riippuu siitä, kuinka paljon ikkunasta on "pilattu". Miksi, koska ohjelmamme ei selvästikään ota kantaa pilaantuneen alueen kokoon (eikä kokoa helposti tässä esimerkissä otetakaan huomioon!) ?
Vastaus on siinä, että BeginPaint on rajannut ikkunalle tulevat piirtämiset tapahtumaan vain pilaantuneeseen (invalidate) alueeseen. Siis vaikka ohjelmamme piirtää koko kuvan, ei fyysiselle näytölle piirretä kuin pilaantuneeseen alueeseen. Aikaeroista voimme karkeasti arvioida näytönohjaimen ja Windowsin suhdetta suorituskykyyn.
static POINTS old = {0,0}; static pen_down = 0; /****************************************************************************/ static EVENT WM_lbuttondown(tMSGParam *msg) { old = MAKEPOINTS(msg->lParam); pen_down = 1; return 0; } /****************************************************************************/ static EVENT WM_lbuttonup(tMSGParam *msg) { pen_down = 0; return 0; } /****************************************************************************/ static EVENT WM_mousemove(tMSGParam *msg) /* # MAKE_DC # */ { POINTS pt = MAKEPOINTS(msg->lParam); if ( !pen_down ) return 0; MoveToEx(msg->hDC,old.x,old.y,NULL); LineTo(msg->hDC,pt.x,pt.y); return 0; }
Siis kaikki hiirellä tapahtuva piirtäminen pitäisi tallettaa piirtämisen lisäksi jonkinlaiseen tietorakenteeseen WM_PAINT -viestiä varten.
Ohjelmoijan kannalta ehkä helpointa on suunnitella kokonaisuus siten, että pitkissä silmukoissa tarvitsisi vain kutsua jotakin funktiota, joka palauttaa tiedon siitä, pitääkö piirtäminen lopettaa vai ei.
void kolmio(HDC hdc, double x, double y, double h) { double s2 = h / (sqrt(3)); if ( CheckMessage() ) return; /* Tämä mahdollistaa "moniajon"!!!!!!!! */ MoveTo(I(x,y)); LineTo(I(x-s2,y-h)); LineTo(I(x+s2,y-h)); LineTo(I(x,y)); if ( h < PIENIN_KOLMIO ) return; kolmio(hdc,x-s2,y,h/2); /* Pienempi kolmio vasemmalle */ kolmio(hdc,x+s2,y,h/2); /* Pienempi kolmio oikealle */ kolmio(hdc,x,y-h,h/2); /* Pienempi kolmio yläpuolelle*/ }
Lisäksi "pääohjelma" täytyy vaihtaa makroksi:
TblClassSWindowMAIN_C
Vielä parempi ratkaisu olisi jos pääfunktiossa voitaisiin toteuttaa:
... AloitaPiirto(keskeytys); kolmio(...) keskeytys: LopetaPiirto(); ...
CheckMessage -kutsuja suoritettaisiin sitten kellokeskeytyksillä, ilman että piirto-ohjelman kirjoittajan täytyy kiinnittää keskeytykseen mitään huomioita. Ongelmaksi tulee mahdollisen monimutkaisen kutsu- ja/tai -tietorakenteen purkaminen. Kuitenkin nytkin esitetyllä tavalla pärjää ja ohjelmoijalle jää tarkempi kontrolli siitä, milloin muut saavat prosessointiaikaa.
Windowsin moniajohan antoi muille aikaa vain GetMessage tai PeekMessage -kutsujen aikana. Koska CheckMessage -kutsu on toteutettu PeekMessage -kutsulla, saavat muut prosessit aikaa jokaisen CheckMessage -kutsun aikana. Ohjelmoijan on vain muistettava sijoitella kutsuja riittävän tiheästi. Ainakin jokaiseen pitemmän aikaa kestävään silmukkaan. Muutaman viivan piirrosta ei tarvitse olla huolissaan. Noin 1 sekunnin viiveen käyttäjä voi ehkä sietää (mutta tämäpä riippuu koneesta!).
Toinen ongelma tulee prosessin lopettamisesta. Mikäli lähetettäisiin vain PostQuitMessage -viesti kesken piirtämisen, saattaa osa tietorakenteista jäädä purkamatta. Siis varsinaisen piirtorutiinin täytyy antaa palata normaalissa järjestyksessä ja prosessin lopettaminen suoritetaan sitten "viivästetysti".
Esimerkin ratkaisussa ei (varmaankaan) ole käsitelty kaikkia mahdollisia viestejä, joissa piirtäminen on lopetettava, mutta malliksi on ainakin kolme: WM_PAINT, WM_SIZE, WM_CLOSE.
CHECK_CHECKER_WIN(hWnd,message,wParam,lParam);
Piirtäminen lopetetaan EndPaint-kutsun sijasta CheckEndPaint-kutsulla (tabhand.c hoitaa molemmat muutokset, jos sitä käytetään), jotta tarkistaja on perillä siitä, saatiinko piirtäminen suoritettua rauhassa loppuun vai ei.
Ongelmia jää kuitenkin vieläkin. Ikkuna saa ylimääräisiä WM_PAINT -viestejä kesken piirron vaikkei ikkunan niitä kuuluisikaan saada. Tämä aiheuttaa ylimääräisen piirron alusta aloittamisen (Windows 3.0:ssa vika ei näyttäisi esiintyvän, siis vika saattaa olla Windows 3.1:ssä???).
Moniajo-ongelma ratkeaa kyllä paremmissa käyttöjärjestelmissä kuten Unix, OS/2, Windows 95 tai Windows NT. Mutta nekään eivät auta keskeyttämisongelmaan ellei sitä erikseen huomioida! Mutta voihan sitä mennä toiseen ikkunaan formatoimaan korppuja silläaikaa kun WP piirtää esikatselusivua.
[1] RGB -värejä voi kokeilla helpoimmin Windowsin Control Panelin Color-toiminnolla: Color Palette - Define Custom Color