previous next Title Contents

5. Windowsin monipuolisempi ohjelmointi


Pääaiheet:

* useita dialogeja samassa ohjelmassa

* eri dialogi-tyyppien erot

* globaalien muuttujien hallinta

* tulostus

* valmiit koordinaatistomuunnokset ja yksiköt

* dynaaminen linkitys (DLL)

Edellä autolaskurissa liikkui itsenäisesti eläviä autoja. Kukin auto oli oma ikkuna varsinaisen ikkunan sisällä. Ohjelmassa on myös esiintynyt joitakin valmiita dialogeja esimerkiksi tiedoston avaamiseen, virheviestien ilmoittamiseen jne. Aivan vastaavasti tehdään ohjelmat, joissa on enemmän toiminnallisuutta muissa ikkunoissa.

Tämän otsikon alle kuuluisi vielä MDI (Multiple Document Interface), mutta tässä luvussa käsitellään aluksi moni-ikkunointia vain normaalein menetelmin. MDI on helpointa jättää C++ -kirjastojen kautta käytettäväksi, jolloin sen monimutkaisuus saadaan haudattua luokkien alle (ks. esim. [DiLascia]. Kuitenkin käytännössä MDI on tärkeä Windows-ominaisuus jos useamman dokumentin käsittelemiseksi täytyy saada useita saman ohjelman sisäisiä ikkunoita käyntiin.

Tämän monisteen esimerkkiohjelmat ovat niin pieniä, että ne voidaan kaikki kääntää Small-muistimallia käyttäen, eikä niissä ole tilanteita, joissa tarvitsisi hirveästi kommunikoida toisen samanlaisen ohjelman kanssa Tällaisia ohjelmia mahtuu olemaan muistissa useita ja näin ollen useita "dokumentteja" voidaan käsitellä aukaisemalla uusia ohjelmia. Muistihan tästä ei kärsi, koska toinen esiintymä käyttää edellisen esiintymän koodia.

Luvun lopuksi tutustumme tulostukseen ja dynaamiseen linkitykseen (DLL).

5.1 Ikkunafunktioiden erot

Kukin erilainen ikkuna tarvitsee tietysti oman ikkunafunktionsa. Valitettavasti ikkunafunktion sisältö riippuu siitä, minkälaisen ikkunan funktiosta on kyse.

Jos ikkuna on dialogi, pitää ikkunafunktion oletuskäsittely suorittaa palauttamalla FALSE ja mikäli ikkuna on normaali CreateWindow -kutsulla luotu ikkuna, suoritetaan oletuskäsittely kutsumalla funktiota DefWindowProc, kuten MHELLO.C ohjelmassa tehtiin. Mikäli ikkuna on dialogi, jolla on luokka, pitää oletuskäsittely tehdä DefDlgProc -kutsulla. Lisäksi ikkunafunktioiden paluutyypit ovat erilaisia. Dialogifunktioille BOOL ja tavallisille ikkunafunktioille LONG. Dialogifunktio ei saa WM_CREATE -viestiä ja toisaalta tavallinen ikkunafunktio ei saa WM_INITDIALOG -viestiä.

Kaiken lisäksi Dialogi-ikkunoita on kahdenlaisia: Modal ja Modeless.

5.2 Modal-dialogit

5.2.1 Modal-dialogin vaatimat kutsut

Modal-dialogi on dialogi, jonka aukiolon aikana isäikkuna ei ole aktiivinen. MessageBox oli esimerkki valmiista modaalisesta dialogista. Voidaan tehdä myös SystemModal -dialogi, jonka aikana mikään muu systeemin ikkuna ei toimi. Näiden käyttöä tulee kuitenkin välttää tavallisissa ohjelmissa.

Modal-dialogi luodaan kutsulla

	DialogBox(hInst,nimi,hWnd,lpProcFunc)

Tätä ennen on tietenkin tarvinnut luoda dialogin ikkunafunktion osoitteelle "pysyvä" osoitin. Tämä tehdään koska Windows haluaa välittää kullekin ohjelman eri esiintymälle oman DS (DataSegment) arvon. Windows muodostaa pienen apualiohjelman, joka sitten kutsuu varsinaista ohjelmaa oikealla DS-arvolla. Apualiohjelman osoite saadaan kutsulla:

	lpProcFunc = MakeProcInstance((FARPROC) func,hInst);

Tämän jälkeen Windows huolehtii dialogin viestisilmukasta. Siis Modal-dialogeille ei tarvitse kirjoittaa viestisilmukkaa. Ikkunafunktio tarvitaan tietenkin.

Kun dialogissa valitaan sen lopetus, pitää dialogin ikkunafunktion kutsua funktiota

	  EndDialog(hDlg,TRUE);

ja lopuksi vapauttaa dialogin funktion osoite kutsulla

	  FreeProcInstance(lpProcFunc);

WIN32:ssa sekä MakeProcInstance että FreeProcInstance ovat tarpeettomia, mutta niille on makrovastike, joka ei tee mitään, joten 16-bittisen yhteensopivuuden vuoksi näitä voidaan edelleen käyttää. Tosin FreeProcInstance:en eteen voi laittaa (void) jottei kääntäjä valita tarpeettomasta koodista.

5.2.2 Funktio Modal-dialogin tekemiseksi

Koska vaiheet modal-dialogin tekemiseksi ovat aina samat, kannattaa ehkä kirjoittaa aliohjelma hoitamaan hommat:

	int DoModalDialog(HWND hWnd,char *name,PDIALOGPROC func)
	{
	  int ret;
	  HINSTANCE hInstance = GetWindowInstance(hWnd);
	  FARPROC lpProcFunc;
	  lpProcFunc = MakeProcInstance((FARPROC)func, hInstance);
	  ret = DialogBox(hInstance, name, hWnd, (DLGPROC)lpProcFunc);
	  (void)FreeProcInstance(lpProcFunc);
	  return ret;
	}
	...
	        case IDM_HELP_ABOUT:           
	          return DoModalDialog(hWnd,"About",About);

5.2.3 Ohjelman esiintymän selvittäminen

Edellä ohjelman esiintymä selvitetään makrolla

	HINSTNACE hInstance = GetWindowInstance(hWnd );

On olemassa muitakin tapoja, mutta tätä on syytä käyttää yhteensopivuuden säilyttämiseksi.

5.2.4 Modal-dialogin funktio

Tyypillinen Modal-dialogin funktio olisi vaikkapa Tietoja -laatikkoa ylläpitävä funktio:

	BOOL CALLBACK_E ModalAbout(HWND hDlg,UINT message, WPARAM wParam,LPARAM lParam)
	{
	  switch (message) {
	    case WM_INITDIALOG:  return TRUE;
	
	    case WM_COMMAND: {
	      int id = GET_WM_COMMAND_ID(wParam,lParam);
	      if ( id == IDOK || id == IDCANCEL ) {
	        EndDialog(hDlg,TRUE);
	        return TRUE;
	      }
	      break;
	    }
	  }
	  return FALSE;
	}

Makro CALLBACK_E on itse määritelty makro

	#define CALLBACK_E CALLBACK _export /* Nyt .DEF tiedosto ei tarvitse EXPORT*/

Tällöin funktiota ModalAbout ei tarvitse esitellä .DEF -tiedoston EXPORTS -osassa (eikä Borland C++:n C++ käännöksessä voinutkaan esitellä!).

5.3 Modeless-dialogi

Modeless -dialogi eroaa Modal -dialogista siinä, että sen aikana voidaan käyttää edelleen myös dialogin isä-ikkunaa. Esimerkiksi edellä autolaskurin resursseja hyväksikäyttävä versio oli toteutettu Modeless-dialogina. Siis Modeless-dialogi on oikeastaan vain tavallinen ikkuna, jonka sisällä on valmiiksi resursseja.

Jos Modeless-dialogin aikana jokin sen sisartason ikkuna tai jopa isäikkuna halutaan tehdä epäaktiiviseksi, pitää tämä tehdä EnableWindow -kutsulla.

Kun Modeless-dialogi suunnitellaan resurssieditorilla, pitää se muistaa laittaa oletuksena näkyväksi. Tämä ei haittaa vaikka dialogia myöhemmin käytettäisiinkin Modal-dialogina.

5.3.1 Otettava huomioon viestisilmukassa

Koska Modeless -dialogi toimii isä-ikkunan rinnalla, tarvitsee se ottaa huomioon jo isä-ikkunan viestisilmukassa. Yleensä tämä tehdään seuraavasti:

	    while (GetMessage(&msg,NULL,NULL,NULL)) {
	      if ( hDlgMyModeless && IsDialogMessage(hDlgMyModeless,&msg) ) continue;
	      TranslateMessage(&msg);     /* Tulkitaan virtuaaliset näp. koodit  */
	      DispatchMessage(&msg);      /* Lähetetään viesti ikkunalle         */
	    }

Tässä hDlgMyModeless olisi globaali dialogin kahva, eli aina kun dialogi on aktiivisena, olisi sen kahvalla 0:sta eroava arvo.

No entä kun ohjelmassa tarvittaisiin yhtäaikaa kahta Modeless-dialogia? Täytyisi kirjoittaa toinen if (...!? Tämä ongelma on ratkaistu tiedostossa ALI\MDIALOG.C.

5.3.2 Modeless-dialogin vaatimat muut kutsut

Modeless -dialogi luodaan kutsulla:

	hDlgMyModeless = CreateDialog(hInstance, name, hWnd, lpProcFunc);

ja funktion osoite on jälleen varattu vastaavasti kuin Modal-dialoginkin tapauksessa. Tämän jälkeen isä-ikkunan viestisilmukka huolehtii dialogin toiminnasta. Tietenkin dialogin funktio pitää jälleen kirjoittaa. Kun isä-ikkunan viestisilmukka on loppunut, pitää Modeless-dialogin ikkunafunktion osoite poistaa

	FreeProcInstance(lpProcFunc)

5.3.3 Modeless -dialogin ikkunafunktio

Modeless -dialogin lopettaminen tulee suorittaa kutsulla:

	      DestroyWindow(hDlg);

Siis Modal -dialogin ikkunafunktio ei kelpaa sellaisenaan Modeless-dialogin ikkunafunktioksi. Näin ollen Tietoja-laatikon Modeless-versio olisi:

	BOOL CALLBACK_E ModelessAbout(HWND hDlg,UINT message, WPARAM wParam,LPARAM lParam)
	{
	  switch (message) {
	    case WM_INITDIALOG:  return TRUE;
	
	    case WM_COMMAND: {
	      int id = GET_WM_COMMAND_ID(wParam,lParam);
	      if ( id == IDOK || id == IDCANCEL ) {
	        DestroyWindow(hDlg);
	        return TRUE;
	      }
	      break;
	    }
	  }
	  return FALSE;
	}

Tuntuu tietysti tyhmältä kirjoittaa kaksi eri ikkunafunktiota, riippuen siitä käytetäänkö dialogia Modeless- vai Modal -dialogina. Tietysti tästä selvitään sopivalla #ifdef makrolla, mutta annamme kohta tällekin ongelmalle yleisemmän ratkaisun.

5.4 "Tavallinen" ikkuna

Mikäli ikkuna on luotu CreateWindow kutsulla, on sen ikkunafunktio vastaavasti:

	LONG CALLBACK_E WinAbout(HWND hDlg, UINT message, WPARAM wParam,LPARAM lParam)
	{
	  switch (message) {
	    case WM_INITDIALOG:  return NULL; /* Turha, koska tätä viestiä ei tule! */
	
	    case WM_COMMAND: {
	      int id = GET_WM_COMMAND_ID(wParam,lParam);
	      if ( id == IDOK || id == IDCANCEL ) {
	        DestroyWindow(hDlg);
	        return NULL;
	      }
	      break;
	    }
	  }
	  return DefWindowProc(hDlg, message, wParam, lParam);
	}

Siis ainoana erona on se, että paluuarvo on erilainen ja kutsutaan oletuskäsittelijää.

5.4.1 "Välittäjä"-funktiot

Tämä voidaan tietysti kaikki haudata "välittäjä"-ikkunafunktioon:

	LRESULT CALLBACK_E WinAbout(HWND hDlg,UINT message,
	                          WPARAM wParam,LPARAM lParam)
	{
	  LONG ret = ModelessAbout(hDlg,message,wParam,lParam);
	  if ( ret ) if ( ret != TRUE ) return ret; else return NULL;
	  return (DefWindowProc(hDlg, message, wParam, lParam));
	}

Näin ei tarvitsisi kirjoittaa uutta funktiota jos ikkunaa käytetäänkin tavallisena ikkunana. Edellä ModelessAbout olisi sama kuin Modeless-dialogin tapauksessa.

Vikana on tietysti se, että menetetään funktion mahdollisesti palauttama LONG-tulos. Tämä voidaan vielä kiertää siten, että ikkunafunktiot tehdään aina palauttamaan LONG arvo ja sekä dialogia että tavallista ikkunaa varten kirjoitettaisiin sopivat välittäjäfunktiot. Oikean "EI KÄSITELTY"-palautusarvon päättäminen on sitten pieni ongelma (0, -1 vaiko mikä?).

5.4.2 _export -funktion kutsuminen on vaarallista

Edellisessä välittäjäfunktiossa kutsuttiin _export -funktiota. Näin ei yleensä saa ohjelmassa tehdä. Varminta olisikin kirjoittaa ei - _export -funktio jota aina kutsutaan välittäjäfunktiolla.

Tavallinen C-aliohjelma konekielisenä alkaa seuraavasti:

	BOOL About(HWND hDlg,UINT message,WPARAM wParam,LPARAM lParam)
	- assembler:
	  PUSH BP
	  MOV  BP,SP
	  ...

CALLBACK -funktio alkaa seuraavasti:

	BOOL CALLBACK About(HWND hDlg,UINT message,WPARAM wParam,LPARAM lParam)
	- assembler:
	  MOV  AX,DS
	  NOP
	  INC  BP
	  PUSH BP
	  MOV  BP,SP
	  PUSH DS
	  MOV  DS,AX
	  ...

Lopuksi _export -funktio alkaa seuraavasti:

	BOOL CALLBACK _export About(HWND hDlg,UINT message, WPARAM wParam,LPARAM lParam)
	- assembler:
	  NOP
	  NOP
	  NOP
	  INC  BP
	  PUSH BP
	  MOV  BP,SP
	  PUSH DS
	  MOV  DS,AX
	  ...

Eli _export funktio olettaa, että AX-rekisterissä tuodaan sen hetkinen DS-rekisterin (data segment) arvo. Näin on pakko tehdä, koska _export -funktiot on tarkoitettu useamman mahdollisesti eri datasegmentin omaavien ohjelmien käyttöön. Huomattakoon, että tavallisen CALLBACK -funktion ja _export -funktion välisen muunnoksen tekee linkittäjä:

	  MOV AX,DS     ->   NOP
	                     NOP

Jos tavallisesta C:n funktiosta kutsutaan _export -funktiota (tai EXPORTS -osassa mainittuja funktioita), ei ole mitään syytä olettaa AX:llä olevan oikeaa arvoa. Todennäköisesti koko Windows kaatuu!

Mallissa ollut "välittäjä"-funktio pystyi kuitenkin kutsumaan _export -funktiota, koska se oli itse _export -funktio ja kutsu oli aivan ensimmäinen suoritettava ohjelmalause, jolloin AX:ssä oli vielä DS:än arvo. Jos kutsu siirretään myöhemmäksi, on AX jo muuttunut ja kone saattaa kaatua!

Neuvo: ÄLÄ KUTSU _EXPORT FUNKTIOITA ITSE!

5.4.3 Dialogi pääikkunana

Aikaisemmin tutkittiin dialogin käyttämistä ohjelman pääikkunana. Autolaskurissahan näin tehtiin. Joskus kuitenkin kannattaneen tehdä pääikkuna rehellisesti CreteWindow -kutsulla ja sitten mahdolliset lapsi-ikkunat dialogeina.

5.5 Moni-ikkunainen sovellus

5.5.1 Vaihtaja (MULTIWIN\VAIHTAJA\VAIHTAJA.C)

Teemme seuraavaksi sovelluksen, jossa on useita ikkunoita:

1. sovelluksen pääikkuna (tavallinen ikkuna)

2. mittakaavan muunnosikkuna (modeless, Sovellukset: dialog Scaling)

3. rahanvaihtoikkuna (modeless, Sovellukset: dialog Change)

4. tietoja-ikkuna (modal, Apua: dialog About)

Ikkunat 2-4 ovat pääikkunan lapsia. Tietoja-ikkuna on modal-ikkuna, joka on modaalinen pääikkunalle. Siis mittakaavan muunnosta tai rahanvaihtoa voidaan jatkaa vaikka tietoja ikkuna olisikin näkyvissä. Kuitenkin pääikkunan menuihin ei päästä käsiksi ennen tietoja-ikkunan sulkemista.

Koska aluksi ikkunat tarvitsevat joukon globaaleja muuttujia, ei samaa ikkunaa sallita avattavaksi kahteen kertaan. Ongelma korjataan myöhemmin DLL-luvussa ja samaa ideaa käyttäen voimme tähänkin ohjelmaan sallia useita mittakaavanmuuttajia käytettäväksi samanaikaisesti.

Tapoja globaalien muuttujien välttämiseksi:

1. Tietojen säilyttäminen dialogin kentissä. Esimerkissä tämä riittää hyvin, ja tällainen toteutus löytyy dialogin ohjelmasta SCALIN3.C.

2. Edellä mainittu tapa tallettaa globaalit muuttujat ikkunakahvakohtaisesti taulukkoon. Toteutus dialogin ohjelmassa SCALING2.C ja MDIALOG.C.

3. Globaalin muistilohkon osoitteen tallettaminen ikkunan ylimääräisiin tavuihin. Tämä vaatisi kuitenkin dialogi-luokan uudelleen määrityksen, jotta luokan tavujen lukumäärää voitaisiin lisätä.

4. Globaalin muistilohkon osoitteen tallettaminen ikkunan ominaisuustietoihin (SetProp, malli käytöstä ks. [DiLascia]).

5.5.2 Useat Modeless-dialogit (ALI\MDIALOG.C)

Jo aiemmin todettiin hankaluus, mikäli halutaan avata useita modeless-dialogeja. Kutakin varten pitäisi viestisilmukkaan lisätä oma if (IsDialogMessage..). Tällä tavoin ohjelmasta ei tule kovinkaan helposti ylläpidettävää.

Ongelmaa voidaan tietysti kiertää tekemällä em. if lause dynaamisesti: kaikki käytössä olevat modeless-dialogit ovat taulukossa Modeless.dialogs ja kokeillaan viestiä kullekin niistä:

	int IsModelessMessage(MSG *msg)
	{
	  int i;
	  for (i=0; i<Modeless.n; i++)
	    if ( Modeless.dialogs[i].visible && 
	         IsDialogMessage(Modeless.dialogs[i].hwnd,msg) ) return 1;
	  return 0;
	}

Globaaliin dialogitaulukkoon on talletettu ainakin dialogin nimi ja dialogin näkyvissä olo sekä dialogin funktion osoite ja tietysti dialogin ikkunan kahva:

	typedef struct {
	  char name[20];
	  int visible;
	  HWND hwnd;
	  FARPROC lpProcFunc;
	} modeless_type;
	
	#define MAX_DIALOGS 10
	static struct {
	  int n;
	  modeless_type dialogs[MAX_DIALOGS];
	} Modeless = {0};

Uusi dialogi voidaan lisätä taulukkoon aliohjelmalla

	int DoModelessDialog(HWND hWnd,char *name,PDIALOGPROC func)

"Pääohjelmassa" kutsut voisivat tulla vaikkapa menuvalinnoista:

	static EVENT WM_command_IDM_APP_SCALING(tMSGParam *msg)
	{
	  DoModelessDialog(msg->hWnd,"SCALING",Scaling); return 0;
	}
	
	static EVENT WM_command_IDM_APP_MONEY(tMSGParam *msg)
	{
	  DoModelessDialog(msg->hWnd,"CHANGE",Money); return 0;
	}
	
	static EVENT WM_command_IDM_HELP_ABOUT(tMSGParam *msg)
	{
	  DoModalDialog(msg->hWnd,"About",MDialogAbout); return 0;
	}

Näin viestisilmukkaan "aktivoituisi" uusi dialogiviestin tarkistus!

Dialogin lisääminen voidaan tehdä siten, että mikäli saman niminen dialogi yritetään avata uudelleen, aktivoidaankin vanha. Näin estetään kaksinkertaiset esiintymät. Jos globaalit muuttujat eivät estä moninkertaisia esiintymiä, niin poistetaan em. esto (DoMultiModelessDialog). Lisäksi jo olemassa olevan dialogin funktion osoitetta ei kannattane luoda uudelleen, vaan voidaan käyttää vanhoja. Samoin dialogin poisto voitaneen tehdä siten, että se vain merkitään poissaolevaksi, mutta jätetään nimi ja funktion osoite paikoilleen:

	      case IDCANCEL:
	        DestroyDialog(hDlg,1);
	        return TRUE;
	      default: break;

Samalla voimme ratkaista modal / modeless dialogin lopettamisen eron. Jos jokainen modeless-dialogi on luotu omalla kutsullamme, niin silloin se löytyy taulukosta ja tuhotaan kuten modeless tuhotaan. Muuten sen täytyy olla modal ja tuhotaan se omalla tavallaan:

	int DestroyDialog(HWND hWnd,int retval)
	/* Tuhoaa modeless dialogit DestroyWindow-komennolla ja muut dialogit      */
	/* EndDialog -komennolla (Modal)                                           */
	{
	  int i;
	  for (i=0; i<Modeless.n; i++)
	    if ( Modeless.dialogs[i].hwnd == hWnd ){
	      Modeless.dialogs[i].visible = 0;
	      DestroyWindow(Modeless.dialogs[i].hwnd);
	      Modeless.dialogs[i].hwnd = NULL;
	      return retval;
	    }
	  EndDialog(hWnd,retval);
	  return retval;
	}

Nyt siis sama ikkunafunktio kelpaa sekä modal että modeless-dialogille! Edellä olevat aliohjelmat on koottu tiedostoon MDIALOG.C. Käytännössä ratkaisut ovat hieman monimutkaisempia, koska edellä mainittu globaalien muuttujien ikkunakohtainen talletus on toteutettu samojen aliohjelmien yhteyteen.

5.5.3 Combo-boxit ja listat

Ohjelman tuntemat mittayksiköt on talletettu taulukkoon josta selviää paljonko ko. mittayksikkö on milleinä:

	typedef struct {
	  char   *unit;
	  double mm;
	} unit_type;
	
	static unit_type units[] = {
	  { "mm",                1.0     },
	  { "cm",               10.0     },
	  { "dm",              100.0     },
	...
	  { "syli",           1781.4     },
	  { "virsta",      1068800.0     },
	  { "peninkulma", 10688000.0     }
	};

Taulukon mittayksiköt ovat Combo-boxissa (List-Box, jossa valittu alkio näkyy yhdellä rivillä ja valinnan ajaksi se voidaan laajentaa list-boxiksi):

Combo-boxiin valintoja lisätään esimerkiksi kutsulla:

	SendDlgItemMessage(hDlg,ID_MAP_UNIT,CB_INSERTSTRING,i,
	                   (LPARAM) ((LPSTR)units[i].unit));

Huomattakoon, että edellä tyypinmuunnos (LPARAM)((LPSTR)...) on pakollinen, koska merkkijonon osoittimeksi on ensinnä saatava pitkä osoitin ja tämän jälkeen siitä pitää saada vielä LPARAM -tyyppi.

Tämä tapa ei kuitenkaan toimi WIN32:ssa, joten lisäys on syytä tehdä WINDOWSX.H:n makrolla:

	HWND hCBMapU = GetDlgItem(msg->hWnd,ID_MAP_UNIT);
	...
	  (void)ComboBox_InsertString(hCBMapU,i,units[i].unit);

Lisäykseen voisi käyttää myös listan loppuun lisäävää:

	(void)ComboBox_AddString(hCBMapU,"kyynärä");

Jos lista haluttaisiin täyttää vaikkapa oletushakemiston niillä *.c -tiedoston nimillä, jotka on kirjoitussuojattu, voitaisiin kutsua:

	ComboBox_Dir(hwndCtl, DDL_READONLY | DDL_EXCLUSIVE, "*.c"); 

List-boxiin lisätään aivan vastaavasti, paitsi kutsulla:

	ListBox_AddString(hMyList,"kyynärä")

Combo-boxissa näkyvä alkuarvo valitaan kutsulla:

	(void)ComboBox_SetCurSel(hCBMapU,MM);

Kun Combo-boxista tehdään valinta, saadaan WM_COMMAND -viestin parametrina viestin aiheuttaneen Combo-boxin tunnus (ID-osa) ja viestin syy:

	static EVENT WM_command_ID_MAP_UNIT__CBN_SELCHANGE(tMSGParam *msg)
	{
	  map_unit = ComboBox_GetCurSel(H(ID_OUT_UNIT));
	  return SEND_COMMAND(change_other ? DO_SHOW_MAP : DO_SHOW_OUT);
	}

Edellä map_unit on ikkunan "sisäinen" static -muuttuja, jossa on sen mittayksikön indeksi, joka on valittuna kartalta mitatulle matkalle. Makro H on yksinkertaisesti:

	#define H(id) GetDlgItem(msg->hWnd,id)

Mittayksikön vaihtaminen aiheuttaa tietenkin muutoksen dialogissa näytettävissä luvuissa. Loogisen muuttujan other avulla valitaan kumpiko muuttuu: kartalta mitattu matka (DO_SHOW_MAP) vaiko maastossa oleva matka (DO_SHOW_OUT). Makron SEND_COMMAND tehtävänä on lähettää ikkunalle viesti siitä, että jotain pitäisi tehdä.

5.5.4 Ikkunalle itselleen lähetettävät viestit

Windows-ohjelmoinnissa on hyvin yleistä myös lähettää viestejä. Mikäli jonkin toisen käynnissäolevan ohjelman halutaan tekevän jotakin, voidaan sille lähettää viesti. Usein viestejä lähetetään myös omalle ikkunalle, jotta vältettäisiin goto -lauseiden käyttö.

Viestit DO_SHOW_MAP ja DO_SHOW_OUT on tarkoitettu vain ohjelman sisäiseen viestintään, mutta miksei jokin toinenkin ohjelma voisi lähettää tälle ikkunalle pyyntöä näyttää arvoja uudelleen.

	static EVENT WM_command_DO_SHOW_OUT(tMSGParam *msg)
	{
	  out_dist = map_dist*units[map_unit].mm*scale/units[out_unit].mm;
	  return ShowDouble(msg->hWnd,ID_OUT_DIST,out_dist);
	}

Tietysti em. näyttämiset olisi tässä tapauksessa ollut helpompi kirjoittaa omiksi aliohjelmikseen, mutta esimerkin vuoksi ne toteutettiin ikkunan sisäisinä viesteinä.

5.5.5 Arvon muuttaminen heti kun edit-kenttä muuttuu

Malliohjelmassa on "käyttäjäystävällisyyden" vuoksi valittu tapa, jossa muutos kartalla mitatussa matkassa näkyy heti luonnossa olevassa matkassa ilman Return-näppäimen painamisen tarvetta.

	static EVENT WM_command_ID_MAP_DIST__EN_CHANGE(tMSGParam *msg)
	{
	  map_dist = GetDouble(msg->hWnd,msg->wParam,0);
	  return SEND_COMMAND(DO_SHOW_OUT);
	}

Valinta aiheuttaa vain yhden ongelman: koska myös ID_OUT_DIST on vastaava edit-kenttä, aiheuttaa tuloksen näyttäminen tässä edit-kentässä muutoksen siihen ja näin ollen dialogin ikkunalle viestin tämän edit-kentän muuttumisesta. Ohjelman halutaan toimivan myös kääntäen, eli jos käyttäjä kirjoittaa luonnossa mitatun matkan, niin sen halutaan muuttavan vastaavasti kartalta mitattua matkaa. Näin ollen muutos luonnossa mitatussa matkassa aiheuttaa muutoksen kartalla mitatussa matkassa joka aiheuttaa muutoksen luonnossa mitatussa matkassa joka...

Siis kehä on valmis! Toistuvan rekursion kiertämiseksi edit-kenttään arvon näyttävä aliohjelma

	  ShowDouble(hDlg,ID_MAP_DIST,map_dist);

on kirjoitettu siten, ettei se muuta kentän arvoa, mikäli kentän arvon suhteellinen muutos on pienempi kuin valittu pieni luku.

5.5.6 Autoradio-button

Radio-button ajatus on kotoisin vanhan matkaradion asemanvalintanäppäimistä. Vain yksi asema kerrallaan voi olla valittuna. Autoradio-button (= automatic radio-button) hoitaa automaattisesti sen, että valittaessa jokin toinen näppäin, muut eivät enää ole valittuja.

Aluksi dialogin alustuksessa valitaan jokin nappuloista aktiiviseksi:

	(void)Button_SetCheck(H(IDR_OTHER),TRUE);

Kun jokin nappula (tässä tapauksessa toinen kahdesta) painetaan, saadaan jälleen WM_COMMAND -viesti, jonka ID -osassa on nappulan tunnus:

	static EVENT WM_command_IDR_THIS(tMSGParam *msg)
	{
	  change_other = 0;
	  return 0;
	}
	
	static EVENT WM_command_IDR_OTHER(tMSGParam *msg)
	{
	  change_other = 1;
	  return 0;
	}

5.5.7 Aktiivisen kentän vaihtaminen

"Käyttäjäystävällisyyden" nimissä ohjelmassa on lisäksi ominaisuus, että painettaessa Enter-näppäintä, siirrytään aina syöttämään kartalta mitattua matkaa, koska todennäköisesti tämä on ohjelman eniten käytetty syöttökenttä. Samalla koko kentän teksti valitaan aktiiviseksi, jotta käyttäjän ensimmäiseksi kirjoittama merkki tuhoaisi kentän sisällön. Tämäkin on todennäköisesti yleisin tarve.

Kun Enter (Return)-näppäintä painetaan dialogin ollessa aktiivinen, saadaan WM_COMMAND -viesti ja ID -osassa tunnus IDOK (vastaavasti ESC-näppäimestä IDCANCEL):

	static EVENT WM_command_IDOK(tMSGParam *msg)
	{
	  SetFocus(H(ID_MAP_DIST)); Edit_SetSel(H(ID_MAP_DIST),0,-1);
	  return 0;
	}
	
	static EVENT WM_command_IDCANCEL(tMSGParam *msg)
	{
	  return DestroyDialog(msg->hWnd, TRUE);
	}

SetFocus vaihtaa aktiivisen ikkunan (sen focus-pisteen, eli kuka saa näppäinviestit). Edit_SetSel -viesti valitsee sitten edit-tyyppisen kentän tietyt merkit, edellähän valittiin kaikki.

Mikäli kentän muutoksen EI haluttaisi näkyvän heti toisessa kentässä, vaan vasta kun on painettu Enter, pitäisi IDOK -viesti käsitellä siten, että kun se tulee, katsotaan millä kentällä on focus, luetaan sen kentän arvo ja muutetaan vastaavasti muita kenttiä.

5.6 Eri kielten käyttö samassa ohjelmassa (MULTIWIN\VAIHTAJA\VAIHTAJ3.C, ALI\TRANSDLG.C, ALI\LANGUAGE.C)

Usein ohjelma aluksi kehitetään jollakin kielellä ja halutaan sitten toimivaksi myös muun kielisenä. Jälkeenpäin tämä muuttaminen voi olla hankalaa. Eräs keino monikielisten ohjelmien tekoon on tehdä kutakin kieltä varten oma resurssitiedosto ja laittaa kaikki ohjelman tulostuksissa tarvitsemat merkkijonot merkkijonoresursseiksi. Menujen osaltahan tätä ideaa käytettiin jo autolaskurin eräässä versiossa.

Kuitenkin resurssitiedostojen muuttaminen ja ylläpitäminen kaikille kielille tahtoo usein unohtua. Erityisen raskasta on muistaa lisätä merkkijonoresursseja jokaista ohjelmassa tarvittavaa merkkijonoa varten. Niinpä vaihtajaohjelman kolmannessa versiossa on esitetty malli, jossa ohjelmoijan ei paljoa tarvitse ottaa käännöstä huomioon:

	    1) Tulostettaviin teksteihin laitetaan kutsu
	         T(teksti).
	
	       Esim. jos normaalisti kirjoitettaisiin:
	          MessageBox(hWnd, "Tallennus epäonnistui!",WTITLE,...
	
	       kirjoitetaan nyt muodossa:
	          MessageBox(hWnd, T("Tallennus epäonnistui!"),T(WTITLE),...
	
	       Jos jonossa on muuttujia toimitaan seuraavasti:
	         printf("Minulla on rahaa %d mk.",rahaa);
	
	       sijasta kirjoitetaan:
	         printf(T("Minulla on rahaa %d mk."),rahaa);
	
	    2) Dialogien alustuksiin (WM_INITDIALOG) lisätään kutsu: 
	         TranslateDialog(hDlg,0)
	    2) tai käytetään kielen vaihtuessa kutsua
	         TranslateProgram(msg->hWnd,0);
	
	    3) Ohjelman alkuun kutsu:
	         ReadLanguage(oma_inifile,kaannos_polku,oletus_kaannos);
	    
	    4) Ohjelman loppuun kutsu:
	         SaveLanguage(oma_inifile,kaannos_polku,oletus_kaannos)

