previous next Title Contents

3. Yksinkertaiset Windows-ohjelmat


Pääaiheet:

* 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.

3.1 EasyWin (WHELLO\HELLO.C, WHELLO\EHELLO.C)

On myös olemassa kirjastoja, joilla vain printf -tulostusta käyttävä DOS-ympäristöön tehty C-ohjelma voidaan kääntää Windowsin ikkunaan tulostavaksi versioksi. Tällöin ohjelmaan jää poikkeuksellisesti main -funktio. Lukija voi itse kokeilla yksinkertaisia C-ohjelmia esimerkiksi Borland-C++/Windowsin EasyWin -kirjaston avulla. Tällöin vain kirjoitetaan tavallinen C-ohjelma ja ajetaan se. Linkittämisen yhteydessä saattaa kuitenkin tulla varoitus:

	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ä.

3.2 "Hello World" oikeana Windows-versiona

Seuraavaksi esitämme "oikean" Windows-ohjelman, joka luo näyttöön uuden ikkunan ja tulostaa siihen tekstin "Hello World!". Ikkuna suljetaan (eli ohjelma lopetetaan) valitsemalla systeeminappulan close (Alt-Spacebar Close) tai Alt-F4.

Yksinkertainenkin Windows-ohjelma tarvitsee projektin, jossa on tarvittavat C-ohjelmat sekä .DEF -tiedosto.

3.2.1 .DEF-tiedosto (WHELLO\WHELLO.DEF)

Koska C-kielessä ei ole varattu mahdollisuutta tallettaa ohjelman suoritettavaan versioon tiettyjä otsikkotietoja, täytyy Windows-ohjelmia varten aina kirjoittaa määrittelytiedosto, josta selviää osa ohjelman tarvitsemista resursseista. Esimerkkiohjelmaamme varten voisi olla kirjoitettu vaikkapa seuraava 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.

3.2.2 C-ohjelma (WHELLO\MHELLO.C)

C-ohjelma sisältää kaikki ohjelman tekemät toimenpiteet ja se on aivan tavallista C-kieltä. Windowsia käytetään API-rajapinnan kautta (Application Interface).

	/*****************************************************************************
	  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.

3.2.3 Nimeämisperusteet

Windows-ohjelmoinnissa tunnisteet nimetään yleensä tavalla, jota joskus kutsutaan unkarilaiseksi nimeämistavaksi (Hungarian Naming) tavan aloittajan Charles Simonyi'n kansallisuuden mukaan.

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)

3.2.4 Windows pääohjelma (WinMain)

Windows-ohjelmissa ei kirjoiteta lainkaan main -funktiota. Tämä funktio on aina valmiina Windows-kirjastossa. Sen sijaan AINA täytyy kirjoittaa funktio WinMain. Windows kutsuu tarvittavien alustusten jälkeen tätä funktiota (vertaa nappulat3.c).

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.

3.2.5 Esiintymän tunnistaminen

Ohjelman esiintymä etsitään .EXE -tiedoston nimen mukaan (esimerkissä WHELLO.EXE) mukaan. Ohjelmakoodista ajetaan aina vain yksi esiintymä ja jos muistissa on jo .DEF -tiedostossa määritellyn nimen mukainen ohjelma (esimerkissä NAME WHELLO), niin ajetaankin tätä ohjelmaa. Siis kaikki erilaisetkin ja saman nimiset ohjelmat joiden kääntämiseen on käytetty samaa .DEF -tiedostoa, tulkitaan saman ohjelman esiintymiksi.

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ä.

3.2.6 Kahvat (HANDLES)

Windowsissa voi olla yhtäaikaa käynnissä useita ohjelmia ja samakin ohjelma voi esiintyä useita kertoja. Samassa ohjelmassa voi olla useita ikkunoita ja kussakin ikkunassa voidaan piirtää usealla eri tyyppisellä kynällä (tai siveltimellä, "harjalla", brush).

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)!

3.2.7 Esiintymä (instance)

Kun ohjelma käynnistetään, tulee siitä tämän ohjelman uusi esiintymä (instance). Sama ohjelma voi olla käynnissä useitakin kertoja samanaikaisesti (kokeile). Mikäli ohjelmasta on jo ennestään käynnissä esiintymä tai useita, ei kaikkia toimenpiteitä kannata (muistin tuhlaus) tehdä uudelleen (eikä välttämättä saakaan).

	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.

3.2.8 Ikkunan luonti

Useimmiten ohjelman esiintymille luodaan kullekin oma pääikkuna. Ikkunan luonnissa saadaan kahva, jolla tähän ikkunaan voidaan myöhemmin viitata.

	    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.

3.2.9 Ikkunan näyttäminen

Kun ikkuna on luotu, pitää se pyytää myös näkyviin. Tämä tehdään yleensä ensimmäisen kerran nCmdShow -parametrin arvolla, jotta Windows voi pyytää ikkunan tulemaan haluamassaan muodossa (esim. ikonina):

	    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

3.2.10 Ikkunan päivittäminen

Ikkunan näyttäminen näyttää vain ikkunan raamit ja mahdollisen otsikon. Itse ikkunan sisukselle ei vielä tapahdu mitään. Yksinkertaisen esimerkkimme olemme tehneet siten, että ikkunan sisus piirretään kun saadaan viesti WM_PAINT.

Tämä viesti voidaan lähettää ikkunalle esimerkiksi UpdateWindow kutsulla.

3.2.11 Viestisilmukka

Viestisilmukan tärkein osa on GetMessage -kutsu. Windowsissa on kaksi viestijonoa: Hardware-jono, jossa on kaikki hiiren ja näppäimistön tapahtumat. Toinen on sovelluksen oma jono, jossa on sovelluksen yksityiset viestit. GetMessage lukee näitä kumpaakin. Mikäli mitään viestiä ei ole, Windows voi nyt vaihtaa toisen ohjelman suoritukseen kuten DOS-osassa katsottiin.

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.

3.2.12 Ikkunafunktio (Window Procedure)

Ohjelman varsinaisen viestien käsittelyn suorittaa ikkunaluokan funktioksi luokan luomisen yhteydessä ilmoitettu funktio. Funktiota ei koskaan kutsuta suoraan omasta ohjelmasta, vaan Windows kutsuu tätä funktiota (yleensä DispatchMessage kutsun jälkeen). Tällaisia funktiota kutsutaan CALLBACK-funktioiksi. Lisäksi funktio pitää julistaa "ulos" omasta modulistaan _export - määreellä.

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ä!              */

