previous next Title Contents

2. "Graafinen" MS-DOS -ohjelma


Luvun pääaiheet:

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

2.1 Yksinkertainen laskin

Valitsemme aluksi tehtävän, jonka myöhemmin tulemme toteuttamaan useilla eri työkaluilla. Olkoon tehtävänämme tehdä liikennelaskijalle laskin, jolla hän voi laskea ohi menevien henkilö- ja kuorma-autojen lukumäärän. Laskimessa tarvitaan siis näppäin, jota painetaan aina kun henkilöauto menee ohi ja tällöin henkilöautojen lukumäärä lisääntyy yhdellä. Vastaava näppäin tarvitaan myös kuorma-autoille.

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.

2.1.1 Yksinkertainen tekstiversio (DOSOHJ\LASKURI.C)

Jottei tehtävästä tulisi heti liian monimutkaista, teemme aluksi version, joka toimii seuraavasti:

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

2.1.2 Viestisilmukka (DOSOHJ\LASKURI2.C)

Kun teemme ohjelmaamme edellämainitut muutokset näyttöön tulostamiseen ja komennon tulkitsemiseen, voisi muutettu laske -aliohjelma olla seuraavan näköinen:

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

2.1.3 Yksinkertainen moniajo

Yksinkertaisessa monenkäyttäjän (tai moniajo) järjestelmässä tehtävän tai käyttäjän vaihto voitaisiin piilottaa funktioon lue_komento. Oletetaan, että meillä olisi yksi ainut prosessori ajamassa kolmen käyttäjän systeemiä, jossa kukin käyttäjä käyttäisi omalta päätteeltään autolaskuria vastaavaa ohjelmaa.


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.

2.2 Ohjelman parametrisoiminen

Malliohjelmamme ei ole kovin helppo ylläpitää. Mikäli ohjelmaan haluttaisiin lisätä vaikkapa polkupyörien laskeminen, pitäisi muutoksia tehdä varsin moneen paikkaan. Meidän malliohjelmassamme on kuitenkin pääasiassa vain kahdenlaisia "olioita": laskureita (ja niiden näytöt) sekä nappuloita (ja niitä vastaavat toiminnot).

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

2.2.1 "Graafinen versio" (DOSOHJ\LASKURI3.C)

Emme vielä aluksi yritäkään laittaa hiirtä mukaan toimintoihin, vaan tyydymme ensin valitun ulkonäön aikaansaamiseen. "Olioiden" ominaisuudet kasaamme tietueisiin:

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

2.3 Hiiren käyttö

Edellinen ohjelma on jo alkuperäisen suunnitelman mukainen, siitä puuttuu vain hiiriohjaus. Hiiren käyttämiseksi etsimme sopivan aliohjelmakirjaston. Tässä otamme käyttöön hiiru.c -nimisen kirjaston (Sirkku Greijula -91).

2.3.1 Malli hiiren käytöstä (DOSOHJ\HIIRIMAL.C)

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

2.3.2 Nappuloiden "kirjastoiminen" (DOSOHJ\NAPPULAT.H)

Laskimen "graafisessa" versiossa tuli määriteltyä tietueita ja funktiota, jotka voisivat olla yleiskäyttöisiä. Siispä siirrämme yleiskäyttöiset osat omaksi aliohjelmakseen tiedostoon DOSOHJ\NAPPULAT.C ja kirjoitamme näiden käyttämistä varten header-tiedoston nappulat.h.

2.3.3 Laskin (DOSOHJ\LASKURIH.C)

Varsinaiseen päätiedostoon jätämme vain globaalien taulukoiden

	nappaimet
	laskurit

ja ei-yleiskäyttöisten funktioiden

	laske
	piirra_naytto
	main

määritykset.

2.3.4 Hiiren käyttö laskimessa (DOSOHJ\NAPPULAT.C)

Miten hiiren käyttö lisätään laskimeen. Kuinka paljon itse päätiedostossa tarvitsee hiirestä välittää? Mitä hiirellä pitää osata tehdä? Hiirenkäytön malliohjelmasta huomasimme, että hiiren liikuttelusta ei tarvitse itse huolehtia.

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.

2.4 Nappuloiden ja laskureiden koon muuttaminen