Itse kääntäminen perustuu sanastotiedostoon, joka on muotoa:

	"Tallennus epäonnistui!"  "Failed to save the file!"
	"Minulla on rahaa %d mk." "I have $%d"

ja jota T-makro ylläpitää automaattisesti (eli lisää sinne kääntämistä vailla olevat sanonnat).

Lisää aliohjelmakirjastojen käytöstä löytyy TRANSDLG.C ja LANGUAGE.C -tiedostojen kommenteista. Malliohjelmassa kielen vaihtamisen takia ei ole tarvinnut tehdä muutoksia yhteenkään dialogin käsittelijäfunktioon!

5.7 Tulostus

Tulostus on tietysti tietojenkäsittelyn tärkeimpiä toimintoja. Aluksi Windowsista tulostaminen saattaa hirvittää kaikkien mahdollisten eri kirjoittimien takia, mutta tätähän varten Windows on. Se huolehtii eri tulostimien välisistä eroista. Ennen Windowsia tulostaminen olikin ohjelmoijan kohtalonpaikka. Nyt yksinkertaiset (jopa graafiset) tulosteet saadaan yhtä helposti (oliko se helppoa?) kuin näyttöönkin tulostaminen. Pitää vain avata laiteyhteys tulostimelle.

Tietysti tulee pieniä ongelmia eri tulostimien välisissä resoluutioissa, niiden kyvyssä tulostaa grafiikkaa jne. Tällöin voidaan tarvittaessa ohjelmaan tehdä hyvinkin yksilöllisiä toimintoja eri tulostimia varten. Windows antaa varsin hyvät tiedot eri tulostimien ominaisuuksista. Emme kuitenkaan lähde tälle tielle, vaan tyydymme nyt yksinkertaisiin tulosteisiin.

