* 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).
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.
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.
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);
HINSTNACE hInstance = GetWindowInstance(hWnd );
On olemassa muitakin tapoja, mutta tätä on syytä käyttää yhteensopivuuden säilyttämiseksi.
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ä!).
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.
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.
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)
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.
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ää.
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ä?).
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!
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]).
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.
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ä.
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ä.
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.
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; }
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ä.
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!
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.
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).
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.
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.
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.
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.
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.
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 */
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.
+ 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ä!
int karkausvuosi(int vuosi) { if ( vuosi % 100 ) return ( vuosi % 4 ) == 0; return ( vuosi % 400 ) == 0; }
int WINAPI _export karkausvuosi(int vuosi)
/* 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 */
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!
D:\KURSSIT\WINOHJ\DLL\SOURCE>IMPLIB pvmdll.lib pvmdll.dll
int WINAPI karkausvuosi(int vuosi);
Declare Function karkausvuosi Lib "PvmDLL.DLL" (ByVal vuosi As Integer) As Integer
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.
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
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.
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ä!
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!