Artikkelit

Artikkelit olivat Suomipelit.comissa sivuston sulkeuduttua. Näin ollen niihin saattaa sisältyä rikkinäisiä linkkejä tai epämääräisiä viitteitä asioihin, joita ei enää ole olemassa, ja joskus jokin saattaa näkyä väärin.

Palaa artikkelilistaan

Windows-ohjelmointi

Tämä artikkeli toimii pohjatietona alkavalle Microsoft DirectX -artikkelisarjalle. Artikkelissa käsitellään yksinkertaisen Windows-sovelluksen tekeminen WIN32-APIa käyttäen.

26.2.2002 julkaistun artikkelin on kirjoittanut Jarkko Parviainen.

  1. Viestien käsittelyä
  2. Käsittelijäfunktio
  3. Loppusanat

Tämä artikkeli toimii pohjatietona alkavalle Microsoft DirectX -artikkelisarjalle. Artikkelissa käsitellään yksinkertaisen Windows-sovelluksen tekeminen WIN32-APIa käyttäen. Vaikka esimerkkikoodi on kirjoitettu C-kielen syntaksia mukaillen, käytetään silti tiedostoissa .cpp-päätettä, jotta ne kääntyvät C++ -kääntäjällä. Näin eri muuttujat saadaan esiteltyä lähempänä kohtaa missä niitä käytetään ja vältetään tärkeimmän koodin hautautuminen C++:n oliopohjaisuuden alle.

Koska kyseessä on opetustarkoitukseen tehty artikkeli, ei kaikki sen sisältämä koodi ole suoraan oikeaan ohjelmaan soveltuvaa ja muutenkin pääasiana on selkeys, ei tehokkuus. Artikkeli on pyritty jakamaan sopivan kokoisiin osioihin ja jokainen läpikäyty asia on pyritty selvittämään mahdollisimman selkokielisesti ilman aiheelle ominaisia monimutkaisia vieraskielestä johdettuja termejä. Lähdekoodi esimerkit on testattu Microsoft Visual C++ 6.0 ohjelmointiympäristössä.

Esimerkkien kääntymisen onnistuminen vaatii oikein asennetun WIN32 ohjelmointikirjastojen olemassaoloa.

Koko artikkelin lähdekoodeineen voit imuroida tästä: http://www.suomipelit.com/artikkelit/art... .

Varsinkin DOS-ympäristössä ohjelmointia tehneille Windows on aluksi hieman mystinen viesteineen ja monine muine ihmeellisyyksineen. Näitä kuitenkin tarvitaan, koska Windows on moniajava käyttöjärjestelmä. Pienellä vaivalla edellä mainitut asiat saa kuitenkin hyvinkin läpinäkyväksi itselleen, eikä niistä tarvitse ainakaan alussa hirveämmin välittää. Useimmat pelikoodaajat tarvitsevat ikkunalleen vain pienimuotoisen viestinkäsittelyn ja tuskin koskaan WIN32 APIn hankalampia osia.

Ensimmäinen huomattava ero DOS:iin tehtyyn C/C++-ohjelmaan verrattuna Windows-ohjelmassa on main()-funktion puuttuminen. Se on korvattu Windowsin omalla WinMain() -funktiolla:

INT WINAPI WinMain( HINSTANCE hInstance, 
                    HINSTANCE hPreviousInstance, 
                    LPSTR     lpCmdLine, 
                    INT       nCmdShow ); 


Kuten näkyy, WinMain -funktio eroaa tavallisesta main-funktiosta suhteellisen paljon. Ainoastaan lpCmdLine voi näyttää tutummalta. Kyseessä onkin vanhan tutun argv, argc -parin Windows-vastine. Se sisältää ohjelmaa käynnistäessä annetut komentokehoteparametrit. HINSTANCE-muuttujat saavat nimensä Instance Handle -sanaparista. Handle (suomeksi kahva) on nimensä mukaisesti kiinnityskohta ohjelman ilmentymään (engl. instance) Windowsissa. Windowsin täytyy tietää koko ajan mitä ohjelmia on käytössä, joten Windows luo muistiin ilmentymän ohjelmasta ja seuraa sen avulla ohjelman toimintaa. hInstance on kahva nykyiseen ilmentymään, eli käynnissä olevaan ohjelmaan. Win32 -ohjelmissa hPreviousInstance on kahva ohjelman edelliseen ilmentymään ja on aina arvoltaan NULL. Viimeinen parametri, nCmdShow, kertoo kuinka ikkuna näkyy ruudulla (esimerkiksi minimoituna tehtäväpalkkiin tai maksimoituna peittäen koko ruudun).