5.7.1 Yksinkertainen tulostus (TULOSTUS\HELLOPRI.C)

Ensimmäinen tehtävä on avata laiteyhteys kirjoittimelle. Mikäli halutaan käyttää Windowsin oletuskirjoitinta, on tehtävä varsin helppo. Luetaan WIN.INI -tiedostosta kirjoittimen tiedot ja tulostetaan.

	WIN.INI:
	[Windows]
	...
	device=Epson LX-800,EPSON9,LPT1:
	...

Seuraava malli taitaakin olla monisteen lyhin täydellinen itsenäinen Windows-ohjelma (mitä ohjelma tekee?):

	#include <windows.h>
	#include <string.h>
	/****************************************************************************/
	HDC CreatePrinterDC (void)
	{
	  static char s[80] ;
	  char        *szDevice, *szDriver, *szOutput ;
	
	  GetProfileString ("windows", "device", ",,,", s,sizeof(s)) ;
	  if ( (szDevice = strtok (s   ,"," ) ) != NULL  &&
	       (szDriver = strtok (NULL,",") ) != NULL  &&
	       (szOutput = strtok (NULL,",") ) != NULL )
	     return CreateDC (szDriver, szDevice, szOutput, NULL) ;
	
	  return 0;
	}
	
	/****************************************************************************/
	int PASCAL WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance,
	                    LPSTR lpszCmdLine, int nCmdShow)
	{
	#pragma argsused
	  HDC hdcPrinter = CreatePrinterDC() ;
	  if ( hdcPrinter == NULL) return 1;
	  if ( Escape(hdcPrinter,STARTDOC,8,"HelloPri",NULL) <= 0 ) return 2;
	  TextOut(hdcPrinter,20,20,"Hello World!",12);
	  Escape(hdcPrinter, NEWFRAME, 0 , 0L, 0L);
	  Escape(hdcPrinter, ENDDOC, 0 , 0L, 0L);
	  DeleteDC(hdcPrinter) ;
	  return 0; 
	}