3.2.13 Laiteyhteys (Device Context)

Piirrettäessä täytyy tietää mihin piirretään, minkä värisellä kynällä piirretään jne. Jottei kullekin piirtofunktiolle tarvitsisi luetella valtavaa joukkoa parametreja, on Windowsissa laiteyhteys (device context) -niminen joukko piirtämisen attribuutteja. Kahva laiteyhteyteen muodostetaan normaalisti kutsulla:

	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.

3.2.14 Miksi piirtäminen WM_PAINT -viestissä?

Miksi jätimme piirtämisen WM_PAINT -viestin käsittelyyn. Olisimme voineet yhtähyvin piirtää tekstin pääohjelmassa heti ShowWindow -kutsun jälkeen. Tällöin kuitenkin jonkun toisen ikkunan peittäessä osittain tai jopa kokonaan meidän ikkunamme, olisi ikkunan uudelleen piirrossa osa tai koko tekstimme hävinnyt!

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.

3.3 Lisää ohjelman esiintymästä (WHELLO\WHELLO.C)

Esimerkkihakemistossa on perusteellisesti kommentoitu malliohjelma. Tämän malliohjelman listausta voidaan pitää aina mukana "Windows-ohjelmoinnin pikaoppaana". Malliohjelmassa tulostetaan ikkunaan käynnistetyn ohjelman esiintymä (instance) ja myös edellinen esiintymä (previous instance).

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ä!

3.4 Yksinkertainen pohja (WHELLO\SIMPLEW\SIMPLEW.C)

Koska yksinkertaiset Windows-ohjelmat noudattavat samaa kaavaa, voimme kirjoittaa valmiin pohjan, joka sisältää ikkunaluokan luomisen, ikkunan luomisen, viestisilmukan ja ikkunan funktion.

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.

3.4.1 Tekstiä näyttöön (WHELLO\SIMPLEW\SWHELLO.C)

"Hello World!" -ohjelman sovellutus olisi tällöin seuraava:

	#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.

3.5 Piirtäminen (WHELLO\SIMPLEW\DRAW_MAN.C)

Ikkunaan voidaan piirtää mm. Ellipse ja LineTo -funktioilla. Seuraava piirtää ikkunaan tikku-ukon:

	#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).

3.6 Hiiren viestit (WHELLO\SAMPLEW\SAMPLEW.C)

Kun hiirtä liikutetaan ikkunan sisällä, saadaan WM_MOUSEMOVE -viesti. Vastaavasti kun vasen nappi painetaan alas tai päästetään ylös, saadaan WM_LBUTTONDOWN tai WM_LBUTTONUP -viestit. Viestin lisäparametrissa lParam on hiiren paikan x ja y-koordinaatti. Windows 3.1:ssä tämä voidaan muuttaa pistetyyppiseksi (POINT) arvoksi valmiilla makrolla MAKEPOINT:

	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.