Usein WinMain()-funktiossa aivan ensimmäiseksi luodaan ikkuna. Koska Windows on moniajava käyttöjärjestelmä, se tarkoittaa hieman suurempaa koodimäärää kuin tavallinen main()-funktio veisi. Seuraava koodi esittelee ensimmäisen käyttämämme WinMain()-funktion, viestinkäsittelijä- sekä ikkunanluontifunktioiden prototyypit.

#include <windows.h>

// 
// Funktioiden prototyyppejä 
// 

LRESULT CALLBACK ViestienKasittelija( HWND hWnd, UINT Viesti, 
        WPARAM wParam, LPARAM lParam ); 
HRESULT LuoIkkuna( HINSTANCE hInstance, INT nCmdShow ); 

//-------------------------------------------- 
// Nimi  : WinMain() 
// Kuvaus: Ohjelman pääfunktio. 
//-------------------------------------------- 
INT WINAPI WinMain( HINSTANCE hInstance, 
                    HINSTANCE hPreviousInstance, 
                    LPSTR     lpCmdLine, 
                    INT       nCmdShow ) 
{ 
    //
    // Luodaan ikkuna 
    // 
    LuoIkkuna( hInstance, nCmdShow ); 
    //
    // Mennään silmukkaan kunnes ohjelma halutaan 
    // lopettaa.  
    // 
    MSG viesti; 
    while( viesti.message != WM_QUIT ){ 
        //
        // Kurkistetaan onko saapuneita viestejä. 
        // 
        if( PeekMessage( &viesti, NULL, 0U, 0U, PM_REMOVE ) ){ 
            //
            // Jos viesti on saapunut se pitää käsitellä. 
            // 
            TranslateMessage( &viesti ); 
            DispatchMessage( &viesti ); 
        }else{ 
            //
            // Ei viestejä. Suoritetaan ohjelmaa. 
            // 
             
        } 
    } 
    //
    // Poistetaan ikkunaluokka muistista.  
    //  
    UnregisterClass( "IkkunaLuokanNimi", hInstance ); 
    
    // 
    // Palautetaan 0 onnistuneen poistumisen merkiksi. 
    // 
    return 0; 
}  


LuoIkkuna()-funktio luo nimensä mukaisesti ohjelman käyttämän ikkunan, ja se esitellään seuraavaksi. Sitä ennen tutustutaan kuitenkin yllä olevan koodin while-silmukkaan. Kyseessä on ensimmäinen osa Windowsin viestienkäsittelystä. Ennen while-silmukkaa luodaan viesti-niminen muuttuja, joka on tyypiltään MSG (lyhennys engl. sanasta Message, viesti). Yllä esittellään nk. idle-silmukka, jossa ohjelmakoodia suoritetaan aina kun viestejä ei käsitellä. Ohjelma on Windowsin näkökulmasta toimeton (eli englanniksi idle) aina, kun viestejä ei käsitellä.

Aivan ensimmäiseksi while-lauseessa tarkistetaan viestin sisältö. Jos viestin arvoksi on saatu WM_QUIT, ohjelman tulee sammuttaa itsensä. Jos viestin arvo on jokin muu kurkataan (engl. peek, kurkistaa) ikkunan viestijonoa ja hankitaan viesti-muuttujaan uusi sisältö PeekMessage()- funktiolla. PeekMessage() ottaa parametreinaan täytettävän viestin osoittimen, tarkistettavan ikkunan kahvan, alimman tarkistettavan viestin arvon, ylimmän tarkistettavan viestin arvon, sekä toiminnon mikä viestille tehdään kun se on tarkistettu. Nyt toisena parametrina on NULL, joka kertoo että haluamme viesti-muuttujaan arvon miltä tahansa ohjelman ikkunalta. Jos laittaisimme siihen alempana olevassa LuoIkkuna()-funktiossa saamamme ikkunan kahvan, tarkistettavaksi saataisiin vain sen ikkunan viestit. Kolmas ja neljäs parametri sallivat viestien suodattamisen, johon ei nyt tarkemmin tutustuta. Viides parametri (PM_REMOVE) ilmoittaa että haluamme poistaa viestin ohjelman viestijonosta sen jälkeen, kun olemme siirtäneet sen viesti-muuttujaan.