Kun kirjoittimen tiedot on saatu, luodaan vastaava laiteyhteys. Pääohjelmassa tulostus on laiteyhteyden saamisen jälkeen aloitettu Escape-funktiolla, jonka avulla Windowsille voidaan kertoa dokumentin tulostamiseen liittyviä asioita, kuten aloitus (STARTDOC), sivunvaihto (NEWFRAME) ja lopetus (ENDDOC).

5.7.2 Kaistatekniikka (banding)

Jos tulostin ei osaa tulostaa viivagrafiikkaa (kuten PS-tulostin ja piirturit osaavat), pitää grafiikka tulostaa pistegrafiikkana (useat laserit, matriisikirjoittimet). Tämä johtaa siihen, että yhden sivun koko saattaa kasvaa niin suureksi, ettei se mahdu koneen muistiin. Viivaahan ei voida piirtää viivana, koska esim. matriisikirjoittimet eivät osaa kunnolla peruuttaa.

Windows hoitaa ongelman siten, että se tulostaa ensin meta-tiedostoon (meta file) ja kun tulee NEWFRAME Escape, niin aloitetaan meta-tiedoston tulostus. Kuitenkin myös meta-tiedosto on vain joukko piirtokäskyjä kuten viivoja, ellipsejä jne. Näin meta-tiedostoa tulostettaessa tulee sama ongelma tulostettavan bittikuvan muistiin mahtumisesta.