3.7 Yhteensopivuus 32-bittisen Windowsin kanssa (WIN32,WINDOWSX.H, ALI\PORTABLE.H)

Ennenkuin jatkamme eteenpäin, täytyy miettiä ohjelmien tulevaisuutta. 16-bittinen Windows 3.1 (tai sen työryhmäversio WFW 3.11) on polkunsa päässä. Suurin osa laitekannasta on jo 386- tai 486 pohjaisia ja täten kykeneviä ajamaan 32-bittisiä sovelluksia. Vaikka Windows 3.1 onkin 16-bittinen, voidaan jo sillä kehittää 32-bittisiä ohjelmia WIN32S -ajureiden avulla. Jotta ohjelmamme olisivat käännettävissä kummassakin ympäristössä, täytyy aina aluksi tarkistaa, että valittu funktio löytyy (tai voidaan makron tai aliohjelman avulla toteuttaa) kummastakin ympäristöstä.

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!

3.8 Usemman viestien käsittely

Kun rupeamme käsittelemään esimerkiksi hiireltä saatuja viestejä, on meillä useita mahdollisuuksia hoitaa lisääntynyt viestimäärä:

3.8.1 switch -lauseen laajentaminen

Voimme kirjoittaa lisää case -rivejä switch lauseeseen. Tämä on yleinen ja hyväksytty tapa, mutta ohjelmasta tulee helposti vain yksi suuri sekava switch -lause.

3.8.2 simplet -idean jatkaminen (whello\samplew\samplew.c)

Toinen mahdollisuus on laajentaa simplet.c:n switch -lausetta ja keksiä joukko uusia funktion nimiä. Tällainen toteutus on esitetty tiedostossa whello\samplew\samplew.c:

	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.

3.8.3 C++ ja ohjelmarungot (Application FrameWork)

Nykyaikainen tapa on ottaa käyttöön C++ ja jokin Windows luokkakirjasto, esim. Microsoftin MFC (Microsoft Foundation Class) tai Borlandin OWL 2.5 (Object Windows Library). Kumpikin on hyvä luokkakirjasto, mutta huono puoli on siinä, että kirjaston valinnan jälkeen sillä on tehtävä ohjelmia "loppuikänsä", koska luokat eivät ole yhteensopivia keskenään ja kirjaston opiskeleminen lukuisine luokkineen on varsin työlästä. Kuitenkin vaikuttaa, että suuret ohjelmistotalot käyttävät jompaa kumpaa (ehkä yleisemmin MFC:tä).

3.8.4 Taulukkopohjainen C-käsittely (WHELLO\SIMPLET\SIMPLET.C)

Eräs mahdollisuus on rakentaa itse alkeellinen vastine luokkakirjastojen viestinkäsittelyidean korvikkeeksi. Yksinkertainen toteutus löytyy tiedostosta WHELLO\FHELLO.C ja monipuolisempi tiedostosta WHELLO\SIMPLET\SIMPLET.C. Ideana on, että itse ikkunafunktiota ei tarvitsisi koskaan kirjoittaa, vaan riittäisi tehdä pieni taulukko, jossa sanotaan mikä viesti käsitellään milläkin funktiolla. Esimerkiksi SIMPLET.C:hen liittyvä "Hello world" -ohjelma voisi olla vaikkapa seuraavanlainen:

	/**************/
	/* 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 }
	};
	/****************************************************************************/

3.8.5 Yleiskäyttöinen taulukkokäsittelijä (ALI\TABHAND.H)

Kun tutkimme edellisen esimerkin pääohjelmaa SIMPLET.C, toteamme sen kelpaavan lähes sellaisenaan useammallekin eri sovellukselle. Kirjoitammekin joukon aliohjelmia ja makroja, jotka tekevät useimmin tarvittavat ohjelman osat. Jos tulee vastaan jokin erikoistapaus, voimme palata aina takaisin vanhaan ilman makroja toteutettuun perusratkaisuun. Kuitenkin kaikki tämän monisteen malliohjelmat on voitu toteuttaa tämän taulukkokäsittelijän avulla. Taulukkokäsittelijän eräs hyvä puoli on siinä, että voimme haudata sen syövereihin eri Windows-järjestelmien välisiä eroja.

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.

3.9 Animaatio, tyylit ja värit (WHELLO\SIMPLET\SHOW_MAN.C)

"Animaation" idea on sama kuin DOS-ohjelmassa näppäinten siirtelyssä näytöllä. Joko liikutettavan olion tausta on tallessa tai liikutettava olio piirretään ensin "peittävällä" kynällä pois ja sitten "piirtävällä" kynällä takaisin uudelle paikalleen. Viivakuvioiden siirtelyssä käytämme usein viimemainittua keinoa:

	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);
	...

3.10 Aikaan perustuvat toiminnot