Jos PeekMessage()-funktio kertoo että viestejä on saatu, käytämme TranslateMessage()-funktiota kääntämään virtual-key -komennot (ohjelmaan määritellyt erikoisnäppäinkomennot, esim. ctrl+F1) kirjainmuotoiseksi viestiksi. Tämän jälkeen DispatchMessage()-funktio lähettää käännetyn viestin sopivalle viestinkäsittelijäfunktiolle.

Nyt teemme funktion, joka varsinaisesti luo ikkunan. Funktio ottaa parametreinaan kahvan ohjelman ilmentymään sekä tiedon ohjelman tilasta (esim. minimoitu).

//-------------------------------------------- 
// Nimi  : LuoIkkuna() 
// Kuvaus: Luo ikkunan. Palauttaa S_OK 
//         jos siinä onnistuttiin, FALSE 
//         muulloin. 
//-------------------------------------------- 
HRESULT LuoIkkuna( HINSTANCE hInstance, 
                   INT nCmdShow ) 
{ 
    HWND hWnd;   // WindowHandle, eli kahva ikkunan ilmentymään 
    WNDCLASS wc; // Ikkunaluokka 
    // 
    // Täytetään ikkunaluokan tiedot. 
    // 
    wc.style       = CS_HREDRAW | CS_VREDRAW;
    wc.lpfnWndProc = ViestienKasittelija;
    wc.cbClsExtra  = 0;
    wc.cbWndExtra  = 0;
    wc.hInstance   = hInstance;
    wc.hIcon       = LoadIcon((HINSTANCE) NULL,IDI_APPLICATION); 
    wc.hCursor     = LoadCursor((HINSTANCE) NULL,IDC_ARROW);
    wc.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
    wc.lpszMenuName  = NULL;
    wc.lpszClassName = "IkkunaLuokanNimi";

    // 
    // Yritetään rekisteröidä ikkunaluokka. 
    //
    if (!RegisterClass(&wc)) 
        return FALSE; 
     
    //
    // Palautetaan onnistumisen merkiksi S_OK.
    // 
    return S_OK; 
}  


Tässä kohtaa on syytä pysähtyä hetkeksi ja tarkastella lähemmin mitä saatiin aikaiseksi. Ylläoleva koodi tekee ensin muuttujan wc, joka kuvastaa ikkunaluokkaa. Se siis kertoo Windowsille millaisen ikkunan tulemme piakkoin luomaan. Tarkastellaan wc:n jäsenmuuttujia hieman tarkemmin:

[DEFTABLE]
[D]wc.Style[/D]
[E]Style määrittää ikkunaluokan tyylin. Arvot CS_VREDRAW ja CS_HREDRAW kertovat, että haluamme koko ikkunan päivittyvän kun sitä liikutellaan tai kun sen kokoa muutellaan. Tyylimuuttujia on Windowsissa lukematon määrä, ja niihin voi jokainen tutustua omalla ajallaan.[/E]

[D]wc.lpfnWndProc[/D]
[E]Osoitin ikkunaluokan viestienkäsittelijä-funktioon.[/E]

[D]wc.cbClsExtra[/D]
[E]Määrittää ikkunaluokan jälkeen varattavien lisätavujen määrän. Useimmiten arvo on 0.[/E]

[D]wc.cbWndExtra[/D]
[E]Määrittää ikkunan ilmentymän jälkeen varattavien lisätavujen määrän Useimmiten 0.[/E]

[D]wc.hInstance[/D]
[E]Kahva ikkunan omistavan ohjelman ilmentymään (HINSTANCE).[/E]

[D]wc.hIcon[/D]
[E]Kahva ikkunaluokan käyttämään ikoniin. LoadIcon()-funktio lataa nyt Windowsin perusikonin.[/E]

[D]wc.hCursor[/D]
[E]Kahva ikkunaluokan käyttämään kursoriin. Taas kerran käytetään Windowsin valmiskursoria LoadCursor()-funktion avulla.[/E]