Tällöin Windows käyttää kaistatekniikkaa (banding technique). Valitaan sen levyinen kaista, jonka kokoinen bittikuva mahtuu hyvin muistiin ja ajetaan metatiedosto leikaten tällä kaistalla. Näin saadaan tuloksen ensimmäinen osa valmiiksi muistiin. Tämä bittikartta tulostetaan sitten kirjoittimelle. Siirrytään seuraavaan kaistaan ja käydään koko meta-tiedosto uudestaan lävitse jne. Kuitenkin tästä kaikesta huolehtii Windows, eikä ohjelmoijan tarvitse sitä surra vaikka laite ilmoittaisikin vaativansa kaistatekniikkaa.

5.7.3 Resoluutio-ongelmat

Suuremmaksi ongelmaksi ohjelmoijalle muodostuu se, että tulostimessa ja näytössä pikselit ovat yleensä eri kokoisia. Tulostuksen malliohjelmassa TextOut voitaisiin tietysti korvata myös kutsulla aikaisemmin tekemäämme rekursiiviseen kolmioon (KOLMIOM.C). Kolmio oli kuitenkin suunniteltu suurinpiirtein näytön täyttäväksi (noin 400 pistettä korkea). Lasertulostimien tarkkuus on ainakin 300 pistettä/tuuma, eli koko kuva olisi tulostettuna vain vähän yli tuuman kokoinen!

Mikäli alkuperäinen kuva perustuu laitekoordinaatteihin (1 piste = 1 yksikkö) tulevat nämä ongelmat esille. Windowsissa on kuitenkin mahdollisuus käyttää muitakin koordinaatistoja, jolloin esim metrisellä koordinaatistolla kuva on samankokoinen sekä näytöllä että paperilla.

5.7.4 Keskeytysongelmat

"Hello World" -tulostus ei kauan kestä, mutta kolmion tulostaminen sitäkin kauemmin. Tulostus on yleensä voitava keskeyttää tai sen ajaksi on voitava siirtyä toiseen ohjelmaan. Ongelma on oikeastaan täsmälleen sama kuin muutenkin Windowsin moniajon mahdollistamisessa. Sikäli tilanne helpottuu, että Windows lisää itse mahdollisuuden tarkistusfunktioon jokaiseen laiteyhteydelle tulevan kutsun yhteyteen (kuten esim. LineTo, Ellipse, TextOut jne.), mutta ohjelman on sitten osattava itse lopettaa piirtäminen.

5.7.5 Tulostuslaitteen vaihtaminen

Aina ei haluta tulostaa samalle laitteelle. Windows voidaankin installoida samalla useammalle kirjoittimelle. Esimerkiksi verkkokäytössä on luonnollistakin vaihtaa tulostimeksi joko oma paikallinen matriisikirjoitin, verkossa oleva piirturi, väri-PS tai joku muu laserkirjoitin.

Usein täytyy valita myös kirjoittimesta tiedostoon tulostava versio, jotta viivagrafiikkana oleva kuva saadaan pienemmällä tilankulutuksella liitettyä tekstinkäsittelyohjelmaan. Paras kirjoitinvalinta tällöin on ehkä HP-piirturi ja tulostus tiedostoon.

Voidaan tietenkin käydä vaihtamassa oletuskirjoitin aina ennen tulostuksen aloittamista, mutta käytännössä tämä on liian työlästä. Parempi on siis lisätä ohjelmaan valikko käytettävistä kirjoittimista. Tällainen valikko löytyy valmiina COMMDLG.DLL -kirjastosta. Samassa dialogissa voidaan valita tulostettavien sivujen määrä, sivun asento jne. Tosin ohjelmoija on vastuussa näiden toimintojen toteuttamisesta.

5.7.6 Tulostuskirjaston käyttö (TULOSTUS\HELLOPR3.C, ALI\TULOSTUS.C)

Koko kirjoittimen hallintaa varten kannattaakin ehkä kirjoittaa valmis kirjasto, jossa on valmiina laiteyhteyden luominen, kirjoittimen valinta ja keskeytyksessä tarvittavat operaatiot, sekä mahdollisesti jonkinlainen resoluutioerot tasaava skaalaus.

Esimerkkinä on kirjoitettu kirjasto tulostus.c, jota voidaan käyttää seuraavien esimerkkien mukaisesti. Seuraavana "ensimmäisen tulostusesimerkin" kirjastoa käyttävä versio:

	#include <windows.h>
	#include "tulostus.h"
	/****************************************************************************/
	int PASCAL WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance,
	                    LPSTR lpszCmdLine, int nCmdShow)
	{
	#pragma argsused
	  HDC hdcPrinter;
	  if ( (hdcPrinter = StartPrinting(NULL,0) ) == NULL) return 1;
	  TextOut(hdcPrinter,20,20,"Hello World!",12);
	  StopPrinting(NULL,hdcPrinter);
	  MessageBeep(MB_OK); /* Ei tarvita, mutta kun ei tul. mitään, niin kuul*/
	  return 0;
	}

Seuraavaksi kolmion tulostus siten, että tuloskolmion koko on samassa suhteessa paperin leveyteen, kuin se suunniteltaessa oli suhteessa näytön leveyteen (PSC_DISPLAY). Lisäksi ennen tulostuksen alkua kysytään laite, jolle tulostetaan (SetupPrinter). Tulostus on myös mahdollista keskeyttää, koska kolmion piirrossa oli alunperin kutsu CheckMessage, joka tulostuskirjastossa tutkii onko tulostus pyydetty keskeyttämään.

	#include <windows.h>
	#include "tulostus.h"
	#include "kolmiom.h"
	
	/****************************************************************************/
	int PASCAL WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance,
	                    LPSTR lpszCmdLine, int nCmdShow)
	{
	#pragma argsused
	  HDC hdcPrinter;
	  SetupPrinter(0);
	  InformPrinter(hInstance); // Tarvitaan koska ei ikkunaa!
	  if ( (hdcPrinter = StartPrinting(NULL,PSC_DISPLAY) ) == NULL) return 1;
	  MyDraw(NULL,hdcPrinter);
	  StopPrinting(NULL,hdcPrinter);
	  MessageBeep(MB_OK);
	  return 0;
	}

Tavallisestihan tulostus valitaan menusta, samoinkuin kirjoittimen vaihto:

	case IDM_FILE_PRINT:                                         
	  if ( ( hDCPrinter = StartPrinting(hWnd,PSC_DISPLAY) ) != NULL ) {
	    MyDraw(hWnd,hDCPrinter);
	    StopPrinting(hWnd,hDCPrinter);
	  } 
	  return NULL; 
	
	case IDM_FILE_PRINTER_SETUP:
	  SetupPrinter(1);
	  return NULL;

Lisää kirjaston käytöstä löytyy TULOSTUS.C -aliohjelman kommenteista. Ohjelmaan voidaan lisätä myös normaali aikaisemmin esitetty keskeytys- ja moniajomenettely kirjastolla ALI\CHECKER.C.

5.7.7 Aliohjelmakirjasto tulostukseen (ALI\TULOSTUS.C)

Kirjaston käyttämisessä esimerkiksi SetupPrinter -kutsu täyttää tietueen, jossa on valittujen sivujen määrä, paperin asento jne. Tämä tietue on toistaiseksi globaalina muuttujana, jottei ohjelmoijan tarvitse tehdä enempää sen olemassaolon varmistamiseksi. Kirjoittimen valinnan tavoitteena on tietysti se, että aina uudella valintakerralla edellisen kerran valinta olisi oletuksena. Tämä teettää jonkin verran ylimääräistä työtä SetupPrinter -aliohjelmaan. Muuten aliohjelma pohjautuu PrintDlg -dialogiin.

Jotta Windows valvoisi tulostuksen mahdollista keskeyttämistä, pitää keskeytystä valvova funktio ilmoittaa Windowsille ennen tulostuksen aloittamista. Tämä tehdään aliohjelmassa StartPrinting:

	GlobalPrinter.PrinterBreakFunc =
	    MakeProcInstance((FARPROC)PrinterBreak,GlobalPrinter.hInstance);  Escape(GlobalPrinter.hPrinterDC,SETABORTPROC,NULL,
	         (LPSTR)(long)GlobalPrinter.PrinterBreakFunc,NULL);