Windowsissa voidaan asettaa ajastimia, joiden ajan täyttyessä saadaan joko WM_TIMER -viesti tai kutsutaan valittua CALLBACK -rutiinia.

3.10.1 Ajan pituuden säätö (WHELLO\SIMPLET\TIME_MAN.C)

Ajastimen tapahtumien (time tick) väli voidaan säätää SetTimer -funktiolla:

	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.*/ 
	}

3.10.2 Ajastimen viestiin vastaaminen

Ajastimen viestin saapuessa wParam -parametrissa on ajastimen numero.

	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*/
	...

3.11 Piirtämisen ongelmat

Yksinkertainen piirtäminen Windowsissa vaikutti siis varsin suoraviivaiselta. Kuitenkin piirtämiseen sisältyy kaksi ongelmaa: päivitys ja piirtämisen kesto (estää moniajon), joiden ratkaiseminen saattaa viedä ohjelmoijan yöunet.

3.11.1 Piirtämisen kesto (WHELLO\SIMPLET\KOLMIO.C)

Esimerkiksi seuraavan fraktaalikuvion piirtäminen onnistuu varsin yksinkertaisella rekursiolla, mutta kuvion syntyminen saattaa kestää kauan. Kuvan kolmio on piirtynyt nopeasti, koska pienimmän kolmion rajaksi on asetettu 12 pistettä. 0.5 pisteen kokoon menevä kuvio kestää yli 6 sekuntia 50 MHz 486-koneellakin (yli minuutin 16 MHz 386SX-koneella).

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.

3.11.2 Näytön päivitys (WHELLO\SIMPLET\KOLMIO.C)

Kolmio-ohjelmaan on lisätty alkeellinen hiirellä piirtäminen: kun hiiren nappi painetaan alas, piirretään jokaisen hiiren siirtymän jälkeen viiva painamispisteestä hiiren uuteen pisteeseen. Sinänsä tällä ei saada mitään hyödyllistä aikaiseksi, mutta havaitaan kuitenkin ongelmat näytön päivityksessä; kun piirretty kuva peitetään toisella ikkunalla ja palautetaan, ei kuva enää palaudukaan.

	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.

3.12 Moniajon mahdollistaminen (ALI\CHECKER.C)

Kolmio -esimerkin moniajo-ongelman korjaamiseksi ei Windowsissa ole tarjolla kovin valmista ratkaisua. PeekMessage -funktiolla voidaan kyllä tarkistaa, onko viestejä viestijonossa, mutta niiden käsittelemiseen tarvittaisiinkin sitten jo uusi ikkunafunktio.

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.

3.12.1 CheckMessage -kutsu (WHELLO\SIMPLET\KOLMIOM.C)

Kolmio-funktioon lisäys olisi seuraava:

	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!).

3.12.2 CheckMessage -funktio (ALI\CHECKER.C)

Itse CheckMessage -funktion tekeminen onkin sitten vaikeampaa. Viestit pitää osata käsitellä oikein. Esimerkiksi WM_PAINT -viesti lähtee jonosta vain BeginPaint, EndPaint -parilla. Kuitenkin viestin saapuessa siihen on syytä reagoida! Entinen piirtäminen voi olla syytä lopettaa, koska kuva on jo pilaantunut aivan uudesta kohtaa. Uuden WM_PAINT -viestin pilaantumisalue tuskin kuitenkin sisältää kuvan piirtämättä jäänyttä osaa. Siis parasta piirtää keskeytyksen jälkeinen WM_PAINT koko alueelle.

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.

3.12.3 Muutokset ikkunan funktioon

Aliohjelmakirjaston CHECKER.C kutsut liitetään ikkunan funktioon ja viestisilmukkaan. Ikkunafunktion aluksi tarkistetaan aina, onko kyseessä jokin viesti, joka tarvitsee tarkistaa moniajon takia, esimerkiksi:

	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ä???).

3.12.4 Auttaako Ossi, Unix, Windows 95 tai NT?

On varsin alkeellista, että käyttöliittymästä puuttuu tällainen mahdollisuus moniajoon. Vaikkei itse moniajoa välttämättä kaivattaisikaan, haluaa käyttäjä kuitenkin joskus peruuttaa käynnistämänsä operaation. Esimerkiksi tekstinkäsittelyohjelman esikatselussa voi sivun laskemiseen mennä varsin kauan ja mahdollisen väärän sivun tullessa näyttöön, haluaisi käyttäjä siirtyä jo uudelle, ennenkuin vanha on piirretty loppuun. Tämä vaatisi esitettyä checker.c:n kaltaista ratkaisua. Onneksi edes tulostukseen on toteutettu "valmiiksi" keskeytysmahdollisuus.

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


previous next Title Contents