[D]wc.hbrBackground[/D]
[E]Kahva ikkunaluokaan tausta siveltimeen. Windows käyttää eri kyniä, siveltimiä, fontteja ja paletteja (pen, brush, font ja palette) ikkunoiden piirtelyyn. Tähän aiheeseen ei nyt tarkemmin puututa. GetStockObject()-funktiolla ladataan vain valmiiksi määritelty väri, tässä tapauksessa valkoinen.[/E]

[D]wc.lpszMenuName[/D]
[E]Osoitin merkkijonoon joka ilmoittaa ikkunaluokan valikon nimen (valikko täytyy olla resurssina sillä nimellä).[/E]

[D]wc.lpszClassName[/D]
[E]Ikkunaluokan nimi.[/E]
[/DEFTABLE]

Kun sopivat arvot ylläoleviin muuttujiin on asetettu, luokka rekisteröidään Windowsiin RegisterClass()-funktiolla. Jos rekisteröinti onnistuu, voi wc:ssä kuvaamasi ikkunan luoda. Jos ei, on parempi lopettaa ohjelman ajaminen samantien. Ikkunaa ei voi luoda jos sitä ei voi kuvailla.

Lisätään aiemmin esiteltyyn koodiin vielä pari kohtaa, jotka viimeistelevät ikkunan luomisen.

//-------------------------------------------- 
// Nimi  : LuoIkkuna() 
// Kuvaus: Luo ikkunan. Palauttaa S_OK 
//         jos siinä onnistuttiin, FALSE 
//         muulloin. 
//-------------------------------------------- 
HRESULT LuoIkkuna( HINSTANCE hInstance, 
                   INT nCmdShow ) 
{ 
    HWND hWnd;   // WindowHandle, eli kahva ikkunan ilmentymään 
    WNDCLASS wc; // Ikkunaluokka 
    // 
    // Täytetään ikkunaluokan tiedot. 
    // 
    wc.style       = CS_HREDRAW | CS_VREDRAW;
    wc.lpfnWndProc = WndProc;
    wc.cbClsExtra  = 0;
    wc.cbWndExtra  = 0;
    wc.hInstance   = hInstance;
    wc.hIcon       = LoadIcon((HINSTANCE) NULL,IDI_APPLICATION); 
    wc.hCursor     = LoadCursor((HINSTANCE) NULL,IDC_ARROW);
    wc.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
    wc.lpszMenuName  = NULL;
    wc.lpszClassName = "IkkunaLuokanNimi"; 
    // 
    // Yritetään rekisteröidä ikkunaluokka. 
    //
    if (!RegisterClass(&wc)) 
        return FALSE;  
    // 
    // Yritetään luoda ikkuna. 
    // 
    hWnd = CreateWindow( "IkkunaLuokanNimi", 
                         "Esimerkkiohjelma", 
                         WS_OVERLAPPEDWINDOW,
                         CW_USEDEFAULT, 
                         CW_USEDEFAULT, 
                         CW_USEDEFAULT,
                         CW_USEDEFAULT, 
                         (HWND)  NULL, 
                         (HMENU) NULL,
                         hInstance, 
                         (LPVOID) NULL ); 
    // 
    // Jos kahvaa ikkunan ilmentymään ei saada, 
    // ikkunan luonti epäonnistui. Palautetaan siis 
    // FALSE. 
    //
    if( !hWnd ) 
        return FALSE;

    // 
    // Näytetään ikkuna ja päivitetään sen sisältö. 
    //
    ShowWindow( hWnd, nCmdShow);
    UpdateWindow( hWnd );
     
    //
    // Palautetaan onnistumisen merkiksi S_OK.
    // 
    return S_OK; 
} 


Varsinainen ikkuna luodaan CreateWindow() -funktiolla, joka ottaa seuraavat parametrit:

[DEFTABLE]
[D]lpClassName[/D]
[E]Ikkunaluokan nimi. Tässä käytettävä ikkunaluokka on oltava rekisteröity.[/E]

[D]lpWindowName[/D]
[E]Ikkunan otsikko. Näkyy luotavan ikkunan vasemmassa yläreunassa, sekä tehtäväpalkissa.[/E]