Keskeytysfunktio on viestisilmukka, joka ylläpitää keskeytysdialogia tulostuksen aikana:

	int CALLBACK_E PrinterBreak(HDC hPrinterDC,int code)
	/* Viestisilmukka tulostuksen aikana.                                       */
	{
	  MSG msg;
	  while ( !GlobalPrinter.BreakPrinting &&
	           PeekMessage(&msg,NULL,NULL,NULL,TRUE) ) {
	    if ( IsDialogMessage(GlobalPrinter.PrinterDialogHWND,&msg ) ) continue;
	    TranslateMessage(&msg);
	    DispatchMessage(&msg);
	  }
	  return ( !GlobalPrinter.BreakPrinting );
	}

Myös keskeytysdialogi luodaan ennen tulostuksen aloittamista funktiossa StartPrinting:

	  GlobalPrinter.PrinterDlgFunc   =
	    MakeProcInstance((FARPROC)PrinterDialogFunction,GlobalPrinter.hInstance);
	  GlobalPrinter.PrinterDialogHWND =
	    CreateDialog(GlobalPrinter.hInstance,"BreakPrinter",hWnd,
	                 GlobalPrinter.PrinterDlgFunc);

Vielä ennen StartPrinting -funktion loppumista pitää tulostava ikkuna tehdä epäaktiiviseksi, jottei siitä vaan vahingossa voida käynnistää uusia tulostuksia tms.

	  if (hWnd) EnableWindow(hWnd,FALSE);

Tämän jälkeen tulostava ikkuna ei enää saa hiiri - eikä näppäinviestejä. Sen ainoa yhteys ulkomaailmaan on keskeytysdialogi, joka edellä luotiin. Keskeytysdialogin ikkunafunktio huolehtii sitten mahdollisen CANCEL-näppäimen painamisesta:

	int CALLBACK_E PrinterDialogFunction(HWND hWnd,unsigned message,
	                                     WPARAM wParam,LPARAM lParam)
	{
	  switch (message) {
	    case WM_COMMAND:
	      return ( GlobalPrinter.BreakPrinting = TRUE );
	
	    case WM_INITDIALOG:
	      SetFocus(GetDlgItem(hWnd,IDCANCEL));
	      return TRUE;
	  }
	  return FALSE;
	}

Kun tulostus on valmis (tai keskeytetty) pitää pääohjelman kutsua funktiota StopPrinting, joka mm. tekee pääikkunasta jälleen aktiivisen:

	int StopPrinting(HWND hWnd,HDC hPDC)
	{
	  if ( !GlobalPrinter.BreakPrinting ) {
	    Escape(hPDC, NEWFRAME, 0 , 0L, 0L);
	    Escape(hPDC, ENDDOC, 0 , 0L, 0L);
	  }
	  if (hWnd) EnableWindow(hWnd,TRUE);
	  FreePrinterRes();
	  return 1;
	}

Omassa ohjelmassa tulisi sitten tarkkailla globaalia lippua:

	      GlobalPrinter.BreakPrinting  - tulostus keskeytetty

Tämä voidaan tehdä myös CheckMessage() -kutsun avulla:

	  if ( CheckMessage() ) return;  

Aliohjelmahan oli alunperin kirjastossa Checker.c, mutta mikäli projektiin lisätään globaali määritys NO_CHECKER, niin aliohjelmasta tehdään pelkästään em. lippua tarkistava versio myös tulostus-kirjastoon. Globaali määritys esim. Borlandin editorissa saadaan: Options-Project-Topics-Compiler-Defines ja kirjoitetaan NO_CHECKER.

5.8 Koordinaatistomuunnokset

Jos käyttäjä on piirtänyt alkuperäisen kuvansa näytölle ilman skaalausta käyttäen näytön pikseleitä yksikköinä, on tulostuksen kanssa oltava tarkkana. Mikäli paperille halutaan samassa skaalassa oleva kuva kuin alkuperäinen, pitää aina tehdä jonkinlainen koordinaatistomuunnos. Tulostus-kirjastossa on valmiina muutama muunnos:

1. paperin leveys on n -pikseliä

StartPrinting(hWnd,n);

2. paperin leveys on sama kuin ikkunan leveys

StartPrinting(hWnd,PSC_WINDOW);

3. paperin leveys on sama kuin näytön leveys

StartPrinting(hWnd,PSC_DISPLAY);

Jos käyttäjä haluaa käyttää näitä muunnoksia, ei hän saa tehdä enää muita koordinaatistomuunnoksia. Tietenkin jos todella halutaan tehdä koordinaatistomuunnos, niin se ei ole kielletty, muunnos ei vaan ole kumuloituva (paitsi osittain käytettäessä muunnosta MM_ANISOTROPIC).

Tulostuskirjastossa muunnokset on toteutettu aliohjelmalla:

	void ScalePaper(HWND hwnd, HDC hPDC, int MapMode)
	/* Skaalataan MapModen mukaan paperin koko "sopivaksi"                      */
	/* y-koordinaatti kasvaa alaspäin!                                          */
	{
	  int scale;
	  HDC hic;
	  RECT rc;
	
	  if ( !MapMode ) return;
	
	  if ( MapMode > 0 ) scale = MapMode;
	  else {
	    switch ( MapMode ) {
	      case PSC_DISPLAY:
	        hic = CreateIC("DISPLAY",NULL,NULL,NULL);
	        scale = GetDeviceCaps(hic,HORZRES); /* Näytön leveys                */
	        DeleteDC(hic);
	        break;
	
	      case PSC_WINDOW:
	        GetClientRect(hwnd,&rc);
	        scale = rc.right;                   /* Ikkunan leveys               */ 
	        break;
	
	      default: return;
	    }
	  }
	
	  SetMapMode(hPDC,MM_ISOTROPIC);
	  SetWindowExt(hPDC,scale,scale); 
	  SetViewportExt(hPDC,GetDeviceCaps(hPDC,HORZRES),GetDeviceCaps(hPDC,VERTRES));
	}

Kolme viimeistä riviä ovat varsinaiset muunnoksen suorittavat rivit. MM_ISOTROPIC kertoo, että halutaan koordinaatisto jossa annetaan itse fyysisen ja loogisen pikselin suhde. Erityisesti halutaan "pyöreät" pikselit, eli fyysisesti 100 yksikköä leveyssuunnassa on yhtä paljon kuin 100 yksikkö korkeussuunnassa.

SetWindowExt -kutsulla kerrotaan kuinka monta loogista yksikköä tulostusalueen halutaan olevan kumpaankin suuntaan (jos ei sama suhde kuin paperissa, niin vajaaksi jäävää kasvatetaan automaattisesti).

SetViewportExt -kutsulla kerrotaan tulostusalueen koko tulostuslaitteen laitekoordinaatteina. Näin ollen suhde

	scale/HORZRES

määrää fyysisen ja loogisen pikselin välisen skaalauskertoimen.

Kutsujen jälkeen piirtäminen suoritetaan aivan samoilla kutsuilla kuin ennenkin. Windowsin GDI (Graphics Device Interface) huolehtii muuntamisesta.

Koordinaatistoa voidaan tietysti muuttaa näytölläkin. Tällöin on muistettava, että hiiri palauttaa aina laitekoordinaatteja. Jos koordinaatistomuunnos on käytössä ja halutaan tietää hiiren koordinaatit loogisissa koordinaateissa, täytyy käyttää kutsua

	DPtoLP(hdc,lpPoints,nNumber);

On olemassa myös käänteinen muunnos:

	LPtoDP(hdc,lpPoints,nNumber);

Muut käytettävissä olevat (valmiit) koordinaatistomuunnokset:

Muunnos

loog. yks.
pyöreys
x kasv.
y kasv.
Huom!
MM_ANISOTROPIC
valittavissa
x != y
valit.
valit.

MM_HIENGLISH
0.001 inch
x = y
oik.
ylös

MM_HIMETRIC
0.01 mm
x = y
oik.
ylös

MM_ISOTROPIC
valittavissa
x = y
valit.
valit.

MM_LOENGLISH
0.01 inch
x = y
oik.
ylös

MM_LOMETRIC
0.1 mm
x = y
oik.
ylös

MM_TEXT
pikseli
x = y ?
oik.
alas
Oletus!
MM_TWIPS
1/20 pt
x = y
oik.
ylös
1 inch = 72 pt

Huomattakoon, että vaikka MM_TEXT -muunnoksessa x=y, niin pikseli ei ole "pyöreä" kaikilla laitteilla, joten tällä muunnoksella piirretty ympyrä ei välttämättä ole pyöreä!

Jos käytetään muunnoksia, joissa y kasvaa ylöspäin, niin on muistettava että oletuksena kuvan origo on vasemmassa yläkulmassa. Tällöin jos halutaan piirtää esim koordinaatiston 1. neljännekseen (postiiviseen neljännekseen), pitää koordinaatistoa "laskea" alaspäin:

	  SetMapMode(hDC,MM_LOMETRIC); /* yksikkö 0.1 mm                       */
	  SetWindowOrg(hDC,0,300);     /* Laskisi origoa 3 cm alaspäin         */
	  SetViewportOrg(hDC,0,300);   /* Laskisi origoa 300 pikseliä alaspäin */