Edellistä ajatusta voidaan vielä jatkaa. Oikeissa Windows-ohjelmissa ikkunoiden reunoihin voidaan tarttua ja niiden paikkaa ja kokoa muutella. Mitä laskuri-ohjelmassa tulisi tehdä, jotta nappuloiden siirtely ja koon muuttaminen olisi mahdollista?

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.

2.4.1 Muutokset päämoduliin (DOSOHJ\LASKURH2.C)

PIIRRA -viestin tulkitsemiseksi täytyy laske -aliohjelmaa hieman muuttaa:

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

2.4.2 Systeemiviestit (DOSOHJ\NAPPULA2.H)

PIIRRA -viestin tapaiset systeemiviestit kannattaa sijoittaa header-tiedostoon, jotta ne ovat kaikkien saatavilla. Windows-ohjelmoinnissa PIIRRA-viestiä vastaa WM_PAINT-viesti.

2.4.3 Muutokset tutki_hiiri -funktioon (DOSOHJ\NAPPULA2.C)

Funktio tutki_hiiri muutetaan siten, että pelkän nappulan päälläolemisen lisäksi tutkitaan onko hiiri mahdollisesti nappulan siirto- tai venytyskulmaksi valitun kohdan päällä. Mikäli on, tehdään tarvittava siirto tai venytys. Vastaavasti voidaan tutkia laskurin laatikosta, koska sekä nappula että laskuri olivat laatikon osalta samanlaisia.

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.

2.5 Taskulaskin (DOSOHJ\LASKUKON.C)

Kun olemme saaneet valmiiksi kirjaston nappuloiden ja laskureiden käsittelemistä varten, voimme nyt kirjaston avulla kirjoittaa helposti vaikkapa taskulaskinta matkivan ohjelman.

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.

2.6 Yhteensopivuus Windows'in kanssa.

Teemme myöhemmin sekä autolaskurista, että laskukoneesta vastaavat Windows-versiot. Pystymme tekemään myös version, jossa alkuperäiset pääohjelmamme kelpaavat lähes sellaisenaan, mutta koska Windows-ohjelmassa ei ole main -funktiota, joudumme muuttamaan pääohjelman nimen ja siirtämään varsinaisen pääohjelman nappulat-kirjaston puolelle (Windows-pääohjelmasta WinMain voitasiin kyllä kutsua aliohjelmaa main, mutta näin saamme varmemman tuloksen).

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.

2.6.1 Muutokset päämoduliin (DOSOHJ\LASKUKO3.C)

Päämodulissa vaihdamme main -funktion nimen vaikkapa nimeksi laskin_main:

	int laskin_main(void)
	{
	  alusta_nappulat(nappaimet);
	  piirra_naytto(nappaimet,laskurit);
	  laske(nappaimet,laskurit);
	  return 0;
	}

2.6.2 Muutokset näppain-kirjastoon (DOSOHJ\NAPPULA3.C)

Nappulakirjastoon lisäämme sitten main -funktion, joka pelkästään kutsuu laskin_main -funktiota.

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

2.7 Windows-versio

Kun olemme hetken opiskelleet Windows-ohjelmointia, kykenemme tekemään nappulat-kirjastosta Windows-version. Emme tässä kappaleessa puutu enää toteutuksen yksityiskohtiin, vaan luettelemme tiedostot, joista lukija voi katsoa toteutuksia.

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.

2.7.1 Yksinkertainen versio (WINLASKI\DOSVER\NAPPULA3.C)

Toteutus on melko suora muunnos vastaavasta DOS-ohjelmasta. Pääohjelmaksi käy DOS-versio, josta pääohjelma oli siirretty nappula-kirjaston puolelle. Lisänä täytyy systeemipoistumisen takia olla vielä tätä varten tarkoitettu viesti.

2.7.2 Siirto ja koon muuttaminen (WINLASKI\DOSVER\NAPPULA4)

Nappulan siirto ja koon muutaaminen voitaisiin jälleen tehdä vastaavasti kuin DOS-versiossa. Toteutamme siirron kuitenkin pelkkinä objektin ääriviivoina.

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.

2.7.3 Laskuriin Windows-nappulat (WINLASKI\DOSVER\NAPPULAW.C)

Windowsin valmiiden resurssien avulla voimme toteuttaa nappulakirjaston Windows-version siten, että nappulat luodaan "BUTTON" -luokan ja laskurit "STATIC" -luokan ikkunoina.

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.


previous next Title Contents