[D]dwStyle[/D]
[E]Ikkunan tyyppi. Nyt tyyliksi määritetään WS_OVERLAPPEDWINDOW, joka luo ikkunan jonka kokoa voi muuttaa ja jolla on otsikkorivi jossa on ikkuna-valikko (eli valikko joka ilmaantuu ikkunan ikonia painettaessa).[/E]

[D]x[/D]
[E]Ikkunan paikka X-akselilla. Nyt käytämme Windowsin perusarvoja.[/E]

[D]y[/D]
[E]Ikkunan paikka Y-akselilla.[/E]

[D]nWidth[/D]
[E]Ikkunan leveys.[/E]

[D]nHeight[/D]
[E]Ikkunan korkeus.[/E]

[D]hWndParent[/D]
[E]Kahva ikkunan isäntä-ikkunaan. Tämä on nyt NULL, koska luomme vain yhden ikkunan.[/E]

[D]hMenu[/D]
[E]Kahva ikkunan valikkoon. Valikot luodaan ikkunan resurssi-tiedostoon. Taas kerran arvoksi asetetaan nyt NULL, koska emme tarvitse valikkoa.[/E]

[D]hInstance[/D]
[E]Kahva ikkunaa käyttävän ohjelman ilmentymään (hInstance).[/E]

[D]lpParam[/D]
[E]Mahdollinen osoitin ikkunan tarvitsemaan lisätietoon. Tätä käytetään MDI-ikkunoita (Multiple Document Interface, Usean Asiakirjan Liittymä, eli monta ikkunaa yhden ison ikkunan sisällä) käytettäessä, joten se on nyt NULL.[/E]
[/DEFTABLE]

Viestien käsittelyä #

Viestien käsittely on yksi Windows-ohjelmoinnin olennaisimpia osia. Windows-ohjelmat joutuvat käsittelemään viestejä aina kun jotakin ohjelman nappia tai muuta osaa painetaan tai muokataan. Viestien avulla selvitetään esimerkiksi vierityspalkkien liikkeet. Ruudulla näkyvän graafisen osan alla on siis todellinen viestien sekamelska, jota ohjelmat tulkitsevat viestienkäsittelyfunktoillaan.

Käsittelijäfunktio #

Alla oleva koodi on koko viestienkäsittelyfunktio mitä tässä esimerkissä käytämme. Kaikessa yksinkertaisuudessaan se käsittelee vain ikkunan sulkemisviestin.

//-------------------------------------------- 
// Nimi  : ViestienKasittelija() 
// Kuvaus: Funktio joka käsittelee ikkunan
//         viestit. 
//--------------------------------------------  
LRESULT CALLBACK ViestienKasittelija(HWND   hWnd, 
                                     UINT   Viesti,
                                     WPARAM wParam,
                                     LPARAM lParam)
{
    //
    // Tutkitaan mikä viesti saatiin ja tehdään
    // jotain sen mukaisesti.
    //
    switch ( Viesti ){
       case WM_DESTROY:
          {
              //
              // Tuhoamisviesti. Lähetetään
              // Lopetusviesti ja palautetaan
              // TRUE, eli "viesti käsitelty".
              // 
              PostQuitMessage( 0 );
              returnTRUE;
          }
       break;
    }
 
    //
    // Tuntematon viesti. Annetaan windowsin
    // perusviestinkäsittelijän käsitellä viesti.
    //
    return DefWindowProc( hWnd, Viesti, wParam, lParam );
} 


Yllä oleva funktio toimii käytännössä siten, että ensin katsotaan viestit joista ollaan kiinnostuneita. Jos yhtään kiinnostavaa viestiä ei ole saapunut, annetaan Windowsissa valmiina olevan viestinkäsittelijän käsitellä viesti.

Loppusanat #

Nyt, kun olet lukenut artikkelin, hallitset pienimuotoisen Windows-sovelluksen tekemisen. Kovin paljoa tätä enempää ei Windows-ohjelmointi osaamista DirectX-ohjelmointiin vaadita. Jotta tiedot kuitenkin painuisivat tarkemmin muistiin on syytä muokkailla annettua esimerkkiohjelmaa. Kuten huomasitkin, kaikissa tapauksissa ei tarkisteta virhetilanteita. Harjoitukseksi jääkin kyseisten tilanteiden etsiminen ja niistä selviytyminen.

Kiitokset Risto Karjalaiselle sekä Jussi Huhtiniemelle avusta artikkelin teossa.