5.9 Dynaaminen linkitys (DLL)

Uudelleen käyttäminen on nykyään muotia. Näin tietysti myös Windows-ohjelmissa. Hyviä aliohjelmia voidaan kirjastoida, jottei niitä tarvitse kirjoittaa uudelleen. Kun Windows-ohjelmasta käynnistyy toinen esiintymä, ei itse koodia ladata uudelleen, vaan käytetään jo muistissa olevaa koodia (tämä estää itseään muuttavien ohjelmien tekemisen!). Ainostaan datasegmentti on kullekin esiintymälle omansa.

Kuitenkin jos samaa aliohjelmaa käytetään useammassa eri ohjelmassa ja ohjelmat käännetään ja linkitetään normaalisti, tulee aliohjelma muistiin uudestaan jokaista ohjelmaa kohti. Tätä sanotaan staattiseksi linkitykseksi.

Vastakohtana voidaan käyttää dynaamista linkitystä (DDL, Dynamic Link Libraries). Aliohjelmakirjasto on talletettu .DLL -tiedostoon, joka ladataan muistiin joko ohjelman käynnistyessä tai vasta kun tiettyä aliohjelmaa tarvitaan. Jos toinen ohjelma tarvitsee samaa kirjastoa ja se jo on muistissa, ei kirjastoa enää ladata uudelleen. Siis kustakin kirjastosta on korkeintaan vain yksi esiintymä muistissa.

5.9.1 Dynaaminen/staattinen kirjasto

Voimme vertailla dynaamisen kirjaston etuja ja haittoja staattiseen kirjastoon verrattuna:

+ yleisillä toiminnoilla pienempi tilankulutus muistissa

+ pienemmät .EXE -tiedostot => nopeampi käynnistys jos kirjastoon ei heti viitata

+ kirjaston virheiden korjaus ei vaadi itse ohjelman uudelleenkääntämistä

+ samoja aliohjelmia voidaan käyttää eri kielistä (esim. Pascal, C, C++, Visual Basic, AutoCad, MS-Word Basic...)

- jos kirjastosta käytetään vain muutamaa aliohjelmaa, on loppuosa kirjastosta turhaan muistissa

- ohjelmoiminen hieman tavallista kirjastoa hankalampaa (ei haittaavasti)

- käyttö hieman (tuskin merkittävästi) hitaampaa

- kun ohjelmaa levitetään, pitää myös kirjasto muistaa antaa mukana

- kirjasto pitää muistaa laittaa polussa olevaan hakemistoon (normaalijärjestelyissä)

Huomaa että edellä miinukset ovat lyhyitä!

5.9.2 Miten tehdään DLL-aliohjelma?

Tehdään aluksi esimerkin vuoksi .DLL -tiedosto, jossa on aliohjelma karkausvuoden tarkistamiseksi.

1.
Kirjoita ja testaa aliohjelma normaalisti. Esimerkiksi:
	int karkausvuosi(int vuosi)
	{
	  if ( vuosi % 100 ) return ( vuosi % 4 ) == 0;
	  return ( vuosi % 400 ) == 0;
	}
2.
Kun aliohjelma on testattu, lisää sen tyypiksi WINAPI _export:
	int WINAPI _export karkausvuosi(int vuosi)
3.
Lisää ympärille "pääohjelma":
	/* pvmdll.c   */
	#include <windowsx.h>
	#include "pvmlib.h"
	
	/****************************************************************************/
	int WINAPI _export karkausvuosi(int vuosi)
	{
	  if ( vuosi % 100 ) return ( vuosi % 4 ) == 0;
	  return ( vuosi % 400 ) == 0;
	}
	
	/***************************************************************************/
	/* Siirrettävyys aliohjelmat, jotta asioita ei tarvitse kirjoittaa moneen  */
	/* paikkaan Win 3.1:stä ja Win32:sta varten erikseen.                      */
	/***************************************************************************/
	static int DLL_lib_init(HINSTANCE hInstance)
	{
	  DLL_G_hInstance = hInstance;
	  return 1;
	}
	
	/***************************************************************************/
	static int DLL_lib_end(void)
	{
	  return 1;
	}
	
	/***************************************************************************/
	/*16*16*16*16*16*16*16*16*16*16*16*16*16*16*16*16*16*16*16*16*16*16*16*16***/
	#ifndef __WIN32__
	
	int WINAPI _export WEP(int bSystemExit)
	{
	  return DLL_lib_end();
	}
	
	int WINAPI LibMain(HINSTANCE hInstance, WORD wDataSeg, WORD wHeapSize,
	                       LPSTR lpCmdLine)
	{
	  if ( wHeapSize > 0 ) UnlockData(0);
	  return DLL_lib_init(hInstance);
	}
	
	/***************************************************************************/
	/*32*32*32*32*32*32*32*32*32*32*32*32*32*32*32*32*32*32*32*32*32*32*32*32***/
	#else /* 32-bit */
	
	BOOL WINAPI DllEntryPoint(HINSTANCE hinstDll,DWORD fdwReason,LPVOID plvReserved)
	{
	#pragma argsused /* fdwReason = 0 lopetuksessa ja 1 aloituksessa */
	  switch( fdwReason ) {
	    case 1: return DLL_lib_init(hinstDll);
	    case 0: return DLL_lib_end();
	  }
	  return 1;
	}
	
	#endif /* 32-bit */
4.
Kirjoita PVMDLL.DEF -tiedosto:
	LIBRARY       PVMDLL          ; Kirjaston nimi.  Huom! LIBRARY, ei NAME
	DESCRIPTION  'Pvm-aliohjelemia'
	EXETYPE       WINDOWS
	PROTMODE
	CODE  PRELOAD MOVEABLE DISCARDABLE
	DATA  PRELOAD MOVEABLE SINGLE ; HUOM! SINGLE
	HEAPSIZE      1024            ; HUOM!  Ei pinoa!
5.
Valitse projektissa kohteeksi Windows DLL.
6.
Jos kääntäjä ei Build -komennolla tee automaattisesti PVMDLL.LIB -tiedostoa, niin tee se vaikkapa DOSissa komennolla:
	D:\KURSSIT\WINOHJ\DLL\SOURCE>IMPLIB pvmdll.lib pvmdll.dll
7.
Tee pvmlib.h, jossa esittelet aliohjelman:
	int WINAPI karkausvuosi(int vuosi);
C.
Kutsuvassa C-ohjelmassa laita #include "pvmlib.h" ja lisää projektiin pvmdll.lib.
VB.
Jos haluat käyttää aliohjelmaa esim. Visual Basicista, kirjoita seuraava rivi lomakkeen yleisiin tietoihin tai projektin yleisiin aliohjelmiin:
	Declare Function karkausvuosi Lib "PvmDLL.DLL" (ByVal vuosi As Integer) As Integer
BP.
Borlandin Pascalissa (Delphi) kirjoita määritys:
	function karkausvuosi(vuosi:integer):integer; far ; external 'pvmdll';
	{ missä 'pvmdll' on .dll tiedoston nimi, joka on polussa tai olet. hakemistossa }

Käytännössä yksinkertaisissa tapauksissa sama pääohjelma kelpaa "kaikille" -DLL -aliohjelmille, joten tiedostosta ALI\DLLMAIN.C löytyy valmis "pääohjelma", joka vain linkitetään mukaan omaan aliohjelmaan.

Kaikki sellaiset aliohjelmat, jotka eivät käytä globaaleja tai static -muuttujia, voidaan helposti muuttaa DLL - aliohjelmiksi. Teemme seuraavaksi aliohjelman, joka tarvitsee sisäisiä static muuttujia.

5.9.3 Merkkijonon luku (ALI\STRLIB.C)

Hyvin usein tarvitaan aliohjelmaa, joka kysyy käyttäjältä yhden merkkijonon. Koska tällaista ei ole Windowsissa valmiina teemme aliohjelman

	int WINAPI_LIB KysyJono(LPSTR kysymys, LPSTR oletus,
	                         LPSTR jono, int max_pit)

joka palauttaa dialogiin kirjoitetun merkkijonon pituuden tai -1, jos dialogi peruttiin. Tätä varten täytyy tietysti tehdä sopiva dialogi ja lisäksi Modal-dialogia ylläpitävä ikkunafunktio. Edellä WINAPI_LIB tulee riippumaan siitä, tuleeko kirjastosta DLL-versio vaiko tavallinen kirjasto.

Erääksi ongelmaksi tällaisessa kirjastossa tulee se, miten ikkunafunktiolle välitetään KysyJono -funktiolle tulleet parametrit. Eräs mahdollisuus on globaalit muuttujat. Siis pakataan kaikki parametrit yhteen tietueeseen:

	typedef struct { /* Globaalien työmuuttujien tyyppi                        */
	  LPSTR jono;
	  LPSTR oletus;
	  LPSTR kysymys;
	  int   max_pit;
	  HWND  hwnd;      /* Ikkuna joko "omistaa" muuttujat                      */      
	} StrGlobals;

