* valmiit ikkunaluokat: Edit, Button, Static, ScrollBar, List- ja ComboBox
* Windows-ohjelman debuggaus: viestien "vakoilu", SPY, WinSight
* resurssitiedosto .RC: työkalut tekemiseen
* valmiit dialogit: tiedoston avaus, värin valinta, fontin valinta, kirjoittimen valinta
* leikekirjan käyttö
* dynaamisen muistin käyttö
* bittikartat ja ikonit: staattiset ja liikkuvat
* dialogi pääikkunana
Windows tarjoaa paljon valmiita ratkaisuja yleisiin perustoimintoihin. Esimerkiksi API-funktioiden dokumentointia on 3 kirjaa jo peruspaketissa. Vaikka edellisessä luvussa piirsimmekin näyttöön, on tällainen piirtäminen ehkä oikeassa Windows-ohjelmassa vähemmällä merkityksellä, koska näyttö koostuu nappuloista, ikoneista, menuista jne., joiden piirtämisen Windows hoitaa itse.
/**************************************************************************** PROGRAM: Thello.c PURPOSE: Ohjelma tyypillisten kontrollien testaamiseen. Editor: Vesa Lappalainen typistänyt malliohjelmista. Project: thello.c, whello.def ****************************************************************************/ #include <windows.h> /* Tarvitaan kaikissa Windows C-ohjelmissa */ #include <windowsx.h> #define MY_BUTTON 175 /* Nappulan tunniste */ #define MY_EDIT 176 /* Tekstikentän (Edit) tunniste */ #define MY_BAR 177 /* Oman likusäätimen tunniste */ #define MY_STATIC 178 /* 1. vakiotekstin tunniste = liun arvo */ #define MY_BARS 179 /* apuvakio seuraaville */ #define MY_SVER (MY_BARS+SB_VERT) /* 2. tekstikenän tunniste = pystyliun */ #define MY_SHOR (MY_BARS+SB_HORZ) /* 3. tekstikentän tunniste = vaakaliun*/ #define MIN_BAR 0 #define MAX_BAR 50 static int value = 0; LONG CALLBACK MainWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { PAINTSTRUCT ps; switch (message) { case WM_PAINT: /* Viesti: Piirrä ikkuna uudelleen */ if ( BeginPaint(hWnd,&ps) ) TextOut(ps.hdc, 10, 10, "Hello World!",12); EndPaint(hWnd,&ps); return NULL; case WM_COMMAND: switch ( GET_WM_COMMAND_ID(wParam,lParam) ) { case MY_BUTTON: /* Painonappia painettu */ SetDlgItemInt(hWnd,MY_STATIC,++value,TRUE); return NULL; case MY_EDIT: /* Jos viesti teksti-ikkunalta */ switch ( GET_WM_COMMAND_CMD(wParam,lParam) ) { case EN_CHANGE: /* Aina kun sisältö muuttuu, lisät.lask */ SetDlgItemInt(hWnd,MY_STATIC,++value,TRUE); return NULL; case EN_KILLFOCUS: {/* Teksti-ikkunasta poistuttu */ char svalue[20]; GetDlgItemText(hWnd,MY_EDIT,svalue,sizeof(svalue)); SetWindowText(hWnd,svalue); /* Sama tulos pääótsikkoon */ return NULL; } } return NULL; default: break; } break; case WM_VSCROLL: { /* Johonkin pysytliukuun koskettu */ HWND shWnd = GET_WM_VSCROLL_HWND(wParam,lParam); int fnBar = SB_CTL; int id = GetDlgCtrlID(shWnd); if ( shWnd == NULL ) { shWnd = hWnd; id = SB_VERT; fnBar = SB_VERT; } switch ( GET_WM_VSCROLL_CODE(wParam,lParam) ) { case SB_BOTTOM : value = MIN_BAR; break; case SB_TOP : value = MAX_BAR; break; case SB_LINEDOWN : value++; break; case SB_PAGEDOWN : value += 10; break; case SB_LINEUP : value--; break; case SB_PAGEUP : value -= 10; break; case SB_THUMBPOSITION: case SB_THUMBTRACK : value = GET_WM_VSCROLL_POS(wParam,lParam); break; } SetScrollPos(shWnd,fnBar,value,TRUE); SetDlgItemInt(hWnd,id == MY_BAR ? MY_STATIC : MY_SVER ,value, TRUE); return NULL; } case WM_DESTROY: /* Viesti: ikkuna hävitetään */ PostQuitMessage(0); return NULL; default: /* Antaa Windowsin käsitellä muut */ break; } return DefWindowProc(hWnd, message, wParam, lParam); } int PASCAL WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) { HWND hWnd,hCtrl;/* Pääikkunan kahva ja apukahva kontrollien luontiin. */ WNDCLASS wc; /* Ikkunaluokka */ MSG msg; /* Viesti */ (void)lpCmdLine; /* Hämäystä, jottei valitusta param. käytt. */ if (!hPrevInstance) { /* Onko muita esiintymiä käynnisssä? */ wc.style = NULL; wc.lpfnWndProc = MainWndProc; wc.cbClsExtra = 0; wc.cbWndExtra = 0; wc.hInstance = hInstance; wc.hIcon = LoadIcon(NULL, IDI_APPLICATION); wc.hCursor = LoadCursor(NULL, IDC_ARROW); wc.hbrBackground = GetStockObject(WHITE_BRUSH); wc.lpszMenuName = NULL; wc.lpszClassName = "WHelloWClass"; if ( !RegisterClass(&wc) ) return 1; } hWnd = CreateWindow("WHelloWClass","Windows Hello", WS_CAPTION | WS_BORDER | WS_VSCROLL | WS_HSCROLL | WS_SYSMENU | WS_THICKFRAME | WS_MINIMIZEBOX | WS_MAXIMIZEBOX, 50,50,300,300,NULL,NULL,hInstance,NULL); if ( !hWnd ) return 1; ShowWindow(hWnd,nCmdShow); /* Näytetään ikkuna */ hCtrl = CreateWindow("Button","Windows Hello", WS_BORDER |WS_CHILD | WS_VISIBLE , 10,30,100,30,hWnd,(HMENU)MY_BUTTON,hInstance,NULL); if ( !hCtrl ) return 1; hCtrl = CreateWindow("Static","0",WS_CHILD | WS_VISIBLE | SS_RIGHT, 40,70,40,30,hWnd,(HMENU)MY_STATIC,hInstance,NULL); if ( !hCtrl ) return 1; hCtrl = CreateWindow("Static","0",WS_CHILD | WS_VISIBLE | SS_RIGHT , 80,70,40,30,hWnd,(HMENU)MY_SHOR,hInstance,NULL); if ( !hCtrl ) return 1; hCtrl = CreateWindow("Static","0",WS_CHILD | WS_VISIBLE | SS_RIGHT, 120,70,40,30,hWnd,(HMENU)MY_SVER,hInstance,NULL); if ( !hCtrl ) return 1; hCtrl = CreateWindow("Edit","Windows Hello", WS_DLGFRAME | WS_CHILD | WS_VISIBLE, 10,110,100,40,hWnd,(HMENU)MY_EDIT,hInstance,NULL); if ( !hCtrl ) return 1; hCtrl = CreateWindow("Scrollbar","Windows Hello", WS_CHILD | WS_VISIBLE | SBS_VERT, 200,10,30,200,hWnd,(HMENU)MY_BAR,hInstance,NULL); if ( !hCtrl ) return 1; SetScrollRange(hCtrl,SB_CTL,MIN_BAR,MAX_BAR,TRUE); SetScrollRange(hWnd,SB_VERT,MIN_BAR,2*MAX_BAR,TRUE); SetScrollRange(hWnd,SB_HORZ,MIN_BAR,3*MAX_BAR,TRUE); while ( GetMessage(&msg,NULL,NULL,NULL) ) { TranslateMessage(&msg); /* Tulkitaan virtuaaliset näp. koodit */ DispatchMessage(&msg); /* Lähetetään viesti ikkunalle */ } return msg.wParam; /* Palautetaan PostQuitMessage-funktion arvo */ }
Pääohjelmassa on aluksi luotu joukko erityyppisiä ikkunoita. Kullekin ikkunalle pitää määritellä oma tunnistin (esim. MY_BUTTON), jolla ikkunan toiminnan tapahtuminen tunnistetaan. Ikkunalta saadaan isä-ikkunalle WM_COMMAND -viesti, kun ikkunassa tapahtuu jotakin, joka vaatii viestin lähettämistä. Ikkunan tunniste saadaan sitten 16-bittisessä Windowsissa wParam -parametrissa ja mahdollinen lisätieto lParam -parametrin yläosassa (HIWORD). Parametrin alaosassa (LOWORD on ikkunan kahva).
32-bittisessä Windowsissa myös wParam on 32-bittinen ja ikkunoiden kahvat (HWND) ovat 32-bittisiä. Näin ollen koko lParam pitää varata ikkunan kahvalle, viestin lisätieto on siirretty wParam yläosaan ja ikkunan (kontrollin) tunniste on wParamin alaosassa. Seuraavassa wParam ja lParam molemmissa Windowseissa, kun esimerkiksi on muutettu editointikentän sisältöä (MY_EDIT), joka on luonnissa saanut tunnisteekseen vaikkapa 402C (MY_EDIT = 00B016 = 17610, EN_CHANGE = 030016):
Windows
wParam
lParam
Win
3.1
00B0
0300
402C
WIN32
0300
00B0
0000
402C
Jos haluamme ohjelman toimivan kummassakin ympäristössä, on parasta käyttää WINDOWSX.H:n makroja. Esimerkiksi viestin aiheuttaneen kontrollin tunnus, viestin lisätieto ja kontrollin ikkunakahva selvitetään makroilla
id = GET_WM_COMMAND_ID(wParam,lParam); cmd = GET_WM_COMMAND_CMD(wParam,lParam); chWnd = GET_WM_COMMAND_HWND(wParam,lParam);
Huom! Makroja ei löydy vanhoista (esim. Borland C++ 3.1) WINDOWSX.H:sta. Tällöin pitää etsiä tilalle uudempi versio.
Kontrolleista poikkeuksen tekee liukusäädin, joka lähettää oman WM_VSCROLL tai WM_HSCROLL -viestin. Windows 3.1:ssä lParam -parametrin yläosassa on jälleen säätimen kahva (NULL, jos ikkunan liuku), alaosassa säätimen asento ja wParam -parametrissa säätimen tapahtuman tyyppi. Arvojen ottaminen on kuitenkin jälleen siirrettävyyden säilyttämiseksi tehtävä makroilla. Viestin parametrien perusteella pitää sitten toteuttaa säätimen aiheuttamat muutokset ruudulla. Liukusäätimen rajat asetetaan SetScrollRange -funktiolla.
Esimerkiksi EDIT-ikkunasta saadaan viesti aina, kun ikkunassa tapahtuu jotakin muutoksia. Esimerkissä muutokset lasketaan lukumääränä STATIC-ikkunaan, jonka arvoa voidaan muuttaa myös liukusäätimellä tai nappulan painamisella. Kun EDIT-ikkunasta poistutaan, kopioidaan EDIT-ikkunan sisältö pääikkunan otsikoksi. Jos EDIT-ikkunan määrittelyssä olisi annettu bitti ES_PASSWORD, olisi EDIT-ikkunaan tulostunut pelkkiä tähtiä.
Jos tarvitaan monirivistä EDIT-ikkunaa, voidaan tämä tehdä EDIT-ikkunasta ES_MULTILINE -määrityksellä. Ikkunaan laitettavaan tekstiin pitää tällöin muistaa laittaa rivinvaihtojen kohdalle \r\n, pelkkä \n ei riitä. Moniriviseen EDIT-ikkunaan saadaan myös automaattinen rullaus.
hCtrl = CreateWindow("Edit","Windows\r\n Hello", 0* WS_CAPTION | 0* WS_BORDER | 1* WS_DLGFRAME | 1* WS_VSCROLL | 0* WS_HSCROLL | 0* WS_SYSMENU | 1* WS_THICKFRAME | 0* WS_MINIMIZEBOX| 0* WS_MAXIMIZEBOX | 0* WS_POPUP | 1* WS_CHILD | 1* WS_VISIBLE | 0* ES_PASSWORD | 1* ES_MULTILINE | 1* ES_UPPERCASE | 0* ES_AUTOHSCROLL| 1* ES_AUTOVSCROLL, 10,160,100,80,msg->hWnd,(HMENU)MY_MEDIT,hInstance,NULL);
Itse asiassa alkeellinen editori (kuten esim. NOTEPAD) saadaan aikaan yhdellä monirivisellä EDIT-ikkunalla.
Joskus EDIT-ikkunan muisti halutaan omaan muistilohkoon ja tällöin EDIT-ikkuna pitää muistaa luoda DS_LOCALEDIT -lippua käyttäen. Tällöin EDIT-ikkunalle pitää myös ilmoittaa muistilohkon kahva. Etuna olisi esimerkiksi helpompi tiedoston käsittely, kun kaikki tieto on valmiiksi jo omassa muistilohkossa - tiedostoon voidaan kirjoittaa pelkkä yhden muistilohkon sisältö. Tosin ominaisuus on valittavissa vain dialogeissa luotuihin EDIT-ikkunoihin.
Jos monirivisessä EDIT-ikkunassa halutaan rivinvaihto pelkästään painamalla Return ja EDIT-ikkuna on luotu dialogi-resurssista, pitää luonnissa käyttää tyyliä ES_WANTRETURN (toimii vain Win 3.1 eteenpäin). Muuten rivinvaihtamiseksi joudutaan painamaan Ctrl-Return.
Ohjelmalla voidaan siis monitoroida Windowsin ikkunoita ja niille tulevia viestejä.
Esimerkissä monitoroinnin (=seurannan) kohteeksi on valittu ikkuna jonka kahva on 3D14 (THELLO-ohjelman isäikkuna). Voitaisiin valita myös useampia ikkunoita jos haluttaisiin. Message -kohdasta voitaisiin valita mitä viestejä tutkitaan. Esimerkissä on valittu kaikki viestit.
Aluksi kursori on siirretty EDIT-ikkunan päälle ja napautettu. Siirtäminen ja napauttaminen on aiheuttanut mm. pääikkunalle WM_SETCURSOR-viestin kursorin muodon vaihtamiseksi.. Tämän jälkeen on painettu BS -näppäintä, josta on tullut viesti aluksi vain EDIT-ikkunalle (402C)! Siis pääikkuna ei ole saanut tätä viestiä. Sitten EDIT-ikkuna on lähettänyt pääikkunalle viestin EN_UPDATE tiedoksi siitä, että sen sisältöä aiotaan muuttaa. Koska tätä viestiä ei ole käsitelty ikkunafunktiossamme, käytetään Windowsin oletuskäsittelijää, joka palauttaa EDIT-ikkunalle tiedon että "muuta vaan". Kun muutos on tehty, on pääikkunalle lähetty viesti EN_CHANGE, tiedoksi siitä, että nyt se sisältö sitten muuttui. Tämä viesti on käsitelty ikkunafunktiossamme ja siinähän pyydettiin STATIC-ikkunaa lisäämään arvoaan yhdellä.
Windowsissa onkin tapa esittää eri resursseja C-kielen ulkopuolisella resurssitiedostolla. Esimerkiksi autolaskuria vastaava dialog-laatikko saataisiin seuraavalla tiedostolla:
#include "laskurir.h" LASKURI DIALOG 17, 25, 180, 88 STYLE WS_OVERLAPPED | WS_VISIBLE | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX CAPTION "LASKURIR" { CONTROL "&Henkilöautoja", HA, "BUTTON", WS_CHILD | WS_VISIBLE | WS_TABSTOP, 33, 10, 50, 20 CONTROL "&Kuorma-autoja", KA, "BUTTON", WS_CHILD | WS_VISIBLE | WS_TABSTOP, 100, 10, 50, 20 CONTROL "0 ", HAL, "STATIC", SS_RIGHT | WS_CHILD | WS_VISIBLE | WS_BORDER | WS_GROUP, 31, 40, 50, 10 CONTROL "0 ", KAL, "STATIC", SS_RIGHT | WS_CHILD | WS_VISIBLE | WS_BORDER | WS_GROUP, 100, 40, 50, 10 CONTROL "&Nollaa", NOLLAA, "BUTTON", WS_CHILD | WS_VISIBLE | WS_TABSTOP, 50, 60, 80, 20 CONTROL "e&Xit", EXIT, "BUTTON", WS_CHILD | WS_VISIBLE | WS_TABSTOP, 0, 0, 24, 14 } PIKA ACCELERATORS BEGIN "K", KA "k", KA "H", HA "h", HA "N", NOLLAA "n", NOLLAA "X", EXIT "x", EXIT END
Dialogin yksikkö riippuu dialogissa käytetyn fonttityypin kirjaimen leveydestä ja korkeudesta siten, että yksikkönä on vaakasuuntaan 1/4 kirjaimen leveydestä ja pystysuuntaan 1/8 kirjaimen korkeudesta.
Kun ohjelma on käännetty ja kertaalleen linkitetty, liitetään vielä käännetty resurssitiedosto (.RES) ohjelmatiedostoon (.EXE). Tämä kaikki hoituu automaattisesti, mikäli myös resurssitiedosto (.RC) on lisätty projektiin kuuluvaksi.
HUOM 1! Jos resurssitiedosto tehdään esimerkiksi Borlandin resurssieditorilla, kannattaa tarkistaa että nappula on todella "BUTTON" tyyppiä eikä esim."BORBTN" -tyyppiä. "BOR" -alkuiset tyylit vaativat että ajon aikana on ladattuna BWCC.DLL -moduli. Jollei ole, niin ne dialogit, joissa on "BOR" -alkuisia tyylejä, eivät näy ruudussa!
HUOM 2! Tässä tapauksessa pikanäppäinten (Accelerators) määrittely on tarpeetonta, koska samat näppäimet toimivat joka tapauksessa alleviivauksen ansiosta!
#define LASKUREITA 2 #define HA 1000 #define KA HA+1 #define HAL 1010 #define KAL HAL+1 #define EXIT 1027 #define NOLLAA 1028
Vakioiden tarkoituksena on tietysti taas toimia tunnistimena nappuloille ja static-kentille.
/**************/ /* laskurir.c */ /**************************************************************************** ** PROGRAM: laskurir.c ** Windows: Win 3.1 & WIN32 ** PURPOSE: Autolaskuri tehtynä mahd. pitkälle Windowsin resursseilla. ** Editor: Vesa Lappalainen ** Projektiin tarvitaan ** laskurir.c - tämä tiedosto ** laskurir.def - moduulin määrittely ** laskurir.rc - resurssit ** ** Tehtäviä: 1) Lisää polkupyörien laskeminen (käytä .rc tiedoston ** korjailemiseen Resource WorkShopia) ** ** 2) Lisää +/- toiminto näppäinten avulla ** ** 3) Muuta .rc tiedostoa siten, että myös DEL-näppäin ** tyhjentää näytön. ** ****************************************************************************/ #include <windows.h> #include <windowsx.h> /* Tarvitaan 32-bit yhteensopivuudeksi */ #include "laskurir.h" static int laskurit[LASKUREITA] = {0,0}; BOOL CALLBACK _export MainWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { #pragma argsused switch (message) { case WM_COMMAND: { int id = GET_WM_COMMAND_ID(wParam,lParam); /* Jotta WIN32 toimisi */ switch ( id ) { case HA: /* Painonappia painettu */ case KA: SetDlgItemInt(hWnd,id-HA+HAL,++laskurit[id-HA],TRUE); return TRUE; case NOLLAA: { int i; for (i=0; i<LASKUREITA; i++) SetDlgItemInt(hWnd,i+HAL,laskurit[i] = 0,TRUE); return TRUE; } /* NOLLAA */ case IDCANCEL: case EXIT: PostQuitMessage(0); return TRUE; } /* switch (id) */ } /* WM_COMMAND */ } /* switch ( message ) */ return FALSE; } int PASCAL WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) { #pragma argsused MSG msg; FARPROC lProc = MakeProcInstance((FARPROC)MainWndProc,hInstance); HWND hWnd = CreateDialog(hInstance,"LASKURI",NULL,(DLGPROC)lProc); HACCEL hAccel = LoadAccelerators(hInstance,"PIKA"); if ( !hWnd ) goto lopetus; while (GetMessage(&msg,NULL,NULL,NULL)) { if ( TranslateAccelerator(hWnd,hAccel,&msg) ) continue; if ( IsDialogMessage(hWnd,&msg) ) continue; TranslateMessage(&msg); /* Tulkitaan virtuaaliset näp. koodit */ DispatchMessage(&msg); /* Lähetetään viesti ikkunalle */ } lopetus: (void)FreeProcInstance(lProc); /* void, jottei 32-bit kääntäjä valita */ return 0; }
Ikkunafunktion erona tavalliseen ikkunafunktioon on se, etteivät dialogin ikkunafunktiot saa kutsua oletuskäsittelijää, vaan niiden on palautettava TRUE, mikäli viesti käsiteltiin ja FALSE, mikäli viestiä ei käsitelty. Tämä on eräs inhottava piirre Windowsissa: eri ikkunat täytyy käsitellä eri tavoin. Ongelmaa on yritetty korjata ali\tabhand.h:n ja ali\mdialog.h:n makroilla (ks. winlaski\tablask\laskurir.c).
1. Käynnistetään resurssieditori.
2. Valitaan uusi projekti (ei ole sama asia kuin kääntäjän projekti) ja projektin tyypiksi .RC
3. h-tiedoston lisääminen: Lisätään projektiin kuuluva .h -tiedosto antamalla nimeksi LASKURIR.H sekä luomiseen vastataan Yes.
4. Parasta on heti antaa projektille nimi File, Save file as, ja tarkistaa myös että talletus tapahtuu oikeaan hakemistoon.
5. Dialogin tekeminen: Valitaan Resource, New ja tyypiksi Dialog. Dialogin tyypiksi valitaan vielä "Standard Window. No Buttons." Seuraavassa on myöhemmän vaiheen valmiiksi täytetty dialogi.
6. Näyttöön tulee tyhjä dialogi-laatikko. Dialogin nimi ja ominaisuudet voidaan vaihtaa näpäyttämällä dialogin otsikkoriviä 2 kertaa ja täyttämällä lomake seuraavasti:
HUOM! Huomaa erityisesti merkitä Visible -kohta, muuten esimerkiksi Modeless-dialogi ei näy ruudussa oletuksena.
7. Kun lomake on hyväksytty, voidaan dialogi venytellä sopivan kokoiseksi tai sitä voidaan venytellä myöhemmin.
8. Nappulan lisääminen: Näyttöön voidaan piirtää eXit-nappula valitsemalla oikeasta laidasta OK-nappulan näköinen työkalu ja siirtämällä se halutulle kohdalle näytössä. Nappulan koko voidaan muotoilla halutuksi. Nappulan tekstirivi voidaan muuttaa oikeassa laidassa olevassa Properties, Caption-kentässä tai näpäyttämällä nappulaa 2 kertaa. (Jos jokin kirjain halutaan alleviivatuksi, pitää sitä edeltää &-merkki, siis e&Xit).
9. Näpäytetään nappulaa 2 kertaa ja saadaan esille lomake, johon voidaan täyttää näppäimen ominaisuuksia.
10. Kirjoitetaan Control ID -kenttään näppäimen tunniste, joksi voidaan valita vaikkapa EXIT . (Jos täytetään Properties kohtaa, pitää muutoksia tehdä kohtiin IdSource ja IdValue).
11. Kun lomake hyväksytään, on tunniste EXIT tuntematon, jolloin sille annetaan jokin uusi arvo. Jos arvoa halutaan muuttaa, voidaan se tehdä menun Resource, Identifieres -kohdasta (tai Properties, IdValue). Tunnisteen arvoksi annetaan haluttu kokonaisluku, vaikkapa 1027 ja tarkistetaan onko tiedostona varmasti oikea .h tiedosto.
12. Vastaavasti lisätään kaikki muut nappulat ja keksitään kullekin hyvä kuvaava tunniste (Control ID) ja sopiva numero (ohjelma kyllä myös ehdottelee numeroita, mutta C-ohjelman kannalta on hyödyllistä saada Henkilöauto ja Kuorma-auto näppäinten tunniste peräkkäisiksi, samoin vastaavien näyttöjen). Kannattaakin ehkä tehdä tarvittavat tunnisteet menun Resource, Identifieres-kohdasta, jolloin voidaan esimerkiksi määrittää KA:lle arvo HA+1.
Huomaa, että kontrolleja (nappuloita jne.) voi laittaa samaan linjaan Alignment-työkalujen avulla. Saman kokoisiksi kontrolleja voidaan tehdä kohdan Align, Size -avulla. Myös valmiin kontrollin kopioiminen on hyvä idea! Hiiren oikealla näppäimellä saadaan myös Dublicate- toiminto, jolla on mukava tehdä esimerkiksi laskukoneen näppäimistö.
13. Tekstikentän lisääminen: Näytöt lisätään STATIC-kenttinä valitsemalla työkaluksi iso T-kirjain. Muotoillaan näyttö sopivan kokoiseksi ja käydään muuttamassa näytön tekstiksi "0 " (huom. ainakin yksi välilyönti loppuun jos haluat näytöstä siistimmän näköisen). Samalla laitetaan teksti oikeaan reunaan ja pyydetään tekstille raamit:
14. Kun dialogi on valmis, suljetaan dialogi-ikkuna koko dialogieditorin vasemmasta yläkulmasta (EI Resource Workshopin, parasta ehkä painaa Ctrl-F4, niin ei tule vahinkoa).
15. Resurssin nimen vaihto: Dialogin nimeksi tuli oletuksena DIALOG_1. Tämä voidaan vaihtaa siirtymällä ensin projekti-ikkunassa nimen päälle ja valitsemalla sitten: Resource, Rename (tai painetaan hiiren oikeaa näppäintä) ja kirjoitetaan uusi nimi LASKURI. Nyt kysymykseen "Create new identifier?" vastataan NO, mikäli halutaan ohjelmakoodissa käyttää nimeä merkkijonona kuten malliohjelmissa on tehty. Jos vastataan Yes, voi käydä helposti niin, että dialogi ei näy ruudussa. Jos vahingossa kuitenkin vastattiin Yes, voidaan myöhemmin poistaa tunniste laskuri LASKURIR.H-tiedostosta.
16. Pikanäppäimet: Nyt näppäimiltä saadaan viestit kun painetaan alleviivattuja kirjaimia Alt-näppäimen kanssa. Viestit pitäisi saada myös K, H, N ja X -näppäimistä. Näin tapahtuu dialogin ansiosta automaattisesti, koska ko. näppäimille ei missään kohdassa ole muuta käyttöä. Esimerkin vuoksi luomme kuitenkin vielä pikanäppäin-resurssin (Accelerator): (Resource, New, Accelerator).
17. Taulukkoon Command -kohtaan täytetään mikä viesti pitäisi saada näppäintä painamalla. Vaikka x näppäintä varten pitäisi saada viesti EXIT. Siis kirjoitetaan Command-kohtaan EXIT.
18. Siirrytään TAB-näppäimellä kenttään Key. Nyt painetaan sitä näppäinyhdistelmää, jolla viesti halutaan. Tässä tapauksessa painetaan pelkkä näppäintä x. Näppäimen valinta lopetetaan painamalla ESC. Lopuksi vielä kuitataan tehdyt toimenpiteet Return -näppäimellä.
19. Samalla tavalla täytetään kaikki muutkin "pikanäppäimet". Muista laittaa myös vastaavat isot kirjaimet.
20. Nimetään ACCELERATORS_1 resurssi nimelle PIKA vastaavasti kuin dialogi-resurssin nimen vaihdossa. (Muista vastata NO!)
21. Talletus: Lopuksi valitaan File, Save Project. Tosin talletus voidaan tehdä työn aikana varalta useitakin kertoja.
22. Nyt meillä on tiedostot LASKURIR.RC ja LASKURIR.H ja voidaan kirjoittaa tarvittava C-ohjelma.
Myöhemmin valmiita tiedostoja voidaan editoida joko editorilla tai jälleen Resource Workshopilla.
Täytyy kuitenkin olla tarkkana, ettei sama tiedosto ole yhtäaikaa avoinna kahdessa eri ohjelmassa! Jos tiedosto on avoinna esimerkiksi Resource Workshopissa ja C-editorissa, käyttää kääntäjä C-editorin tiedostoa eikä tällöin RWS:llä tehdyt muutokset tunnu vaikuttavan!
Menut voidaan kirjoittaa joko tekstieditorilla tai jollakin resurssieditorilla. Resurssieditorin hyvä puoli on siinä, että menun toimintaa voi heti kokeilla. Usein käytetään molempia sopivasti vuorotellen. Windows-ohjelmia käytettäessä on kuitenkin syytä jälleen huomata, ettei sama tiedosto ole vahingossa auki kahdessa ohjelmassa yhtäaikaa!
Menuja suunniteltaessa on syytä pitäytyä valmiissa termeissä ja menujen sijoituksissa. Lähes jokaisessa Windows-ohjelmassa on vasemmassa laidassa toiminnot File ja Edit. Vastaavasti oikeassa laidassa (Help break) on toiminto Help. Muut ohjelman menutoiminnot sijoitetaan näiden väliin taas sopivasti valmiista ohjelmista matkien.
Menujen suunnittelu onkin hyvä tapa aloittaa ohjelman suunnittelu.
hMenuE = LoadMenu(hInstance,"EPAAMENU"); SetMenu(hWnd,hMenuE); ... DestroyMenu(hMenuE); /* Muista tuhota menut lopuksi!!! */
Menusysteemi on itsenäisesti toimiva kokonaisuus, joka antaa menun omistavalle ikkunalle WM_COMMAND -viestin kun menusta on lopulta valittu haluttu toiminto. Viestin ID-osassa (GET_WM_COMMAND_ID) tulee menuvalinnalle annettu kokonaislukuarvo. Vain menupuun lehtisolmuista saadaan viesti. Toiminnot kannattaa numeroida kuvaavasti siten, että myöhemmin ohjelmakoodissa muistaa mikä valinta tarkoitti mitäkin. Numeroarvot eivät ole suositeltavia. Hyvä nimi olisi esimerkiksi CM_FILE_NEW:
komento CM (CoMmand),
alasvetovalikko File (FILE)
ja sen alakohta New (NEW).
Menujen valinnat voidaan jo suunnitteluvaiheessa merkitä valituiksi (Checked, v-merkki) tai ei-aktiivisiksi (disabled, grayed, harmaa). Käytön mukaan näitä sitten ohjelmassa muutetaan esimerkiksi kutsulla
CheckMenuItem(hMenu,IDM_OPTIONS_DEC,MF_BYCOMMAND | MF_UNCHECKED); EnableMenuItem(hMenu,IDM_OPTIONS_DEC,MF_BYCOMMAND | MF_GRAYED);
Kahva menuun saadaan esimerkiksi kutsulla:
HMENU hMenu = GetMenu(hWnd);
Edellä MF_BYCOMMAND tarkoittaa, että menun valintaa etsitään tunnuksen (tässä IDM_OPTIONS_DEC) mukaan. Toinen vaihtoehto olisi järjestyksen mukaan, mikä ei suinkaan ole hyvä vaihtoehto, jos joku muuttaa menun valintojen järjestystä.
Voitaisiin esimerkiksi tehdä ohjelma, jossa olisi käytettävissä kaksi kieltä: suomi ja englanti. .RC -tiedostoon kirjoitettaisiin kaksi erillistä menua, joissa toisessa olisi kielen vaihto suomeksi ja toisessa englanniksi ja tietysti kumpikin menu omalla kielellään. Jossakin kohti alustuksissa ladattaisiin kahvoihin hMenuE ja hMenuF vastaavat menut. Ikkunan funktiossa sitten menuja vaihdettaisiin seuraavasti:
case WM_COMMAND: /* Dialogin toiminnot: */ switch ( GET_WM_COMMAND_ID(wParam,lParam) ) { ... case IDM_OPTIONS_ENG: SetMenu(hWnd,hMenuE); return TRUE; case IDM_OPTIONS_FIN: SetMenu(hWnd,hMenuF); return TRUE; ...
Menuihin voitaisiin myöhemmin vaihtaa bittikarttoja tekstien tilalle kutsulla:
ModifyMenu(hMenu,IDM_OPTIONS_FIN, MF_BYCOMMAND | MF_BITMAP,IDM_OPTIONS_FIN, (LPSTR)hBitFin) )
Toisaalta bittikarttoja voitaisiin suoraan lisätä kutsulla:
InsertMenu(hMenu,IDM_OPTIONS_FIN, MF_ENABLED | MF_BITMAP,IDM_OPTIONS_ENG, (LPSTR)hBitEng);
ChooseColor - värin valintadialogi, vertaa ChooseFont - fontin valinta FindText - tekstin etsintädialogi GetOpenFileName - tiedoston nimi avaamista varten -dialogi GetSaveFileName - tiedoston nimi tallettamista varten -dialogi PrintDlg - dialogi tulostustietojen kysymistä varten ReplaceText - tekstin korvaamisdialogi
Näiden käyttö selviää parhaiten Helpin avulla.
/***************************************************************************/ DWORD AskFileName(HWND hwnd,int save,char *FileName) { static OPENFILENAME ofn; static char szDirName[256]="."; static char szFile[256]="", szFileTitle[256]; static char szCustomFilter[256]="(*.*)\0*.*\0"; static int FirstTime = 1; /* Call this first time */ static char szFilter[256]= "Laskuri (*.las)\0*.las\0Teksti (*.txt)\0*.txt\0Kaikki (*.*)\0*.*\0"; DWORD error = 0; if ( FirstTime ) { /* If first call. */ FirstTime = 0; /* Set all structure members to zero on first time. */ memset(&ofn, 0, sizeof(OPENFILENAME)); ofn.lStructSize = sizeof(OPENFILENAME); ofn.lpstrFilter = szFilter; ofn.nFilterIndex = 1; ofn.lpstrFile = szFile; ofn.nMaxFile = sizeof(szFile); ofn.lpstrCustomFilter = szCustomFilter; ofn.nMaxCustFilter = sizeof(szCustomFilter); ofn.lpstrFileTitle = szFileTitle; ofn.nMaxFileTitle = sizeof(szFileTitle); ofn.lpstrInitialDir = szDirName; } else if ( save ) strcpy(szFile,FileName); else szFile[0]=0; ofn.hwndOwner = hwnd; ofn.Flags = OFN_SHOWHELP | OFN_PATHMUSTEXIST | OFN_FILEMUSTEXIST; if ( ( save ? GetSaveFileName(&ofn) : GetOpenFileName(&ofn) ) == TRUE ) { char s[80]; wsprintf(s,"%s - [%s]",(LPSTR)WTITLE,ofn.lpstrFileTitle); SetWindowText(hwnd,s); strcpy(FileName,szFile); /* strcpy is bad idea! */ return 0; } if ( CommDlgExtendedError() == 0 ) return -1; /* Only user pressed CANCEL or something like that */ return error; }
Yleinen kirjasto hoitaa seuraavat asiat:
uuden tiedoston luonti (vanhan talletus)
tiedoston avaaminen (vanhan talletus)
tiedoston talletus jos sitä muutettu (jollei nimeä, kysytään nimi)
vanhojen tiedoston listan ylläpito File -menussa
Käyttäjä täyttää tiedostoa kuvaavan tietorakenteen:
static FileType File={ "", /* Tiedoston nimi, aluksi "" */ WTITLE, /* Ikkunan otsikko, jonka perään tied.nimi */ 1, /* Luetun funktion tyyppi, ks. szFilter */ "Laskuri (*.las)\0*.las\0Teksti (*.txt)\0*.txt\0Kaikki (*.*)\0*.*\0", 0, /* Onko tiedostoa muutettu. */ MySave, /* Osoitin talletusfunktioon */ MyRead, /* Osoitin lukufunktioon */ MyNew, /* Osoitin tiedon nollausfunktioon */ laskurit /* Osoitin varsinaiseen dataan. */ };
missä MySave, MyRead ja MyNew ovat käyttäjän kirjoittamia omia funktioita talletukseen, lukemiseen ja uuden luomiseen.
/*-------------------------------------------------------------------------*/ /* FILE */ static EVENT WM_command_IDM_FILE_NEW(tMSGParam *msg) { return NewFile(msg->hWnd,&File); } static EVENT WM_command_IDM_FILE_OPEN(tMSGParam *msg) { return OpenAndReadFile(msg->hWnd,&File);; } static EVENT WM_command_IDM_FILE_SAVEAS(tMSGParam *msg) { return SaveFileAs(msg->hWnd,&File); } static EVENT WM_command_IDM_FILE_SAVE(tMSGParam *msg) { return SaveFile(msg->hWnd,&File); } /*-------------------------------------------------------------------------*/ /* OPTIONS */ static EVENT WM_command_IDM_OPTIONS_INC(tMSGParam *msg) { HMENU hMenu = GetMenu(msg->hWnd); CheckMenuItem(hMenu,IDM_OPTIONS_DEC,MF_BYCOMMAND | MF_UNCHECKED); CheckMenuItem(hMenu,IDM_OPTIONS_INC,MF_BYCOMMAND | MF_CHECKED); suunta = 1; return 0; } static EVENT WM_command_IDM_OPTIONS_DEC(tMSGParam *msg) { HMENU hMenu = GetMenu(msg->hWnd); CheckMenuItem(hMenu,IDM_OPTIONS_DEC,MF_BYCOMMAND | MF_CHECKED); CheckMenuItem(hMenu,IDM_OPTIONS_INC,MF_BYCOMMAND | MF_UNCHECKED); suunta = -1; return 0; }
Sitten testejä varten talletus olisi vain:
/***************************************************************************/ int MySave(FileType *File) { MessageBox(File->hWnd,File->Name,"Talletus:", MB_APPLMODAL | MB_OK | MB_ICONEXCLAMATION); return 0; }
MessageBox palauttaa arvonaan tiedon siitä, mitä nappulaa painettiin laatikosta poistumiseksi. Tämän avulla on helppo laatia mm. kyllä/ei -tyylisiä valintoja:
... wsprintf(s,"%s\nTiedostoa muutettu.\n\nTalletetaanko?", (LPSTR)File->Name); answ = MessageBox(hWnd,s,File->Title,MB_APPLMODAL | MB_YESNOCANCEL | MB_ICONEXCLAMATION); switch ( answ ) { case IDYES: if ( (error = SaveFile(hWnd,File)) != 0 ) return error; case IDNO: return ClearFileName(hWnd,File,ClearName); default: return -1; }
MB_ICONEXCLAMATION tarkoittaa huutomerkkiä laatikon vasempaan reunaan. Myös STOP-merkki, kysymysmerkki ja i-kirjain ovat mahdollisia.
static int MySave(FileType *File) { static OFSTRUCT OfStruct; int count,n; static char *s; HFILE hFile = OpenFile(File->Name,&OfStruct,OF_CREATE); if ( hFile == HFILE_ERROR ) goto error; s = laskurit_tekstiksi((int *)File->data); count = strlen(s); n = _lwrite(hFile,s,count); _lclose(hFile); if ( n == count ) return 0; error: MessageBox(File->hWnd,File->Name,"Talletus epäonnistui", MB_APPLMODAL | MB_OK | MB_ICONEXCLAMATION); return -1; }
Windowsissa voidaan käyttää myös normaaleja fopen, fgets, fprintf ja fscanf -funktioita. Kuitenkin Windowsin luonteen takia suositellaan käytettäväksi funktioita OpenFile, _lwrite, _lread ja _lclose. Tosin rivi kerrallaan lukemista varten täytyy tällöin tehdä itse fgets:ää vastaava funktio. OpenFile:n parametreillä voidaan valita miten tiedosto avataan:
OF_CANCEL - CANCEL-nappula levynvaihto viestiin OF_CREATE - luo uuden tiedoston, vanha häviää OF_DELETE - tuhoaa tiedoston OF_EXIST - avaa ja sulkee, kätevä tiedoston olemassaolon testaamiseksi OF_PARSE - täyttää OFSTRUC-tietueen, muttei tee muuta OF_PROMPT - pyytää tarvittaessa levykettä asemaan OF_READ - avataan vain lukemista varten OF_READWRITE - lukemista ja kirjoittamista varten OF_REOPEN - jo avattu (ja suljettu) tiedosto avataan uudelleen OF_SEARCH - tiedostoa etsitään koko polusta OF_SHARE_COMPAT - muutkin saavat avata tiedoston, jopa useasti OF_SHARE_DENY_NONE - muutkin saavat avata OF_SHARE_DENY_READ - muut eivät saa lukea tiedostoa OF_SHARE_DENY_WRITE - muut eivät saa kirjoittaa OF_SHARE_EXCLUSIVE - muut eivät saa lukea eivätkä kirjoittaa OF_VERIFY - vertaa tiedoston luontiaikaa OFSTRUCT tietueen tietoihin OF_WRITE - avaa kirjoittamista varten
Itse lukeminen ja kirjoittaminen tapahtuvat sitten C:n alemman tason ei-formatoiduilla funktioilla.
Huomattakoon, ettei OpenFilellä avattu kahva ole yhteensopiva fprintf vaatiman FILE -tyyppisen osoittimen kanssa.
Kun ohjelma menettää kontrollin, olisi hyvä sulkea tiedostot. Jokin toinen ohjelmahan saattaa tänä aikana pyytää vaikkapa levykkeen vaihtoa. Kun kontrolli jälleen palautuu ohjelmalle, voidaan uudelleen avaaminen tehdä OF_REOPEN -parametrilla. Tämän vuoksi OpenFile -funktion käyttö on suositeltavaa.
WIN.INI on yksi tyypillinen tällainen tiedosto. Tähän tiedostoon olisi kuitenkin suositeltavaa kirjoittaa vain informaatiota, joka on tarkoitettu muidenkin ohjelmien käytettäväksi. Lukeminen ja kirjoittaminen tapahtuu GetProfileString ja WriteProfileString -funktioilla. Kokonaisluvun lukemiseksi on lisäksi GetProfileInt -funktio.
Ohjelman omat tiedot talletetaan vastaavassa formaatissa olevaan ohjelman omaan .INI -tiedostoon. Tätä varten on vastaavat ???PrivateProfile??? -funktiot Esimerkiksi laskurin tila voitaisiin ohjelman lopuksi tallettaa tiedostoon LASKURI.INI muodossa:
[Laskuri] - 1. param. määrää tämän nimen Laskuri=21 5 - 2. param. määrää tämän nimen
Ohjelmakoodissa talletus olisi vaikkapa kun saadaan ohjelman lopettamista vaativa viesti:
#define WTITLE "Laskuri" #define ININAME ".\\"WTITLE".INI" ... static EVENT lopeta(tMSGParam *msg) { WritePrivateProfileString(WTITLE,WTITLE, laskurit_tekstiksi(laskurit), ININAME); if ( NewFile(msg->hWnd,&File) == 0 ) PostQuitMessage(0); return 0; }
Ensimmäinen WTITLE -parametri tarkoittaa hakasulkeisiin tulevan osion nimeä, toinen osion alle tulevaa valintaa Laskuri=. Näiden ei tietenkään tarvitse olla samoja.
Tiedoston lukeminen tapahtuu vastaavasti ikkunan syntyessä:
static EVENT WM_initdialog(tMSGParam *msg) { char s[50]; GetPrivateProfileString(WTITLE,WTITLE,"",s,sizeof(s), ININAME); teksti_laskureiksi(msg->hWnd,s,laskurit); return 0; }..
Välttääkseen muistin pirstoutumista Windows haluaa mielellään muistinvaraukset tehtäväksi siten, että systeemillä on oikeus järjestellä muistia uudelleen. Tämä johtaa siihen, että muistinhallintaan tarvitaan useita funktiota: muistin varaus (Alloc), muistin lukitseminen käytön ajaksi paikalleen (Lock), muistin lukituksen vapauttaminen (UnLock) ja muistin vapautus (Free).
Kustakin funktiosta on sekä Global että Local -versio.
Käytännössä Alloc-funktiot palauttavat kahvan muistialueeseen. Sen avulla ei siis voida kirjoittaa mihinkään. Lock -toiminto antaa sitten fyysisen muistiosoitteen (32 bittiä Global-versiossa), jonka avulla varsinainen muistin käsittely suoritetaan.
Kun käsittely on suoritettu loppuun, pitää muistilohko päästää jälleen kellumaan Unlock -funktiolla. Tällöin Lock -funktiolla saatu muistiosoite ei tietenkään häviä mihinkään, mutta Windows saattaa siirtää muistilohkon eri paikkaan ja em. muistiosoitteeseen saattaa siirtyä vallan muuta tietoa. Siis ohjelmoija on itse vastuussa siitä, ettei hän käytä muistiosoitetta Unlock -funktion kutsumisen jälkeen.
Laskuriohjelman näyttöjen arvot voitaisiin tallettaa leikekirjaan seuraavasti:
static int laskurit_leikekirjaan(HWND hWnd,int *laskurit) { const char *pText = laskurit_tekstiksi(laskurit); return TextToClipboard(hWnd,pText); }
#define TextToClipboard(hWnd,s) \ AddToClipboard(hWnd,CF_TEXT,s,strlen(s)+1,TRUE) /***************************************************************************/ int AddToClipboard(HWND hWnd,UINT fmt,const char *Text,int len,BOOL empty) /* Lisätään leikekirjaan data Text, josta otetaan len tavua. ** Jos hWnd == NULL, oletetaan leikekirjan olevan avoinna. ** Jos empty==TRUE, niin leikekirja tyhjennetään ennen lisäystä. ** Palautetaan != 0 jos lisäys ei onnistu. */ { HANDLE hData; char FAR *lpData; if ( !(hData = GlobalAlloc(GMEM_MOVEABLE, len)) ) return -1; if ( (lpData = GlobalLock(hData)) == NULL || (hWnd && !OpenClipboard(hWnd)) ) { GlobalFree(hData); return -2; } _fmemcpy(lpData, Text,len); GlobalUnlock(hData); if ( empty ) EmptyClipboard(); SetClipboardData(fmt, hData); if ( hWnd ) CloseClipboard(); return 0; }
Leikekirjalle tieto viedään globaalin muistilohkon kahvana. Siis ensiksi pitää moinen lohko luoda ja siirtää tieto sinne. Kun tieto on saatu lohkoon, pitää lohko päästää kellumaan. Leikekirja avataan, tyhjennetään (jollei haluta tallettaa useita eri formaatteja samasta asiasta) ja sille välitetään edellä täytetyn muistilohkon kahva. Samalla kerrotaan mitä tyyppiä muistilohkossa oleva tieto on.
Muistilohkon tuhoamisesta vastaa tämän jälkeen leikekirja (esim. EmptyClipboard -kutsun yhteydessä). Leikekirjassa voi olla sama tieto useita kertoja eri formaatissa. Esimerkiksi taulukkolaskenta voi tallettaa leikekirjaan solujen sisällön tekstinä ja toisaalta myös bittikuvana. Usein myös talletetaan leikekirjaan tieto siitä, miten voidaan muodostaa DDE-linkki tietoon.
int leikekirja_laskureihin(HWND hWnd,int *laskurit) { const char *pText = TextClipboard(hWnd); if ( !pText ) return -1; teksti_laskureiksi(hWnd,pText,laskurit); return 0; }
#define CopyFromClipboard(hWnd,fmt) CopyFromClipboardN(hWnd,fmt,NULL) #define TextClipboard(hWnd) CopyFromClipboard(hWnd,CF_TEXT) /***************************************************************************/ char *DataFromClipboardN(HWND hWnd,UINT fmt,UINT *n,char *buf, UINT bufm) /* Otetaan tieto leikekirjasta. Jos hWnd=NULL, oletetaan leikekirjan ** jo olevan auki, eikä sitä avata eikä suljeta. ** Jos muoto on tekstiformaattia, on koko jonon pituus. */ { HANDLE hClipData; char FAR *lpClipData; DWORD gsize,size = bufm-1; if ( n ) *n = 0; buf[0]=0; if ( !fmt ) return NULL; if ( hWnd && !OpenClipboard(hWnd) ) return NULL; if ( !IsClipboardFormatAvailable(fmt) ) { if ( hWnd ) CloseClipboard(); return NULL; } /* get data from the clipboard */ if (!(hClipData = GetClipboardData(fmt))) { if ( hWnd ) CloseClipboard(); return NULL; } gsize = GlobalSize(hClipData); if ( (lpClipData = GlobalLock(hClipData)) == NULL ) { if ( hWnd ) CloseClipboard(); return NULL; } if ( gsize < size ) size=gsize; _fmemcpy(buf, lpClipData,(UINT)size); buf[(UINT)size] = 0; GlobalUnlock(hClipData); if ( hWnd ) CloseClipboard(); if ( TextFmt(fmt) ) size = strlen(buf); if ( n ) *n = (UINT)size; return buf; } /***************************************************************************/ const char *CopyFromClipboardN(HWND hWnd,UINT fmt,UINT *n) { static char pText[MAX_COPY]; return DataFromClipboardN(hWnd,fmt,n,pText,sizeof(pText)); }
Leikekirja palauttaa globaalin muistilohkon kahvan, mikäli siellä on halutun muotoista dataa. Lohko lukitaan, tieto siirretään omaan käyttöön ja leikekirja suljetaan. Muistilohkon vapauttaminen jätetään edelleen leikekirjalle, mutta lohko päästetään tietysti kellumaan.
Edellähän jo näytettiin, miten bittikartta laitetaan mukaan menuihin.
wc.hIcon = LoadIcon(hInstance,"Ikoni");
Mikäli .RC -tiedostossa on tehty dialogeja, voidaan dialogeihin laittaa näitä ikoneita. Huomattakoon, että tällöin ikonin nimi annetaan Caption -kohtaan, ei ID-kohtaan.
Laskuriohjelmassa ei kuitenkaan ollut pääikkunaa, vaan dialogi oli ohjelman pääikkunana. Tällöin ohjelmalle ei voida ilmoittaa ikonia. SetClassWord -funktiolla voidaan kyllä muuttaa dialogi-luokan ikonia, mutta tällöin muuttuu kaikkien dialogien ikoni (myös muiden ohjelmien), joten sitä ei sopine tehdä.
Jos dialogille ei tehdä muita muutoksia, pitää ikoni piirtää itse kun laskuriohjelma kutistetaan ikoniksi, Tämä voidaan tehdä vaikkapa seuraavasti:
Alustuksessa:
... ikoni = LoadIcon(hInstance,"IKONI"); ...
Ikkunafunktiossa:
case WM_PAINT: if ( !BeginPaint(hWnd,&ps) ) break; /* Ikoni joudutaan piirtämään itse, koska dialogi luokalla ei ikonia */ if ( IsIconic(hWnd) ) PiirraIkoni(ps.hdc); EndPaint(hWnd,&ps); return TRUE;
Itse piirtävä aliohjelma on sitten vaikkapa seuraava:
/***************************************************************************/ void PiirraIkoni(HDC hDC) /* Piirretään ikoni ikonialueelle maalaten ensin ikonin tausta harmaaksi */ { SelectObject(hDC,GetStockObject(GRAY_BRUSH)); Rectangle(hDC,0,0,40,40); DrawIcon(hDC,2,2,ikoni); }
Vastaavasti voitaisiin piirtää ikoni myös itse pääikkunan tai dialogin alueelle.
static HBITMAP hBitBack; ... hBitBack = LoadBitmap(hInstance,"TAUSTA");
Latauksen tuloksena saadaan siis kahva bittikarttaan. Ohjelman lopuksi pitää bittikartat poistaa kutsulla:
DeleteObject(hBitBack);
Bittikarttaa tuhottaessa pitää olla varma, ettei se ole minkään laiteyhteyden objektina sillä hetkellä!
Taustakuva piirretään kun saadaan WM_ERASEBKGND -viesti:
case WM_ERASEBKGND: /* Ison bittikartan venyttäminen kestää kauan ja ant. myös huon. tul.*/ /* TaytaBittikarttalla(hWnd,(HDC)wParam,hBitBack); */ PiirraBittikartta((HDC)wParam,hBitBack,0,0); return TRUE;
Bittikartta normaalimuodossa saadaan tulostettua aliohjelmalla:
/***************************************************************************/ int PiirraBittikartta(HDC hDC,HBITMAP hBitmap,int x,int y) /* Piirretään bittikartta alkaen paikasta x,y */ { HDC hDCBitmap = CreateCompatibleDC(hDC); HGDIOBJ hOld = SelectObject(hDCBitmap,hBitmap); BITMAP bm; GetObject(hBitmap,sizeof(bm),&bm); BitBlt(hDC,x,y,bm.bmWidth,bm.bmHeight,hDCBitmap,0,0,SRCCOPY); SelectObject(hDCBitmap,hOld); DeleteDC(hDCBitmap); return 1; }
Bittikartan tulostaminen halutulle laiteyhteydellä aloitetaan tekemällä bittikartalle vastaava laiteyhteys muistiin. Tähän laiteyhteyteen valitaan sitten käytettävä bittikartta. Mikäli nyt piirrettäisiin muistilaiteyhteyteen, menisi tulos bittikarttaan. Siis tätä voitaisiin käyttää bittikarttojen luomiseen.
Laiteyhteyksien välillä bittikartta voidaan siirtää BitBlt -funktiolla. Ensin on kuitenkin otettu selville alkuperäisen bittikartan tiedot, erityisesti leveys ja korkeus.
Aliohjelma käy tietysti bittikartan tulostamiseksi mihin tahansa kohtaan, ei vain taustakuvaksi.
Jälleen on oltava tarkkana, että tuhottaessa muistiin tehtyä laiteyhteyttä, ei siellä ole vahingossa mitään bittikarttaa aktiivisena. Siis objektin vaihdossa on muistettava tallettaa vanha objekti (hOld) myöhempää palauttamista varten!
Taustan piirtoa helpottamaan on tehty aliohjelmakirjasto ALI\TAUSTA.C.
Kun kieli halutaan vaihtaa, voidaan se valita menusta tai napauttamalla lipun kuvaa, joka on Button-tyyppinen, eikä staattinen kuten vasemmassa laidassa oleva ikoni. Siis lipusta saadaan WM_COMMAND -viesti. Viestiin vastataan vaihtamalla seuraavaan kieleen:
/***************************************************************************/ int AsetaKieli(HWND hWnd,int k) { kieli = k; if ( kieli >= Kielia ) kieli = 0; SetMenu(hWnd,Languages[kieli].hMenu); InvalidateRect(hWnd,NULL,FALSE); return 0; }
Kun ikkuna pyydetään piirtämään uudelleen, tulee dialogi piirretyksi uudelleen. Koska lipun tyypiksi on ilmoitettu User Draw, tyhjentää systeemi lipun alta alueen ja sitten antaa ikkunafunktiolla viestin WM_DRAWITEM:
static EVENT WM_drawitem_ID_LANGUAGE(tMSGParam *msg) { DRAWITEMSTRUCT *dis = (DRAWITEMSTRUCT *)msg->lParam; HWND hwnd = GetDlgItem(msg->hWnd,ID_LANGUAGE); TaytaBittikartalla(hwnd,dis->hDC,Languages[kieli].hBit); return 0; }
Koska koko lipulle varattu alue halutaan lipun kokoiseksi, pitää lippua näytöstä riippuen joko pienentää tai suurentaa (dialogin koko saattaa riippua näytön resoluutiosta, joten sen pikselimäärä ei aina ole sama). Siis piirtofunktiolle pitää viedä myös lippuikkunan kahva, jotta ikkunan dimensiot saadaan selville.
/***************************************************************************/ int TaytaBittikartalla(HWND hWnd,HDC hDC,HBITMAP hBitmap) /* Täytetään koko alue bittikartalla */ { HDC hDCBitmap = CreateCompatibleDC(hDC); HGDIOBJ hOld = SelectObject(hDCBitmap,hBitmap); RECT rc; BITMAP bm; GetObject(hBitmap,sizeof(bm),&bm); GetClientRect(hWnd,&rc); StretchBlt(hDC,0,0,rc.right,rc.bottom,hDCBitmap, 0,0,bm.bmWidth,bm.bmHeight,SRCCOPY); SelectObject(hDCBitmap,hOld); DeleteDC(hDCBitmap); return 0; }
StretchBlt -funktio muotoilee bittikartan mahtumaan haluttuun alueeseen. Joko pikseleitä joudutaan jättämään välistä pois tai sitten joudutaan lisäämään uusia. Tulos saattaa siis hieman kärsiä ja toimitus on hitaampi kuin pelkkä BitBlt. StretchBlt -funktiolla voidaan myös peilata bittikarttoja antamalla joko leveyteen tai korkeuteen negatiivisia arvoja. Tällöin tulee myös muistaa muuttaa alkupisteen koordinaatteja (ks. AUTOT.C).
1. luetaan bittikartan kokoinen alue valitusta paikasta taustakuvaa toiseen bittikarttaan
2. piirretään bittikartta
3. jonkin ajan kuluttua talletetaan tausta
4. lasketaan bittikartan uudet koordinaatit
5. jatketaan kohdasta 1.
Windowsissa tämä tapa tuottaa kuitenkin huomattavasti ongelmia. Tausta saattaa muuttua ja luettu tausta on väärä. Näin tapahtuu esimerkiksi jonkin alla olevan ikkunan muuttaessa arvoaan, lipun vaihtuessa lippunappulassa, ikkunan koon muuttuessa jne.
Siis koko toimitus onkin parempi tehdä muodostamalla kustakin liikkuvasta bittikuvasta oma Windowsin ikkuna. Nyt liikuttelemalle näitä ikkunoita Windows hoitaa kaikki taustaan liittyvät ongelmat sekä lisäksi leikkauksen muiden ikkunoiden kanssa.
Loppu onkin varsin helppoa, mikäli liikuteltava kuva on suorakaiteen muotoinen ja täyttää koko ikkunansa. Jos kuitenkin liikuteltavan kuvan halutaan olevan epämääräisen muotoinen, pitää käyttää monimutkaisempaa menetelmää kuin pelkkä bittikuvan piirtäminen.
Samoin tehtävä vaikeutuu jos kuvan halutaan olevan erilainen eri suuntiin liikuttaessa. Tällöin tarvitaan tietysti kaikkiin liikesuuntiin erilaiset bittikartat. Yksinkertaisessa tapauksessa nämä voidaan muodostaa peilaamalla alkuperäistä bittikarttaa.
Läpinäkyvä kuva muodostetaan seuraavasti:
Piirrettävälle alueelle tehdään piirrettävän kuvan muotoinen musta aukko. Tähän aukkoon laitetaan OR-operaatiolla haluttu värillinen bittikuva, josta ylimääräiset reunat on muutettu mustaksi.
Tarvitaan siis kaksi bittikuvaa. AND-maski, joka tekee mustan aukon ja OR-maski joka piirtää varsinaisen kuvan (joskus käytetään myös XOR-maskia, jolla voidaan lisäksi vaihtaa taustan väriä sopivista kohdista). Nämä maskit voidaan tietysti muodostaa alkuperäisestä kuvasta, jos oletetaan jonkin tietyn värin (esim. valkoisen) tarkoittavan läpinäkymistä.
Seuraavassa auton alustamisen hieman yksinkertaistettu versio:
typedef struct { /* Yhden auton bittikartojen kahvat */ HBITMAP hAnd; /* AND -maski (mustavalkoinen) */ HBITMAP hOr; /* OR -maski (mahd. värillinen) */ } MaskiTyyppi; /***************************************************************************/ /* .INI -merkityt kohdat voi alustaa .INI-tiedostolla */ typedef struct { /* Auton ylläpitämiseksi tarvittavat tiedot: */ char nimi[20]; /* Bittikuvan nimi. .INI */ int korkeus; /* Auton yläreunan y-koordinaatti, 0 on pohj. .INI */ int vauhti; /* x-siirtymä kerrallaan .INI */ int aikavali; /* Siirtymien aikaväli ms .INI */ int paikka; /* Auton x-koordinaatti .INI */ int suunta; /* +/- vauhti kulkusuunnan mukaan */ int xkoko; /* Bittikartan x-koko (Selvitetään bittikartasta) */ int ykoko; /* Bittikartan y-koko */ int xmin; /* Auton liikkeen pienen x-koordinaatti */ int xmax; /* Vastaava suurin x-koordinaatti (ikkunan koko määr) */ int kopio; /* Onko bittikartat kopioita jostakin muusta */ MaskiTyyppi AutoO; /* Oikealle kulkevan auton bittimaskit */ MaskiTyyppi AutoV; /* Vasemmalle kulkevan auton bittimaskit */ MaskiTyyppi Nyt; /* Kulkusuunnan mukaan joko AutoO tai AutoV */ int alustettu; /* Onko tietorakenne alustettu (0 = ei) */ int nakyvissa; /* Onko auto näkyvissä (nykyinen versio ei käytä) */ int pysakoity; /* Onko auto pysäköity vai ei (oletus ei). */ HWND hwnd; /* Autoa kuvaavan ikkunan kahva. */ HWND ohitus; /* Kulkeeko auto päällä vai pohjalla .INI */ HWND ParenthWnd; /* Isäikkunan kahva. */ HINSTANCE hInstance;/* Esiintymä, jossa auton ikkuna luotu */ int kokokuva; /* Auto täyttää koko ikkunansa,helppo siirtää .INI */ } AutoTyyppi;
static int alusta_ja_lisaa_auto(HWND hWnd,AutoTyyppi *a) { RECT rc; AutoTyyppi *pa; if ( !a ) return 1; if ( a->alustettu ) return 0; a->ParenthWnd = hWnd; if ( !hWnd ) hWnd = GetDesktopWindow(); GetClientRect(hWnd,&rc); if ( luo_bittikartat(hWnd,a) ) return 1; a->Nyt = a->AutoO; if ( !a->vauhti ) a->vauhti = 10; if ( !a->aikavali ) a->aikavali = 100; if ( !a->xmax ) a->xmax = rc.right - a->xkoko; if ( !a->korkeus ) a->korkeus = rc.bottom - a->ykoko; a->suunta = a->vauhti; a->paikka = a->xmax/2; a->hwnd = CreateWindow(WAutoWClass,a->nimi, WS_CHILD | WS_VISIBLE, a->paikka,a->korkeus,a->xkoko,a->ykoko, hWnd,NULL,a->hInstance,(void *)a); if ( a->hwnd == NULL ) { putsaa_auto(a); return 1; } a->nakyvissa = 0; a->alustettu = 1; if ( ( pa = lisaa_listaan(a) ) == NULL ) return 1; SetWindowLong(a->hwnd,0,(LONG)pa); SetTimer(a->hwnd,1,a->aikavali,NULL); return 0; }
static int luo_bittikartat(HWND hWnd,AutoTyyppi *a) { HDC hDC,hDCBit1,hDCBit2; HGDIOBJ hOld1,hOld2; /* Ja niiden vanhat "työkalut" */ BITMAP bm; /* Oikealle ja vasemmalle liikkuvat värilliset autot */ a->kopio = 0; a->AutoO.hOr = LoadBitmap(a->hInstance,a->nimi); if ( !a->AutoO.hOr ) return 1; hDC = GetDC(hWnd); hDCBit1 = CreateCompatibleDC(hDC); /* Apu muisti-DC:t piirtelyyn */ hDCBit2 = CreateCompatibleDC(hDC); GetObject(a->AutoO.hOr,sizeof(bm),&bm); a->xkoko = bm.bmWidth; a->ykoko = bm.bmHeight; a->AutoV.hOr = CreateCompatibleBitmap(hDC,a->xkoko,a->ykoko); /* Mustavalkoiset maskiautot (bitmap on mustavalk. jos CCB muisti DC:stä */ /* jossa ei ole ennestään bitmappia!) */ a->AutoO.hAnd = CreateCompatibleBitmap(hDCBit1,a->xkoko,a->ykoko); a->AutoV.hAnd = CreateCompatibleBitmap(hDCBit2,a->xkoko,a->ykoko); hOld1 = SelectObject(hDCBit1,a->AutoO.hOr); hOld2 = SelectObject(hDCBit2,a->AutoO.hAnd); /* Maskiauto, jossa vain mustaa autossa ja valkoista reunoilla */ BitBlt(hDCBit2,0,0,a->xkoko,a->ykoko,hDCBit1,0,0,SRCCOPY); /* Alkuperäisestä autosta reunat mustaksi maskiauton avulla */ BitBlt(hDCBit1,0,0,a->xkoko,a->ykoko,hDCBit2,0,0,SRCINVERT); /* Vasemmalle liikkuva auto tehdään oikealle liikkuvasta autosta */ SelectObject(hDCBit2,a->AutoV.hOr); /* Tehdään auton peilikuva bittikarttaan AutoV.hOr */ StretchBlt(hDCBit2,0,0,a->xkoko,a->ykoko, hDCBit1,a->xkoko-1,0,-a->xkoko,a->ykoko,SRCCOPY); /* Mustavalkoinen vasemmalle menevä maskiauto */ SelectObject(hDCBit1,a->AutoV.hAnd); SelectObject(hDCBit2,a->AutoO.hAnd); /* Tehdään mustavalkoisen auton peilikuva bittikarttaan AutoV.hAnd */ StretchBlt(hDCBit1,0,0,a->xkoko,a->ykoko, hDCBit2,a->xkoko-1,0,-a->xkoko,a->ykoko,SRCCOPY); /* Poistetaan turhat DC:t */ SelectObject(hDCBit2,hOld2); SelectObject(hDCBit1,hOld1); DeleteDC(hDCBit2); DeleteDC(hDCBit1); ReleaseDC(hWnd,hDC); return 0; }
Aluksi ladataan alkuperäinen auton kuva resursseista. Tästä tehdään sitten tarvittavat 4 maskia: vasemmalle ja oikealle AND ja OR-maskit.
Mustavalkoinen bittikartta saadaan tekemällä "yhteensopiva" bittikartta muistiDC:stä. Värillisen uuden bittikartan tekemiseksi pitää se tehdä oikeasta fyysisestä laiteyhteydestä. Kun bittikartta on luotu ja yhdistetty jonkin muistilaiteyhteyteen, voidaan sinne jälleen piirtää kaikilla Windowsin funktiolla.
Kun bittikartat on saatu valmiiksi, luodaan autoa varten uusi ikkuna, jonka ikkunafunktio on sama kaikille autoille ja taustan harjana on läpinäkyvä harja. Tähän ikkunaan pistetään timeri käyntiin siirtojaksojen mukaisesti.
... auton ikkunafunktiossa case WM_TIMER: if ( !(a = EtsiAuto(hWnd)) ) break; if ( a->pysakoity ) break; if ( IsIconic(a->ParenthWnd) ) return NULL; siirra_auto(a); return NULL; ...
Koska kaikille autoille on sama ikkunafunktio, täytyy ensin etsiä autoista se, jolle viesti tuli. Tämä tunnistetaan ikkunan kahvan perusteella.
Siirtämisessä lasketaan ikkunan uusi paikka ja mikäli auton tausta ei näy läpi, voidaan pyytää pelkkä ikkunan siirto. Mikäli autosta pitää näkyä lävitse, pitää auton kuva "pilata", jotta Windows varmasti piirtää uuden autonkuvan tyhjän taustakuvan päälle. Siirto voitaisiin tehdä myös MoveWindow -funktiolla, mutta tällöin auton "syvyyttä" ei voitaisi muuttaa.
static int siirra_auto(AutoTyyppi *a) { int suunta_vaihtui = 0; if ( !a->alustettu || a->pysakoity ) return 1; a->paikka += a->suunta; if ( a->paikka > a->xmax ) { /* Valitaan auto vasemmalle */ a->paikka = a->xmax; a->suunta = -a->vauhti; a->Nyt = a->AutoV; suunta_vaihtui = 1; } if ( a->paikka < a->xmin ) { /* Valitaan auto oikealle */ a->paikka = a->xmin; a->suunta = a->vauhti; a->Nyt = a->AutoO; suunta_vaihtui = 1; } if ( !a->kokokuva || suunta_vaihtui ) InvalidateRect(a->hwnd,NULL,FALSE); SetWindowPos(a->hwnd,a->ohitus,a->paikka,a->korkeus,a->xkoko,a->ykoko, SWP_NOSIZE ); return 0; }
Malliohjelmassa alustettavien autojen tiedot luetaan .INI -tiedostosta:
[Laskuri] Laskuri=0 40 [Autot] Auto0=HAUTO,0,0,10,200 Auto1=KAUTO,0,100,20,300,10,1 Auto2=HAUTO,1,70,30,100 Auto3=KAUTO,0,0,10,300,100,1
Tiedosto luetaan alusta_autot -aliohjelmassa:
... for (lkm=0;;lkm++) { sprintf(pname,"Auto%d",lkm); /* Tehdään Auto? */ if ( GetPrivateProfileString("AUTOT",pname,"",s,sizeof(s),ininame) == 0 ) break; /* Jollei autoa saatu, lopetetaan */ if ( jono_autoksi(hInst,s,&a) ) continue; alusta_ja_lisaa_auto(hWnd,&a); } ...
static int jono_autoksi(HINSTANCE hInst,char *s,AutoTyyppi *a) { char *p; int ohitus = 0; a->alustettu = 0; a->nimi[0] = 0; a->kokokuva = a->xmin = a->xmax = a->korkeus = a->vauhti = a->aikavali = 0; a->paikka = 0; a->ohitus = HWND_TOP; a->hInstance = hInst; if ( (p=strtok(s,",")) == NULL ) return 1; strncpy(a->nimi,p,sizeof(a->nimi)); a->nimi[sizeof(a->nimi)-1] = 0; if ( (p=strtok(NULL,"\0")) == NULL ) return 0; sscanf(p,"%d,%d,%d,%d,%d,%d",&a->kokokuva,&a->korkeus, &a->vauhti,&a->aikavali,&a->paikka,&ohitus); if ( ohitus ) a->ohitus = HWND_BOTTOM; return 0; }
Näin kaiken kaikkiaan liikkuvista kuvista on saatu aliohjelmakirjasto, jonka käyttämiseksi käyttäjän on vain kutsuttava pääikkunan luomisen jälkeen aliohjelmaa alusta_autot ja lopuksi tuhoa_autot (tosin autot voidaan tuhota automaattisesti, koska ne ovat ikkunoita, joista kukin saa WM_DESTROY -viestin ennen pääikkunan tuhoutumista), kirjoitettava .INI -tiedosto ja tehtävä tietysti resursseihin liikkuvien otusten bittikartat.
Dialogin suunnittelun yhteydessä voidaan dialogille kuitenkin antaa luokka. Tällöin tämä luokka pitää rekisteröidä. Luokan rekisteröinti tapahtuu kuten tavallisenkin ikkunan. Koska kyseessä on kuitenkin dialogi, pitää ikkunan ylimääräisten tavujen määrä muistaa laittaa oikein:
wc.cbWndExtra = DLGWINDOWEXTRA + OMAT_TAVUT;
Luokan ikkunafunktioksi voidaan laittaa jokin oma funktio tai DefDlgProc. Itse dialogille voidaan rakentaa sitten normaali dialogin funktio. Tiedoston ALI\TABHAND.H makroilla koko "ilkeys" on käyttäjän kannalta piilotettu siihen, että kumpi seuraavista makroista valitaan "pääohjelmaksi":
TblClassDlgMAIN("LASKURM4","ikoni","PIKA","LASKURI",MsgTbl); ... vaiko .. TblDlgMAIN("PIKA","LASKURI",MsgTbl);
Jos käytetään Class -versiota, pitää .RC -tiedostossa dialogissa "LASKURI" olla mainittuna luokka "LASKURM4".
LASKURI DIALOG 16, 25, 179, 88 STYLE WS_OVERLAPPED | WS_VISIBLE | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX CLASS "LASKURM4" CAPTION "LASKURM4" MENU KIELI_0 { PUSHBUTTON "&Henkilöautoja", HA, 33, 10, 50, 20
Jollei dialogissa ole luokkaa, ei ikonia kuitenkaan piirretä. Jos vastaavasti dialogissa on luokka, mutta sitä ei ole rekisteröity, dialogi ei näy ruudussa!
Automaattisen ikonin lisäksi dialogiluokan rekisteröimisellä saavutetaan mm. se etu, että luokan ominaisuudet ovat tarkemmin kontrolloitavissa. Useissa sovelluksissa ikkunan pitäisi pystyä tallettamaan ikkunan kahvaan kuuluvaa tietoa. Rekisteröimällä ikkunan luokka itse, voidaan ikkunalle varata tavuja, joissa tieto talletetaan.