* tehtävän asetus ja yksinkertainen ratkaisu
* ratkaisun muuttaminen "graafisemmaksi"
* hiiren käyttö
* tapahtumaohjatun ohjelman idea
Varsinaisen Windows-ohjelman tekeminen vaatii aika paljon työtä C-kielellä. Valmiit esimerkit ovat täynnä "mystisen" näköisiä kutsuja ja tietueita. Lisäksi Windows-ohjelmoinnin tapahtumaohjaus saattaa tuntua aluksi vieraalta. Tämän vuoksi jotkin Windows- (ja olio-ohjelmointi) kirjat alkavat sanonnalla:
"Unohda kaikki mitä olet aikaisemmin oppinut ohjelmoinnista" (ÄLÄ USKO TÄTÄ!)
Tämä ei täysin pidä paikkaansa, jos aikaisempi ohjelmointityyli on ollut oliomaista. Onko tätä oliomaisuutta omassa ohjelmointityylissä vai ei? Tämän selvittämiseksi ja Windows-ohjelmoinnissa tarvittavien käsitteiden "keksimiseksi" aloitammekin "keksimällä" itse, minkälainen graafisella liittymällä varustettu ohjelma olisi ja mitä ominaisuuksia siihen lähes välttämättä tulisi.
Näin saamme hieman kerrattua C-kieltä ja samalla saamme tutuiksi Windowsissa vilisevät termit.
Samalla periaatteella voisimme tehdä ohjelman tivolin karusellin hoitajalle, jonka pitää painorajoitusten takia laskea montako aikuista ja montako lasta karuselliin on päästetty ja sulkea karusellin portti kun sallitut rajat tulevat täytteen.
Olkoon autolaskurimme ulkonäkö lopullisessa Windows-versiossa vaikkapa seuraava:
Nyt ohjelman toimintojen suunnittelu ei ole enää järin vaikeaa:
painettaessa Henkilöauto -nappia lisätään napin alla olevan näytön lukemaa
painettaessa Kuorma-auto -nappia lisätään napin alla olevan näytön lukemaa
painettaessa Nollaa -nappia nollataan molemmat näytöt
painettaessa eXit -nappia, lopetetaan koko ohjelma
Lisätään ohjelmaan vielä mahdollisuus pikavalintoihin, eli hiiren sijasta toiminto voidaan valita myös näppäimistöltä. Miten? Ehkäpä sujuvin tapa olisi painaa napin nimen isolla kirjoitettua kirjainta, eli esimerkissämme X,H,K tai N. Tällöin tapahtuisi vastaava toiminto kuin varsinaisen napinkin painamisella.
Nyt käyttäjäliittymän suunnittelu on valmis, joten voimme siirtyä itse ohjelmointiin.
Ohjelmalla lasketaan kuorma- ja henkilöautojen lukumääriä sitä mukaa kun ne ajavat ohi. Käyttöohje: K - lisää kuorma-autojen lukumäärää H - lisää henkilöautojen lukumäärää N - nollaa molemmat laskurit X - lopettaa ohjelman Henkilöautoja Kuorma-autoja 0 0 0 1 0 2 0 3 1 3 2 3 3 3
Siis näytölle tulee uusi rivi aina kun jotakin on tapahtunut. Toiminnoiltaanhan tämäkin ohjelma on hiirenkäyttöä lukuunottamatta samanlainen kuin suunnittelemamme versio.
Miten ohjelma kirjoitetaan. Pääasiassahan ohjelman runko on seuraava:
1. tulosta ohjeet ja otsikot
2. alusta laskurit
3. odota näppäintä
3.1. jos näppäin K, niin lisää kuorma-autoja
3.2. jos näppäin H, niin lisää henkilöautoja
3.3. jos näppäin N, niin nollaa laskurit
3.4. jos näppäin X, niin lopeta ohjelma
4. jatka kohdasta 3
C-kielellä voisimme kirjoittaa seuraavan pääohjelman:
int main(void) { piirra_naytto(); nollaa_laskurit(); laske(); return 0; }
Algoritmin kohtien 3 ja 4 silmukka on korvattu aliohjelmalla laske(), joka suorittaa vastaavaa silmukkaa:
int laske(void) { char c; while (1) { c = lue_merkki(); switch (c) { case 'h': case 'H': lisaa_ha(); break; case 'k': case 'K': lisaa_ka(); break; case 'n': case 'N': nollaa_laskurit(); break; case 'x': case 'X': return 0; } } }
Funktio lue_merkki voidaan tehdä esimerkiksi Turbo-C:ssä funktion getch avulla. Edellä ison ja pienen kirjaimen samaistaminen on tehty switch-lauseessa. Samaistaminen voitaisiin tehdä valmiiksi myös lue_merkki funktiossa, jolloin switch-lause jäisi yksinkertaisemmaksi.
Aliohjelmat lisaa_ha ja lisaa_ka huolehtivat laskureiden lisäämisestä ja näyttöön näyttämisestä:
void nayta_autot(void) { printf("%20d%20d\n",henkiloautoja,kuorma_autoja); } void nollaa_laskurit(void) { henkiloautoja = 0; kuorma_autoja = 0; nayta_autot(); } void lisaa_ha(void) { henkiloautoja++; nayta_autot(); } void lisaa_ka(void) { kuorma_autoja++; nayta_autot(); }
Laskurit on toistaiseksi ehkä helpointa toteuttaa globaalien muuttujien avulla.
Mikäli ohjelmasta haluttaisiin enemmän "graafinen", pitäisi näytön pysyä "vakaana" ja napin painasulla ei saisi tulostua uutta riviä, vaan vain laskurin arvon tulisi muuttua. Yksinkertaisimmillaan tässä ohjelmassa riittäisi korjaus (miksi?):
void nayta_autot(void) { printf("%20d%20d\r",henkiloautoja,kuorma_autoja); }
Usein kuitenkin tiedon muuttuva osa ei ole näytön viimeisellä rivillä, joten parempi ratkaisu olisi käyttää Turbo-C:n gotoxy(x,y) -funktiota siirtämään kursori haluttuun paikkaan ennen näytölle tulostamista. Funktion origo (1,1) on näytön vasemmassa yläkulmassa:
void nayta_autot(void) { gotoxy(1,laskuri_rivi); printf("%20d%20d",henkiloautoja,kuorma_autoja); }
void lisaa_laskuria(int *laskuri) { (*laskuri)++; nayta_autot(); } int laske(void) { char c; while (1) { c = lue_komento(); switch (c) { case 'H': lisaa_laskuria(&henkiloautoja); break; case 'K': lisaa_laskuria(&kuorma_autoja); break; case 'N': nollaa_laskurit(); break; case 'X': return 0; } } }
Esimerkin laske -aliohjelman silmukka on koko malliohjelman tärkein opetus!
Windows-ohjelmoinnissa vastaavaa silmukkaa nimitetään viestisilmukaksi (message loop). Tämä voidaan kuvitella siten, että lue_komento -funktio haluaa lähettää laske -aliohjelmalle viestin siitä, mitä pitää tehdä.
Tässä tapauksessa "viestit" ovat kirjaimia: 'H', 'K', 'N' ja 'X'. Oikeassa Windows-ohjelmassa viestejä on useita kymmeniä erilaisia. Kuitenkin kuten esimerkkiohjelmamme ei välitä muista kirjaimista, ei tavallisen Windows-ohjelman tarvitse välittää likikään kaikista viesteistä.
Käyttäjien kannaltahan tilanteen tulisi näyttää sellaiselta, että kullakin on prosessori tasan omassa käytössään, eli aina kun hän painaa näppäintä, hän haluaa vastaavan laskurin arvon muuttuvan heti omalla näytöllään.
Tällöin lue_komento -funktio olisi pääpiirteissään seuraava:
char lue_komento(void) { static int palveltava = 0; char c; talleta_palveltavan_ohjelma(palveltava); do { palveltava++; if ( palveltava >= KAYTTAJIA ) palveltava = 0; c = lue_kayttajan_nappaimisto(palveltava); } while ( !c ); ota_palveltavan_ohjelma(palveltava); return toupper(c); }
Siis kun tullaan funktioon lue_komento talletetaan aluksi kaikki mitä tiedetään nykyisin menossa olevasta ohjelmasta (pino, tietoalueet, jne) ja luetaan seuraavan käyttäjän näppäimistöä. Jos hän (esim. käyttäjä 1) on painanut jotakin, otetaan hänen ohjelmansa käyttöön ja palautetaan tälle ohjelmalle tiedoksi mikä näppäin oli painettu. Kun käyttäjän 1 ohjelma tulee siihen vaiheeseen, että ohjelmassa halutaan lukea uusi näppäin (ks. viestisilmukka), luetaankin tällä kertaa käyttäjän 2 näppäimistöä ja palvellaan häntä.
Mikäli käyttäjä ei ole painanut mitään kun hänen näppäimistöään luetaan, jätetään hänet väliin. Koska autolaskuri ei kuluta aikaansa paljon viestisilmukoiden ulkopuolella, jää kullekin käyttäjälle mielikuva siitä, että heti kun hän painaa näppäintä, häntä myös palvellaan. Mikäli kuitenkin viestisilmukan sisällä jokin toimenpide, esim. lisääminen kestäisi vaikkapa minuutin, joutuisivat KAIKKI käyttäjät odottamaan tämän saman minuutin ajan. Siis tällä tavoin toteutettu monenkäyttäjän järjestelmä ei ole yksinkertaisuudestaan huolimatta kovin hyvä kaikissa tapauksissa.
Samalla tavalla voidaan myös toteuttaa moniajo vaikkapa yhdelläkin käyttäjällä: lue_komento -aliohjelma osaa vaihtaa ohjelmasta toiseen. Esimerkiksi Windows 0.001:ssä lue_komento huolehtisi siitä, minkä ikkunan päällä hiiri on. Jos hiiren nappia painettaisiin, vaihtaisi lue_komento tämän ikkunan ohjelman tiedot keskusmuistiin ja mikäli jotakin näppäimistön näppäintä painetaan, palauttaisi tiedon näppäimen painamisesta tälle ohjelmalle. Siis kaikki käynnissä olevat ohjelmat odottavat lue_komento -funktiolla tietoa itselleen. Arvatenkin mikäli joku ohjelmista viipyisi kauan lue_komento -funktion ulkopuolella, ei hiiren liikkeitä noteerattaisi.
Microsoftin Windows 3.1:ssä moniajo on toteutettu pääasiassa tähän tyyliin (cooperative multitasking, nonpreemptive multitasking). Tästä seuraa ohjelmoijalle suuri vastuu: ohjelmat eivät saa kuluttaa liikaa aikaa viestisilmukoiden ulkopuolella!
Paremmissa järjestelmissä (OS/2, UNIX) moniajo toteutetaan aikaan perustuvan (time slicing, preemptive multitasking) tehtävänvaihdon avulla. Näin yksittäinen ohjelma ei pääse omimaan kaikkea prosessoriaikaa itselleen.
Ennenkuin kirjoitamme ohjelmasta alkuperäisen suunnitelman näköistä, kannattaa miettiä näiden "olioiden" ominaisuuksia. Voidaanko kaikki olioiden ominaisuudet kerätä tietueisiin? Vastaus on kyllä.
/****************************************************************************/ /* Yleiset tyypit: */ typedef struct { int x; int y; } koord_tyyppi; typedef struct { koord_tyyppi vasen_yla; /* Laatikon vasemman yläkulman koordinaatit */ koord_tyyppi koko; /* Laatikon sisäkoko. Jos (0,0) niin pienin mah.*/ } laatikko_tyyppi; /* Jos <0, niin ei raameja */ typedef struct { /* Tyyppi näppäimelle: */ char valinta_kirjain; /* Kirjain, jota painamalla komento tot. */ char *teksti; /* Näppäimeen tulostettava teksti. */ laatikko_tyyppi nappain; /* Näppäimen sijainti ja koko */ int komento; /* Mikä komento näppäimellä tehdään. */ int lisa_viesti; /* Jos halutaan välittää jokin lisätieto */ } nappain_tyyppi; /* esim. numeronäppäimen arvo tms. */ typedef struct { /* Tyypit laskurinäytöille: */ int tunnus; /* Laskurin tunniste */ int arvo; /* Laskurin nykyinen arvo */ char *format; /* Tulostuksen formaatti C-muodossa */ laatikko_tyyppi tila; /* Laskurin tulostusalueen paikka ja koko*/ } laskuri_tyyppi; /****************************************************************************/ /* Globaalit näppäimet: */ typedef enum { /* Tunnetut komennot: */ TYHJA, /* Lopun tunniste */ POISTU, NOLLAA, LISAA } komento_tyyppi; typedef enum { /* Käytetyt laskurityypit ja niiden tunnisteet */ TYHJA_LASKURI, /* Lopun tunniste */ HENKILOAUTOJA, KUORMA_AUTOJA } laskuri_tunniste_tyyppi; nappain_tyyppi nappaimet[] = { /* x y lev kork */ { 'X',"eXit" ,{{ 2, 2},{ 0, 0}}, POISTU , 0 }, { 'N',"Nollaa" ,{{30,13},{ 0, 0}}, NOLLAA , 0 }, { 'H',"Henkilöautoja" ,{{13, 5},{ 0, 0}}, LISAA , HENKILOAUTOJA }, { 'K',"Kuorma-autoja" ,{{38, 5},{ 0, 0}}, LISAA , KUORMA_AUTOJA }, { 0 ,NULL ,{{ 0, 0},{ 0, 0}}, TYHJA , 0 } }; /****************************************************************************/ /* Globaalit laskurit: */ laskuri_tyyppi laskurit[] = { /* x y lev kork */ { HENKILOAUTOJA, 0 , "%19d ",{{10, 8},{20, 1}} }, { KUORMA_AUTOJA, 0 , "%19d ",{{35, 8},{20, 1}} }, { TYHJA_LASKURI, 0 , NULL ,{{ 0, 0},{ 0, 0}} } };
Kaikki tietous näppäimien ja laskureiden paikoista on voitu laittaa globaaleihin tietueisiin. Tietueet muodostavat taulukon, jonka loppu tunnistetaan samoin kuin C:n merkkijononkin loppu: tunnistemerkki (merkkijonossa NUL). Laskureiden ja komentojen numerointi on tehty enum:in avulla, tällöin ei itse tarvitse huolehtia numeroista. Muuten POISTU, NOLLAA ja LISAA on tulkittava vain tavallisiksi kokonaisluvuiksi, joille on annettu kuvaavampi nimi (tämä menettely tulee toistumaan Windows-ohjelmissa useita kertoja).
Nyt lue_komento -funktion tulisi palauttaa komennon numero, eli komennolle valittu tunnus. Miten funktio nyt tehdään. Jos painetaan näppäintä H, pitäisi etsiä missä nappulassa ko. kirjain on valintakirjaimena. Jos valittu nappula löytyy, palautetaan sen tunnus ja mahdollinen lisäviesti. Missä lisäviesti palautetaan? Tarvitsemme tätä varten uuden parametrin. Mahdollisten myöhempien laajennusten varalta tuomme lue_komento -funktiolle myös koko nappulataulukon parametrina:
/****************************************************************************/ komento_tyyppi lue_komento( /* */ nappain_tyyppi *nappaimet, int *lisa ) /* ** Funktio palauttaa valitun komennon tunnuksen ja mahdollisen lisäviestin. ** Funktiosta palataan vasta kun oikeata näppäintä on painettu. */ { /* Seuraava toimii mm. Turbo C:ssä: */ char c; int i; *lisa = 0; while (1) { c = toupper(getch()); for (i=0; nappaimet[i].valinta_kirjain; i++) if ( nappaimet[i].valinta_kirjain == c ) { *lisa = nappaimet[i].lisa_viesti; return nappaimet[i].komento; } } }
Viestisilmukka muuttuisi vastaavasti:
/****************************************************************************/ int laske(nappain_tyyppi *nappaimet,laskuri_tyyppi *laskurit) { komento_tyyppi komento; int lisa_viesti; while (1) { komento = lue_komento(nappaimet,&lisa_viesti); switch (komento) { case LISAA : lisaa_laskuria(laskurit,lisa_viesti); break; case NOLLAA : nollaa_laskurit(laskurit); break; case POISTU : return 0; } } }
Laskurin lisäämiseksi täytyy ensin etsiä millä laskurilla on lisäviestissä tullut tunnus ja lisätä tätä laskuria.
/****************************************************************************/ void lisaa_laskuria(laskuri_tyyppi *laskurit,int tunnus) { int i; for (i=0; laskurit[i].tunnus != TYHJA_LASKURI; i++) { /* Ets. laskuri */ if ( laskurit[i].tunnus == tunnus ) { laskurit[i].arvo++; nayta_laskuri(&laskurit[i]); return; } } }
Kun nappuloista ja laskureista on tietueessa tiedossa niiden koordinaatit, voidaan ne piirtää näyttöön laatikoina. Tätä varten teemme oman aliohjelman piirra_laatikko, jolle tuodaan parametrina laatikon koordinaatit ja laatikon oletusleveys, mikäli laatikolle ei ole annettu leveyttä (näin saamme tasan laatikon sisälle tulevan tekstin levyisiä laatikoita).
/****************************************************************************/ void nayta_laskuri(laskuri_tyyppi *laskuri) { textcolor(BLACK); textbackground(CYAN); gotoxy(laskuri->tila.vasen_yla.x,laskuri->tila.vasen_yla.y); cprintf(laskuri->format,laskuri->arvo); }
/****************************************************************************/ void piirra_naytto(nappain_tyyppi *nappaimet,laskuri_tyyppi *laskurit) { int i; textcolor(WHITE); textbackground(WHITE); clrscr(); textcolor(BLACK); textbackground(YELLOW); for (i=0; nappaimet[i].valinta_kirjain; i++) { piirra_laatikko(&nappaimet[i].nappain,strlen(nappaimet[i].teksti)); gotoxy(nappaimet[i].nappain.vasen_yla.x,nappaimet[i].nappain.vasen_yla.y); cprintf(nappaimet[i].teksti); } textcolor(BLUE); textbackground(CYAN); for (i=0; laskurit[i].tunnus != TYHJA_LASKURI; i++) { piirra_laatikko(&laskurit[i].tila,1); nayta_laskuri(&laskurit[i]); } }
Muuten ohjelman pääasiallinen idea ei muutukaan, olemme vain saaneet ohjelmasta näyttävämmän näköisen. Nyt ohjelma on lisäksi helposti muutettavissa. Esimerkiksi uuden laskentakohteen lisäämiseksi pitää vain lisätä yksi rivi taulukkoon laskurit ja yksi rivi taulukkoon nappaimet.
/***************************************************************************** ** Malliohjelma, jolla kokeillaan hiiren toimintaa. ** Aluksi näyttöön tulostetaan hiiren koordinaatti aina oikean näppäimen ** painamisen jälkeen. Kun vasen nappi käytetään alhaalla, ** ruvetaan tulostamaan hiiren paikkaa jatkuvasti kunnes painetaan ** näppäintä 'x'. ** ** VGA-näytöllä (80x25) koordinaatti muuttuu (0,0)-(632,192). ** ** Malliohjelman kääntämiseksi pitää tehdä projekti, jossa on ** hiirimal.c ** hiiru.c ** ** Kääntäminen onnistuu vain TURBO-C:n uudemmilla versioilla. ** ** Vesa Lappalainen 3.9.1992 *****************************************************************************/ #include <stdio.h> #include <conio.h> #include "hiiru.h" int main(void) { int c; int nappi1,nappi2,x,y,montako; alustahiiri(); clrscr(); naytahiirikursori(); gotoxy(1,1); cprintf("%3s %3s (%3s,%3s)","vas","oik","x","y"); do { hiirenpainallukset(0,&nappi1,&nappi2,&montako,&x,&y); gotoxy(1,2); cprintf("%3d %3d (%03d,%03d)",nappi1,nappi2,x,y); } while ( !nappi2 ); do { hiirentila(&x,&y,&nappi1,&nappi2); gotoxy(1,2); cprintf("%3d %3d (%03d,%03d)",nappi1,nappi2,x,y); if (kbhit()) c = getch(); } while ( c != 'x' ); katkehiirikursori(); return 0; }
Hiirtä käytettäessä on oikeasti oltava tarkka siitä, ettei näyttöön kirjoiteta mitään hiirikursorin ollessa näytössä, muuten näyttöön saattaa jäädä ylimääräisiä "kursoreita".
nappaimet laskurit
ja ei-yleiskäyttöisten funktioiden
laske piirra_naytto main
määritykset.
Autolaskuri-ohjelmassa hiiri saa liikkua vapaasti koko ruudun alueella. Ainoastaan mikäli hiiren vasenta nappia painetaan nappulan kuvan päällä, pitäisi tästä saada tieto. Miten asia voidaan hoitaa?
Nappuloiden koordinaatit ovat tiedossa. Siispä tutkitaan hiiren tilaa ja mikäli vasen näppäin on painettu alas, tutkitaan sattuuko hiiri olemaan jonkin näppäimen päällä. Jos on, palautetaan tieto siitä, minkä näppäimen päällä. Mihin kohtaan tämä sitten sijoitetaan varsinaisessa ohjelmassa?
Funktio lue_komento odottaa näppäimen painallusta. Miksei se voisi vuorotellen katsoa onko hiiri kriittisellä kohdalla tai näppäimistön (keyboard) näppäin painettu:
/****************************************************************************/ int lue_komento( /* */ nappain_tyyppi *nappaimet, laskuri_tyyppi *laskurit, int *lisa ) /* ** Funktio palauttaa valitun komennon tunnuksen ja mahdollisen lisäviestin. ** Funktiosta palataan vasta kun oikeata näppäintä on painettu. ** Mikäli näppäintä ei ole painettu, tutkitaan onko hiirikursori jonkin ** näppäimen kuvan päällä. Jos on, palautetaan tämä tieto. ** Tämä aliohjelma sytyttää ja sammuttaa hiirikursorin, joten muualla ** ohjelmassa hiirestä ei tarvitse tietää muuta kuin alustaa se kerran. */ { char c; int i; *lisa = 0; if ( HIIRI ) naytahiirikursori(); while (1) { if ( kbhit() ) { c = toupper(getch()); for (i=0; nappaimet[i].valinta_kirjain; i++) if ( nappaimet[i].valinta_kirjain == c ) goto palauta; } if ( ( i = tutki_hiiri(nappaimet,laskurit) ) >= 0 ) goto palauta; } palauta: if ( HIIRI ) katkehiirikursori(); *lisa = nappaimet[i].lisa_viesti; return nappaimet[i].komento; }
Huomattakoon, että aina kun funktiosta lue_komento lähdetään pois, sammutetaan hiiren kursori näytöltä, jotta näyttöön saa vapaasti kirjoittaa.
Funktio tutki_hiiri palauttaa sen nappulan indeksin, jonka kohdalla hiiren vasen näppäin on painettu alas. Mikäli näin ei ole tapahtunut palautetaan negatiivinen luku. Näin hiiren käyttö ja tavallinen näppäimistö voidaan samaistaa.
/****************************************************************************/ int /* -1 jollei hiiri minkään napin koh */ tutki_hiiri( /* napin numero, jonka kohdalla hiiri*/ nappain_tyyppi *nappaimet, laskuri_tyyppi *laskurit ) { int i,nappi1,nappi2,montako,x,y; if ( laskurit ); /* Hämäystä, jottei kääntäjä valita */ if ( !HIIRI ) return -1; hiirenpainallukset(0,&nappi1,&nappi2,&montako,&x,&y); SKAALAA_XY; if ( !montako ) return -1; for (i=0; nappaimet[i].valinta_kirjain; i++) { if ( laatikossa(&nappaimet[i].nappain,x,y) ) return i; } return -1; }
Funktio laatikossa tutkii onko annettu koordinaatti laatikon sisällä vai ei.
Tämä lähestymistapa oli mielenkiintoinen. Lähes sama pääohjelma (nappuloiden alustus hiiren alustamiseksi on lisätty) toimii sekä hiirellisessä että hiirettömässä versiossa.
Tarvitseeko jälleen pääohjelman olla tietoinen siitä, että nappulan paikkaa on muutettu. Jos ei, paikan ja koon muuttaminen voitaneen jättää täysin lue_komento -funktion huoleksi. Itse asiassa edes lue_komento -funktion ei tarvitse tietää nappulan paikan ja koon muuttamisesta. Riittää kun funktio tutki_hiiri osaa tämän tehdä.
Periaatteessa asia on näin yksinkertainen. Käytännössä nappulan siirtäminen tai koon muuttaminen saattaa johtaa siihen, että näyttö on mennyt sekaisin! Tästä selvittäisiin piirtämällä näyttö uudelleen. Mutta koska tutki_hiiri ei voi millään tietää sitä, mitä näytössä pitäisi olla (mahdollisia ohjetekstejä yms.), on päämoduli ainoa joka voi suorittaa näytön uudelleen piirtämisen. Tätä varten pitäisi saada tieto viestisilmukkaa pyörittävälle laske -aliohjelmalle näytön uudelleen piirtämiseksi.
Tieto voidaan välittää helpoimmin keksimällä uusi viesti PIIRRA, joka kertoo että nyt pitäisi näyttö piirtää uudelleen.
/****************************************************************************/ int laske(nappain_tyyppi *nappaimet,laskuri_tyyppi *laskurit) { komento_tyyppi komento; int lisa_viesti; while (1) { komento = lue_komento(nappaimet,laskurit,&lisa_viesti); switch (komento) { case LISAA : lisaa_laskuria(laskurit,lisa_viesti); break; case NOLLAA : nollaa_laskurit(laskurit); break; case PIIRRA : piirra_naytto(nappaimet,laskurit); break; case POISTU : return 0; } } }
Muuten päämoduli voidaan säilyttää ennallaan.
Mikäli muutoksia on tapahtunut, palautetaan viestinä PIIRRA -viesti. Järjestämällä systeemiviestit esimerkiksi negatiivisiksi, saadaan lue_komento -aliohjelma pysymään lähes ennallaan.
Miten laatikon kuvaa siirretään näytöllä? Tähän on kaksi päätapaa.
Toisessa tavassa näytöstä pyyhitään vanha kuva ensin pois esim. piirtämällä kuva negatiivisella (XOR) kynällä. Tämän jälkeen piirretään kuva uuteen paikkaan negatiivisella kynällä. Tätä tapaa käytetään erityisesti graafisessa näytössä silloin, kun siirrettävä objekti on hyvin pitkä ja ohut (esim. viivat tai hiusristikkokursori).
Toinen tapa on lukea uuden paikan alta kaikki tieto ja sitten piirtää objekti uuteen paikkaan. Kun objekti seuraavan kerran täytyy piirtää uuteen paikkaan, talletetaan edellisen kerran tausta, luetaan uuden paikan tausta ja piirretään. Tämä tapa sopii "möhkälemäisten"-objektien liikutteluun.
Tekstinäytössä taustan luku jää lähes ainoaksi vaihtoehdoksi, joten toteutamme laatikoiden siirtelyn ja venyttelyn sillä tavalla. Ensimmäisellä kerralla tausta jää kuitenkin lukematta, joten alkuperäinen nappula jää paikoilleen. Tämän takia tarvitsemme uudelleenpiirtoviestin; saamme vanhan laatikon pois näytöstä.
Emme enää enempää perehdy itse siirtelyn tekniikkaan, vaan jätämme tekniikan opiskelun lukijan harjoitustehtäväksi.
Windows-ohjelmoinnissa ikkunoiden siirrot, koon muutokset, ikoniksi muuttaminen jne. tapahtuvat systeemin puolesta. Samoin tapahtuu nyt meidänkin ohjelmassamme laske -aliohjelman kannalta.
Määrittelemme nappaimet -taulukkoon kaikki numeronäppäimet ja peruslaskutoimitukset. Lisäksi tarvitsemme peruutusnäppäimen (BS) ja tulostusnäppäimen (=). Kun keksimme toiminnoille nimet, onkin laskukone melkein valmis.
Mitä pitää tehdä kun painetaan numeronäppäintä? Tämä riippuu aikaisemmasta toiminnasta. Mikäli edellinen lasku on lopettettu, pitää numeron aloittaa uusi luku näytössä, muuten sen pitää mennä vanhan luvun oikeaan reunaan. Tämä saadaan helposti aikaan kertomalla vanha luku 10:llä ja lisäämällä uusin numero tähän.
Valmiin nappula-aliohjelmakirjaston ansiosta laskukoneen kaikkien näppäinten koko ja paikka on muutettavissa. Mikäli joku suurella vaivalla nappulat siirtelee parempiin paikkoihin, olisi tietenkin toivottavaa voida tallettaa tämä muutettu tilanne. Tätä ei voida suoraan lisätä nappula-kirjaston huoleksi, vaan tarvittaisiin jälleen pieni yhteistyö päämodulin kanssa, lähinnä alustusvaiheeseen. Emme kuitenkaan enää jatka tämän ohjelman kehittämistä, vaan siirrymme seuraavaksi oikeaan Windows-ohjelmointiin.
Tämän muutoksen jälkeen pystymmekin tekemään vastaavia sovelluksia, jotka toimivat sekä MS-DOSissa että Windowsissa vain nappulat-kirjastoa vaihtamalla. Samalla tavalla nappulat-kirjasto voitaisiin tehdä myös UNIXin curses-kirjaston avulla tekstipohjaiseksi ja vaikkapa X11-versio graafiseksi.
Ei siis ole lainkaan mahdotonta tehdä ohjelmia, jotka saadaan pienellä työllä siirretyksi muihin laiteympäristöihin. Esimerkiksi Wingz-niminen taulukkolaskentaohjelma on saatavissa ainakin Windows, OS/2, X11 että Mac -versioina.
int laskin_main(void) { alusta_nappulat(nappaimet); piirra_naytto(nappaimet,laskurit); laske(nappaimet,laskurit); return 0; }
extern int laskin_main(void); int main(void) { return laskin_main(); }
Jokaisessa C-ohjelmassa pitää olla tasan yksi main -funktio, joten linkittämällä esimerkiksi käännetyt laskuko3.obj ja nappula3.obj saamme valmiin ohjelman, jossa on vaaditusti tasan yksi pääohjelma.
Sama käytäntö tulee esille Windows-ohjelmissa: main -funktion ei tarvitse olla omassa ohjelmassa (eikä olekaan)!
Autolaskurin Windows-versio voidaan toteuttaa monella tavalla (ja monella eri työkalulla). Eräs tapa olisi käyttää valmiita Windowsin resursseja. Palaamme tähän Windowsmaisempaan tapaan myöhemmin. Aluksi rakennamme kuitenkin DOS-version kanssa yhteensopivan version piirtämällä itse näppäimet suorakaiteina ja tarkistamalla itse hiiren sijainti suhteessa suorakaiteesiin.
Ohjelmaan on lisätty myös kokonaisen joukon valinta kerralla. Tämä on malliksi, koska useissa Windows-ohjelmissa voidaan käsitellä objekti-joukkoja: valitut objektit liitetään johonkin tietorakenteeseen, johon muutokset sitten kohdistetaan. Joukko valitaan joko suorakaiteella kehystämällä tai pitämällä CTRL-näppäin pohjassa ja lisäämällä hiiren napautuksella objekteja valintaan. Valitut objektit merkitään mallissa harmaalla yläreunalla.
Edelliseen versioon nähden viaksi jää se, ettei nappuloiden väriä voida muuttaa(?). Esimerkin versiossa myöskään nappuloiden kokoa ei voida muuttaa dynaamisesti, mutta muutos olisi toteutettavissa samoin kuin NAPPULAT4.C -versiossa. Kun haluttu koko tai paikka on venytetty, käytettäisiin MoveWindow -kutsua.
Esimerkiksi taskulaskin -ohjelman ulkoasu ei ole kovin kaksinen, koska näppäinten paikat oli suunniteltu DOS-version merkkipohjaisten koordinaattien mukaan:
Ulkoasuongelmasta pääsemme eroon vaihtamalla koko lähestymistapaa Windowsmaisemmaksi. Samalla menetämme kuitenkin yhteensopivuuden DOS-ohjelmaan. Tosin yhteensopivuus voitaisiin palauttaa täältäkin päin tekemällä DOS-kirjasto, jossa on vastaavat kutsut kuin Windowsissa. Osittain tällainen on esimerkiksi Borlandin Application Frameworkin mukana tuleva Turbo Vision (C++ versio). Osittain vain siksi, että kirjaston kutsut eivät ole täsmälleen samoja kuin Windows-version.
On olemassa myös parempia (ja kalliimpia) kirjastoja, joissa yhdistetään useiden käyttöjärjestelmien ja käyttöliittymien ominaisuuksia ja näin ohjelmankehitys voidaan tehdä "missä tahansa" käyttöliittymässä tai käyttöjärjestelmässä ja uudelleen kääntämällä siirtää sovellus toiseen ympäristöön.
Jatkossa luovumme kuitenkin DOSin painolastista ja keskitymme tekemään ohjelmaa Windowsin ehdoilla.