Nyt seuraava ongelma on se, että aliohjelmakutsuja saattaa olla käynnissä useitakin samalla kertaa. Tämän takiahan emme voineet yksinkertaisessa Vaihtaja-ohjelmassa sallia useampaa vaihtajaa yhtäaikaisesti. Erityisesti useita kutsuja voi olla lopullisessa DLL-versiossa, mutta myös tavallisenkin kirjaston tapauksessa moni-ikkunaisessa ohjelmassa useammasta ikkunasta voi olla merkkijonon luku kesken! Siis ei auta muu kuin tehdä jonkinlainen taulukko käytössä olevista globaaleista tietueista ja tunnistaa sitten ikkunan kahvan perusteella se, kenen muuttujia nyt kuuluu käyttää. Tarvittavat aliohjelmat ovat:
	/***************************************************************************/
	StrGlobals *GetG(HWND hwnd,int child)
	/* Palauttaa sen globaalin osoitteen, jonka hwnd omistaa.
	** Jos child-lippu päällä, otetaan hwnd:ksi isän hwnd
	** Jos hwnd:tä ei vielä ole allokoitu, tehdään sille uusi.
	** Virhetilanteessa palautetaan joukon 1. (indeksi 0)
	*/
	{
	  int i;
	  HWND hwndp=GetParent(hwnd);
	  if ( child ) hwnd = hwndp;
	  for ( i = 1; i < Gnum; i++ )
	    if ( Globals[i].hwnd == hwnd ) return &Globals[i];
	
	  if ( Gnum >= MAX_GLOBALS ) return &Globals[0];
	
	  Globals[Gnum].hwnd = hwnd;
	  return &Globals[Gnum++];
	}
	
	/***************************************************************************/
	void ReleaseG(HWND hwnd)
	/* Poistetaan se globaali, jonka hwnd omistaa.  Jos hwnd:tä
	** ei ole allokoitu, niin ei poisteta mitään!
	*/
	{
	  int i,j;
	  for ( i = 1; i < Gnum; i++ )
	    if ( Globals[i].hwnd == hwnd ) { /* Jos löytyi, niin poistetaan        */
	      for ( j = Gnum-1; j > i; j--)
	        Globals[j-1] = Globals[j];
	      Gnum--;
	      return;
	    }
	}

Nyt voidaan kirjoittaa itse KysyJono -aliohjelma:

	/***************************************************************************/
	int CALLBACK_DLL KysyJono(LPSTR kysymys, LPSTR oletus,
	                         LPSTR jono, int max_pit)
	{
	  int ret;
	  HWND hwnd = GetFocus();
	  StrGlobals *G = GetG(hwnd,0);
	  G->oletus  = oletus;
	  G->kysymys = kysymys;
	  G->jono    = jono;
	  G->max_pit = max_pit;
	  ret=DoModalDialog(hwnd,"KYSYJONO",KysyJonoDialog);
	  ReleaseG(hwnd);
	  return ret;
	}

DoModalDialog tarvitsee esiintymän kahvaa. Tämä pidetään globaalissa muuttujassa DLLG_hInstance, joka DLL-versiossa otetaan DLL-pääohjelmasta (LibMain) ja tavallisessa kirjastossa ikkunan käynnistäneestä ohjelmasta.

Jonoa kysyvän dialogin ikkunafunktio olisi esimerkiksi seuraava:

	/***************************************************************************/
	BOOL CALLBACK _export KysyJonoDialog(HWND hDlg,unsigned message,
	                                     WPARAM wParam,LPARAM lParam)
	{
	#pragma argsused
	  StrGlobals *G;
	  switch (message) {
	    case WM_INITDIALOG:
	      G = GetG(hDlg,1);
	      SetWindowText(hDlg,G->kysymys);
	      SetDlgItemText(hDlg,EDITJONO,G->oletus);
	      MoveAskDialog(hDlg);
	      return (TRUE);
	
	    case WM_COMMAND:
	      switch ( GET_WM_COMMAND_ID(wParam,lParam) ) {
	        case IDOK:
	          G = GetG(hDlg,1);
	          GetDlgItemText(hDlg,EDITJONO,G->jono,G->max_pit);
	          EndDialog(hDlg,lstrlen(G->jono));
	          return (TRUE);
	        case IDCANCEL:
	          EndDialog(hDlg,-1);
	          return (TRUE);
	      }
	      break;
	  }
	  return (FALSE);
	}

Nyt aliohjelmaa voitaisiin kokeeksi kutsua vaikkapa "Hello World" -versiossa, jossa hiiren vasemman näppäimen painaminen aiheuttaisi uuden jonon kysymisen (DLL\SOURCE\CTEST\STRTEST.C):

	#include <windowsx.h>
	#include <string.h>
	#include "tabhand.h"
	#include "strlib.h"
	
	/***************************************************************************/
	TblClassSWindowMAIN("WKysyjonoWClass",NULL,"Kysyjono-testi",MsgTbl,0);
	/***************************************************************************/
	
	static char jono[50];
	
	static EVENT WM_lbuttondown(tMSGParam *msg)
	{
	  KysyJono("Tiedoston nimi?",jono,jono,sizeof(jono));
	  InvalidateRect(msg->hWnd,NULL,TRUE);
	  return 0;
	}
	
	static EVENT WM_paint(tMSGParam *msg) /* # MAKE_DC # */
	{
	  TextOut(msg->hDC, 10, 10, jono,strlen(jono));
	  return 0;
	}
	
	static EVENT WM_create(tMSGParam *msg)
	{
	  GetModuleFileName(GetWindowInstance(msg->hWnd),jono,sizeof(jono));
	  return 0;
	}
	...
	

Tällöin projektissa on mukana

	STRTEST.C
	STRTEST.DEF
	ALI\STRLIB.C,
	ALI\STRLIB.RC
	ALI\TABHAND.C

5.9.4 Aliohjelmakirjaston .DLL -versio

Jos kirjastosta halutaan tehdä .DLL -versio, pitää kunkin kutsuttavan aliohjelman olla _export-tyyppiä. Edellä teimme tämän jo valmiiksi, sillä WINAPI_LIB muuttuu automaattisesti oikeaksi (STRLIB.H, DLLMAIN.H):

Header-tiedostossa on otettu huomioon myös se mahdollisuus, että kirjasto käännettäisiin C-kääntäjällä ja kutsut tehtäisiin C++ -ohjelmasta. Tällöinhän nimet eivät automaattisesti mene oikein.

Seuraavaksi .DLL -kirjastolle pitää tehdä "pääohjelma", joksi kelpaa DLLMAIN.C. Siis linkitetään DLL-käännöksessä (vaihda kohteeksi DLL):

	STRDLL.DEF
	ALI\DLLMAIN.C
	ALI\STRLIB.C
	ALI\STRLIB.RC

Sitten vaan kääntämään ja tulokseksi syntyy STRDLL.DLL -niminen .DLL -kirjasto.

5.9.5 DLL-kirjaston käyttäminen

Kirjaston käyttämiseksi kelpaa edellä tehty pää(testi)ohjelma sellaisenaan. Se pitää vaan linkittää hieman eri tavalla.

Projektiin laitetaan nyt mukaan STRDLL.LIB, jossa on vain tieto aliohjelman olemassaolosta, ei itse aliohjelmaa kuten normaaleissa .LIB- tiedostoissa. Ilman tätä linkittäjä ei löydä ko. aliohjelmia (koska linkitysvaiheessa DLL-aliohjelmia ei tarvitse olla edes olemassa!) ja valittaa niiden puutteesta. Samalla linkittäjä osaa lisätä ohjelmaan koodin, jolla .DLL-kirjasto haetaan muistiin. Testiohjelman projektissa on siis

	TESTDLL.C
	TESTDLL.DEF
	STRDLL.LIB

Aluksi on tietysti muistettava vaihtaa käännöksen tulos takaisin Windows App -muotoon edellisen .DLL -käännöksen jäljiltä.

Ohjelman (STRTEST.EXE) ajamista varten DLL-kirjaston (STRDLL.DLL) tulee olla joko oletushakemistossa tai jossakin polussa (PATH) mainitussa hakemistossa.

Jos kirjaston toiminnassa on jotakin vikaa, ei testiohjelmaa enää tarvitse uudelleen kääntää, vaan riittää korjata pelkkää kirjastoa.

Eihän se DLL niin vaikeaa ollutkaan! Mutta silti useat tämän monisteen aliohjelmakirjastot eivät sellaisenaan sovellu DLL:ksi koska niissä ei ole yksinkertaisuussyistä edellä mainittua globaalien muuttujien käsittelyä!

5.10 Yleiskäyttöinen optiodialogi (ALI\OPTDLG.C)

Eräs yleinen tilanne normaalissa ohjelmassa on sellainen, että käyttäjältä pitää kysyä tiettyjen tomintaparametrien (optioiden) arvoa. Tällainen vakiomuotoinen ongelma voidaan ratkaista siten, että tehdään yleiskäyttöinen optioidialogin käsittely aliohjelmisto. Mallina ALI\OPTDLG.C on Sirpa Pasasen tekemä versio, jossa ohjelmoijan tarvitsee vain

1. suunnitella optiodialogi resurssieditorilla

1. tehtävä tietue, johon dialogin arvot tulevat

1. tehtävä taulukko, jossa yhdistetään dialogin kenttien ID-numerot tietueen alkioiden paikkoihin

1. kutsuttava dialogin käsittelurutiinia

Tarvittaessa aliohjelmat kirjoittavat optiot talteen myös .INI-tiedostoon. Esitetty malliratkaisu ei tietenkään toimi kaikissa monimutkaisissa tilanteissa, missä dialogin ulkonäön pitää muuttua valintojen mukaan, mutta se on jälleen hyvä esimerkki siitä, mitä Windowsissa olisi pitänut olla jo valmiina!


previous next Title Contents