Wstęp do programowania w WinAPI

Adam Boduch

Pełna nazwa skrótu WinAPI to Windows Application Programming Interface. Temu właśnie poświęcony będzie ten rozdział. Od razu chcę zaznaczyć, że ta technika to dla jednych kolejny stopień nauki Delphi, a dla innych początek przygody z Delphi. Tak, tak - niektórzy przyzwyczajeni do Turbo Pascala gubią się trochę w korzystaniu z komponentów, klas itp. Wolą raczej zaczynać od "czystego" programowania bez użycia klas, z podstawowymi modułami tak jak to ma miejsce w Turbo Pascalu. Dla Ciebie to jednak będzie trudny etap nauki - jesteś bowiem przyzwyczajony do używania wygodnych klas, komponentów i formularzy. Nie musisz za bardzo martwić się o takie rzeczy jak inicjalizacja formularza, obsługa formularz itp. Ale do czego zmierzam? Możesz przecież powiedzieć: "na co potrzebna mi jest taka wiedza?". Przede wszystkim wielu rzeczy nie da się zrobić za pomocą VCL, gdyż nie jest ona tak elastyczna. Drugi powód: jeżeli chcesz pisać szybkie programy, małych rozmiarów to wiedza ta będzie Ci potrzebna. Bo co decyduje o rozmiarze pliku EXE? Delphi dołącza do niego niezbędne biblioteki i informacje takie jak kod VCL i niezbędne biblioteki. I to nie tylko to, gdyż dołącza także przeważnie niepotrzebne śmieci jak kursory, ikony i komunikaty. Pisząc program bez wykorzystania formularzy i komponentów możesz osiągnąć rozmiar pliku rzędu kilku kB! Czyż to nie jest mało w porównaniu z tym co zawiera "czysty" projekt - formularz (ponad 300 kb!).

Funkcje API ( bo tak też się o nich mówi ) są zawarte w bibliotekach DLL Windowsa takich jak kernel32.dll, user32.dll i wielu, wielu innych. Moduł Windows.pas jest podstawowym modułem, który będziemy wykorzystywać. Jeżeli otworzysz ten plik to zobaczysz, że zawiera on np. definicję bardzo wielu funkcji i procedur, które są importowane z bibliotek DLL właśnie.

Pierwszy program w API

No, trochę skłamałem bo nie jest to zupełnie nasz pierwszy program. Pamiętasz pewnie, że w drugim rozdziale omawiając podstawę języka Object Pascal także nie stosowaliśmy żadnych formularzy, czy komponentów - tak dla ułatwienia. Wtedy jednak nie było to nic nadzwyczajnego. Przy pomocy WinAPI można także tworzyć formularze, komponenty i inne obiekty. Tym zajmiemy się w dalszej części tego rozdziału - teraz coś na przypomnienie, czyli najprostszy program napisany w API. Z menu File wybierz New Application. Zamknij jednak edytor kodu i formularz. Na pytanie, czy zapisać formularz odpowiedz przecząco naciskając na przycisk "No". Teraz z menu Project wybierz opcję View Source. Mamy otwarty projekt DPR, który możesz już zapisać. Moduł oraz kod zapisany w sekcji begin..end nie będzie nam potrzebny więc możesz go usunąć. Do sekcji uses dodaj natomiast moduł Windows. Stworzymy także plik z zasobami. W zasobach umieść ikonę - zakładam, że po lekturze poprzednich rozdziałów wiesz jak to zrobić. Narysuj jakąś ikonkę, która będzie ozdabiała nasz plik EXE. Kod źródłowy naszego programu będzie wyglądał tak:
program Hello;

uses
  Windows;

  {$R IKONA.RES}

begin

end.

No, w sumie na razie jest to pusty projekt z włączonymi jedynie zasobami. Jak już wiesz właściwy kod programu należy umieścić w sekcji begin..end. Po uruchomieniu programu kod z tej właśnie sekcji jest odczytywany. Teraz możesz już skompilować program - u mnie na Delphi 6 PE zajmuje on jedynie 9 kB!

Tworzenie formularza

Od razu mówię, że z tworzeniem i wyświetleniem formularza w API wiążę się kilka wątków. Po pierwsze trzeba stworzyć formularz. To akurat nie powinno przysporzyć nam większych problemów - dzieje się to za sprawą funkcji CreateWindow. Będzie to zaledwie jedna linia kodu. Druga sprawa to rejestracja klasy formularza. Podczas rejestracji klasy trzeba będzie przypisać funkcję okienkową formularza. Inaczej mówiąc funkcja taka będzie obsługiwała wszystkie komunikaty jakie dotrą do okienka. Przykład: program działa - okienko jest pokazane. Teraz użytkownik chce zamknąć program naciskając na przycisk "krzyżyka" na pasku okienka. W tym momencie do okna jest kierowany komunikat WM_DESTROY, który MY będziemy musieli obsłużyć. I tak jest ze wszystkim w API - każdy komunikat trzeba obsłużyć z osobna - nie jest to takie proste jak w przypadku VCL, gdzie wiele rzeczy jest robione za nas.

Funkcja okienkowa

Może zaczniemy od funkcji okienkowej. Zadeklaruj przed blokiem begin..end funkcję, która musi przedstawiać się tak:
function WndProc(Wnd: HWND; uMsg: UINT; wPar: WPARAM; lPar: LPARAM): LRESULT; stdcall;
begin 

end;

Napisałem, że musi przedstawiać się w ten sposób, ale chodzi mi tutaj wyłącznie o parametry zawarte w tej funkcji. Do tej funkcji przekazywane są wszelkie komunikaty Windows kierowane do okna naszej aplikacji. Pierwszy parametr stanowi oczywiście uchwyt okna. Kolejny parametr to nazwa komunikatu jaki otrzymuje nazwa okienkowa. Kolejne dwa parametry, czyli wPar oraz lPar to dodatkowe parametry jakie są przekazywane przez komunikat.

Na tym etapie prac będziesz musiał do listy uses dodać moduł Messages.pas. Jest to wymagane ponieważ moduł ten zawiera deklaracje wszystkich komunikatów.

Jest jeszcze jedna bardzo ważna rzecz. Otóż funkcja ta musi zwrócić wartość 0 w przypadku przetwarzania meldunku (komunikatu) - jeżeli meldunku nie przetwarza musi zwrócić rezultat wykonania funkcji DefWindowProc. W praktyce kod powyższej funkcji należy zmodyfikować tak:

function WndProc(Wnd: HWND; uMsg: UINT; wPar: WPARAM; lPar: LPARAM): LRESULT; stdcall;
begin
{ na początek zwracamy wartość 0 - meldunek jest przetwarzany }
  Result := 0;
  case uMsg of
   { tutaj, w tym miejscu należy obsłużyć należne komunikaty }
   { w funkcji DefWindowProc przekazujemy takie same parametry jak w funkcji okienkowej }
    else Result := DefWindowProc(Wnd, uMsg, wPar, lPar);
  end;
end;

W instrukcji case należy obsłużyć komunikaty, które obsłużyć i zareagować chcemy. Taki kod się jeszcze nie skompiluje gdyż w instrukcji case trzeba umieścić jakieś komunikaty do obsłużenia. Przykładowo: koniecznie trzeba obsłużyć komunikat WM_DESTROY. Komunikat ten przekazywany jest do funkcji okienkowej w przypadku, gdy użytkownik nacisnął przycisk zamykający formularz. W tym momencie należy zakończyć program, a można to zrealizować wykonując funkcję PostQuitMessage która nie robi nic innego jak wysłanie komunikatu WM_QUIT. Czyli na sam początek w wersji finalnej nasza funkcja okienkowa może wyglądać tak:

function WndProc(Wnd: HWND; uMsg: UINT; wPar: WPARAM; lPar: LPARAM): LRESULT; stdcall;
begin
{ na początek zwracamy wartość 0 - meldunek jest przetwarzany }
  Result := 0;
  case uMsg of
   { tutaj, w tym miejscu należy obsłużyć należne komunikaty }
   { w funkcji DefWindowProc przekazujemy takie same parametry jak w funkcji okienkowej }
    WM_DESTROY: PostQuitMessage(0);
    else Result := DefWindowProc(Wnd, uMsg, wPar, lPar);
  end;
end;

Rejestracja klasy

Drugim krokiem jaki musimy przedsięwziąć jest rejestracja klasy okna głównego. Ażeby to wykonać trzeba będzie wypełnić pola rekordu TWndClass. Trzeba będzie gdyż rekord taki trzeba przekazać procedurze RegisterClass. Zacznijmy więc. Te część kodu trzeba będzie wpisać w sekcji begin..end:
var
  Wnd: TWndClass;  // klasa okna

begin
  with Wnd do
  begin
    lpfnWndProc := @WndProc; // funkcja okienkowa
    hInstance := hInstance; // uchwyt do zasobów
    lpszClassName := 'My1stApp'; // klasa
    hbrBackground := COLOR_WINDOW; // kolor tła
  end;

  RegisterClass(Wnd); // zarejestruj nowa klase

end.

Na samym początku korzystając z instrukcji wiążącej with wypełniam pola rekordu. I tak pierwszy element tego rekordu to adres funkcji okienkowej - trzeba tutaj przypisać nazwę funkcji stosując operator @. Kolejny element to uchwyt do zasobów. W tym miejscu wpisujemy globalną zmienną wskazującą na naszą aplikację. Kolejny element to nazwa nowej klasy. Ostatni to kolor tła. Może w tym miejscu być podstawiony jeden z tych wartości: COLOR_ACTIVEBORDER
COLOR_ACTIVECAPTION, COLOR_APPWORKSPACE, COLOR_BACKGROUND, COLOR_BTNFACE, COLOR_BTNSHADOW, COLOR_BTNTEXT, COLOR_CAPTIONTEXT, COLOR_GRAYTEXT, COLOR_HIGHLIGHT, COLOR_HIGHLIGHTTEXT, COLOR_INACTIVEBORDER, COLOR_INACTIVECAPTION, COLOR_MENU, COLOR_MENUTEXT, COLOR_SCROLLBAR, COLOR_WINDOW, COLOR_WINDOWFRAME, COLOR_WINDOWTEXT.

To co zostało zadeklarowane w rekordzie to nie wszystko co jest możliwe. Otóż ten rekord posiada jeszcze kilka innych elementów, których w tym momencie nie zadeklarowaliśmy. Jest to np. hIcon. Możesz tutaj przypisać uchwyt do ikonki, która będzie ozdabiać aplikację podczas gdy użytkownik naciśnie klawisze Al+Tab. Ikonę taką oczywiście odczytujemy z zasobów przy pomocy funkcji LoadIcon, ale to już przerabialiśmy wcześniej. Oprócz tego możesz w tym miejscu przypisać nazwę do standardowych ikon Windows, czyli: IDI_APPLICATION, IDI_HAND (IDI_ERROR), IDI_QUESTION, IDI_EXCLAMATION (IDI_WARNING), IDI_ASTERISK (IDI_INNFORMATION), IDI_WINLOGO.

Istnieje możliwość przypisania także kursora, który będzie używany przez naszą aplikację. Element hCursor rekordu TWinClass za to właśnie odpowiada. Kursor możesz załadować z zasobów lub podając w to miejsce jedno z podanych wartości: IDC_ARROW, IDC_IBEAM, IDC_WAIT, IDC_CROSS, IDC_UPARROW, IDC_SIZE, IDC_ICON, IDC_SIZENWSE, IDC_SIZENESW, IDC_SIZEWE, IDC_SIZENS, IDC_SIZEALL, IDC_NO, IDC_APPSTARTING, IDC_HELP.
Aby dopełnić procesu rejestracji klasy należy wywołać procedurę RegisterClass podając jako parametr nazwę rekordu typu TWinClass.

Tworzenie formy

Jak już mówiłem wcześniej za to odpowiada funkcja CreateWindow. Użycie tej funkcji do utworzenie formularza przedstawia się tak:
// stwórz formę...
  CreateWindow('My1stApp', 'Program pierwszy w WinAPI',
	WS_VISIBLE or WS_TILEDWINDOW,
        20, 20, 320, 120,
	0, 0, hInstance, NIL);

Już objaśniam, co oznaczają poszczególne parametry. Pierwszy parametr to nazwa klasy formularza.

Uwaga! W tym momencie trzeba wpisać tę samą nazwę, co podczas rejestracji samej klasy (patrz: poprzedni punkt).

Drugi parametr to nazwa (Caption) formularza, czyli tekst, który pojawi się na pasku tytułowym formy. Trzeci parametr to styl okna. Zaczynają się one od przedrostka WS_ (Window Style) i mogą być mieszane operatorem or. Styli tych jest dużo, a oto niektóre z nich:
WS_OVERLAPPED - okno główne, stanowiące tło dla innych okien
WS_POPUP - okno nakładane, np. dialogowe
WS_CHILD - okno potomne, które nie może wyjść poza okno rodzicielskie
WS_CAPTION - okno ma pasek tytułu
WS_SYSMENU - okno ma menu systemowe
WS_MINIMIZEBOX - okno ma przycisk zwijania do paska zadań
WS_MAXIMIZEBOX - okno ma przycisk rozwijania na cały ekran
WS_VISIBLE - okno jest widoczne
WS_HIDE - okno ukryte, niewidoczne
WS_DISABLED - okno nieaktywne, nie reaguje na nic tzn. nie otrzymuje meldunków
WS_BORDER - oko posiada ramkę
WS_THICKFRAME - gruba ramka pozwalająca myszka zmieniać rozmiary
WS_VSCROLL, WS_HSCROLL - okno posiada paski przewijania: pionowy, poziomy
itd. itd. ... o innych parametrach możesz poczytać w systemie pomocy.

Parametry WS_VISIBLE oznaczają, że okno będzie widoczne, a WS_TITLEDWINDOW, że będzie miało pasek tytułowy. Co oznaczają kolejne? Są to rozmiary początkowe okna. Pierwsze dwa parametry to położenie X, Y na ekranie. Kolejne dwa to szerokość i wysokość okna. Podstawienie w to miejsce parametru CW_USEDEFAULT oznacza iż Windows sam zadecyduje o położeniu i wymiarach okna. Kolejny parametr oznacza uchwyt okna - rodzica. Wiadomo bowiem iż w programie może być kilka okien i jedno z nich może być oknem Child dla innego. W naszym wypadku to pole pozostawiliśmy puste wstawiając cyfrę 0. Następny parametr oznacza uchwyt menu - nie mamy w programie menu więc pozostawiamy ten parametr pusty. Przed ostatni parametr instancje programu - wstawiamy w to miejsce słowo kluczowe hInstance oznaczające iż zasoby będą pobierane z tej właśnie aplikacji EXE. Ostatni parametr określa dodatkowe dane, ale tym na razie nie będziemy się zajmować - wstawiamy w to miejsce słowo nil.

Myślę, że można już przedstawić cały kod źródłowy programu:

{
   Copyright (c) 2002 by Adam Boduch
}

program WndApp;

uses
  Windows,
  Messages;


function WndProc(Wnd: HWND; uMsg: UINT; wPar: WPARAM; lPar: LPARAM): LRESULT; stdcall;
begin
{ na początek zwracamy wartość 0 - meldunek jest przetwarzany }
  Result := 0;
  case uMsg of
   { tutaj, w tym miejscu należy obsłużyć należne komunikaty }
   { w funkcji DefWindowProc przekazujemy takie same parametry jak w funkcji okienkowej }
    WM_DESTROY: PostQuitMessage(0);
    else Result := DefWindowProc(Wnd, uMsg, wPar, lPar);
  end;
end;

var
  Wnd: TWndClass;  // klasa okna
  Msg: TMsg;

begin
  with Wnd do
  begin
    lpfnWndProc := @WndProc; // funkcja okienkowa
    hInstance := hInstance; // uchwyt do zasobów
    lpszClassName := 'My1stApp'; // klasa
    hbrBackground := COLOR_WINDOW; // kolor tła
  end;

  RegisterClass(Wnd); // zarejestruj nowa klasę

// stwórz formę...
  CreateWindow('My1stApp', 'Program pierwszy w WinAPI',
	WS_VISIBLE or WS_TILEDWINDOW,
        CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
	0, 0, hInstance, NIL);

  while GetMessage(msg, 0, 0, 0) do DispatchMessage(msg);
end.

Ten kod po kompilacji powinien zadziałać i w jego efekcie na ekranie powinna pojawić się nowa forma. To czego jeszcze nie omawiałem to ostatnie instrukcje tego listingu. Są one bardzo ważne i bez tego program się nie uruchomi. Te operacje trzeba po prostu wykonać, aby funkcja okienkowa otrzymała potrzebne meldunki. Podczas uruchamiania program musi wejść w tzw. fazę meldunków. Funkcja GetMessage pobiera kolejno meldunki wpisując je do struktury TMsg (parametry są nieistotne), a następnie przekazuje funkcji DispatchMessage, która to z kolei przekazuje meldunek do funkcji okienkowej.

Komunikaty i uchwyty

Podczas tego rozdziału będziemy korzystać wyłącznie z funkcji SendMessage/PostMessage. Jak już mówiłem podczas omawiania komunikatów są to funkcje API i tylko z nich możemy korzystać. Wszystkie komunikaty będziemy odbierać i reagować w funkcji okienkowej. Małe przypomnienie - komunikaty mogą pochodzić z:
  • komunikaty klawiaturowe (użytkownik nacisnął/zwolnił) klawisz,
  • komunikaty myszy (użytkownik wykonał jakąś czynność myszą),
  • komunikaty od zegara oznaczające upływ określonego odcinka czasu,
  • komunikaty od systemu - tworzenie okna , zmiana rozmiaru i położenia okna, zwijanie i rozwijanie okna, zmiana kolorów systemowych itp.
  • komunikaty wewnętrzne wysyłane przez inne okna utworzone w naszym programie;

W każdym komunikacie będzie trzeba podać uchwyt okna docelowego (lub kontrolki docelowej). Uchwyt jest liczbą 32-bitową która identyfikuje kontrolkę w systemie Windows. To właśnie Windows przydziela uchwyty różnym kontrolką. Typ reprezentujący uchwyt zaczyna się od litery 'H', czyli np. HWND, HBRUSH, HFONT, HBITMAP. Po tym łatwo rozpoznać, czy dana zmienna jest uchwytem, czy też nie.

A co z łańcuchami danych?

Przyzwyczajaj się do tego iż pisząc programy w API będziemy używać raczej typu PChar. Jest to typ wskazujący na łańcuch danych zakończonych zerem. Wyróżnia się tym, że można na nim dokonywać operacji matematycznych. Obie zmienne P1 i P2 są typu PChar - teraz zobacz na kod:
  P2 := 'To jest Delphi';
  P1 := P2 + 8;

Mogłoby to wyglądać tak, że dodajemy do zmiennej operującej na tekście cyfrę 8. W rzeczywistości kompilator interpretuje to tak iż do zmiennej P1 jest dodawana wartość P2, tyle, że bez pierwszych 8 znaków. Podczas lektury dalszej części książki możesz napotkać się także na deklaracje zmiennej w postaci tablicy:

  Variable : array[0..255] of char;

Następnie do takiej zmiennej dane można przypisać w zwykły sposób, tyle, że będą one ograniczone 255 znakami.

  Variable := 'Adam Boduch';

Natomiast deklaracja Stringa powoduje iż kompilator przydziela pamięć dla takiej zmiennej w sposób dynamiczny. Stosowanie typu PChar jest szybsze, mniej pamięcio-żerne i mniej obciąża program. Przeprowadź test. W programie zastosuj zmienną PChar i przypisz jej jakieś dane. Po kompilacji programu zajmuje on ok. 8 kB. Podczas, gdy zamiast typu PChar zastosujesz String ten rozmiar wzrasta do 14 kB! Ew. jeżeli uprzesz się na używanie Stringa możesz w programie określić jego długość:

SetLength(S, 255);

Oczywiście S to nazwa zmiennej wskazującej na Stringa, a drugi parametr to długość tej zmiennej.

Podczas czytania tego może nasuwać się jeszcze jedno pytanie - a co z konwersją? Jak realizować konwersję pomiędzy poszczególnymi typami danych nie stosując modułu SysUtils, w którym zawarte są funkcje konwersji? Do tego użyjemy funkcji WinAPI - wvsprintf. Jest ona podobna do funkcji Format, a o niej akurat mówiłem we wcześniejszych rozdziałach więc nie powinno być z tym problemu. Pierwszym parametrem tej funkcji musi być zmienna, która będzie zawierała tekst wyjściowy po konwersji. Drugim parametrem jest tekst wejściowy ( input ), który może zawierać takie symbole jak w przypadku funkcji Format, czyli np. %d. W takim wypadku kompilator wie iż w tym miejscu ma się znaleźć liczba. Ostatnim parametrem są dane do konwersji - oto przykład całej funkcji:

var
  Buffer : array[0..128] of char;
  Format : Integer;
begin
  Format := 11;
  wvsprintf(Buffer, 'Witaj w %d części kursu!', @Format);
  MessageBox(0, Buffer, '', 0);
end.

Po takich operacjach zmienna Buffer będzie zawierać przekonwertowany tekst, który można wyświetlić np. w okienku MessageBox(). W funkcji wvsprintf w drugim parametrze znajduje się znak %d, czyli w to miejsce wstawiona zostanie liczba ze zmiennej Format. Możliwe jest bardziej skomplikowana konwersja kilku typów zmiennych:

var
  Buffer : array[0..255] of char;
  Format : packed record  // deklaracja rekordu danych do konwersji
    Int : Integer;
    Fl : String;
  end;
begin
{ wypełnienie danych do konwersji }
  Format.Int := 11;
  Format.Fl := 'Adam Boduch';

  wvsprintf(Buffer, 'Witaj w %d części kursu, ja nazywam się %s!', @Format);
  MessageBox(0, Buffer, '', 0);
end.

W tym wypadku konwertowany jest cały rekord, a konkretnie pola z tego rekordu. Na podstawie zdobytej tu wiedzy można napisać odpowiednik funkcji IntToStr w API:

{
  Copyright (c) 2002 by Adam Boduch
}

uses Windows;

function IntToStr(Value : Integer) : String;
var
  Buffer : array[0..255] of char;  // bufor, w którym przechowywać będziemy dane
begin
  wvsprintf(Buffer, '%d', @Value); // tu następuje funkcja konwersji
  Result := Buffer; // zwracamy rezultat
end;

begin
  MessageBox(0, PChar('Witaj w ' + IntToStr(11) + ' części kursu!'), 'Witaj!', MB_OK); // wyświetl wartość zmiennej
end.

Umieszczanie komponentów na formie

Nie będzie to trudne. Tworzenie zarówno formularzy jak i komponentów odbywa się za pośrednictwem funkcji CreateWindow. W przypadku komponentów nie będzie konieczna rejestracja nowych klas itp. rzeczy. Tworzenie nowego komponentu odbędzie się za pomocą jednej linii. Komponent trzeba utworzyć z flagą WS_CHILD oraz WS_VISIBLE. Stworzenie przycisku będzie więc odbywało w ten sposób:
CreateWindow('BUTTON', 'Przycisk', WS_CHILD or WS_VISIBLE, 100, 100, 120, 25, Wnd, 0, hInstance, nil); 

Jedyną charakterystyczną cechą jest pierwszy parametr tej funkcji. Jeżeli chcesz stworzyć przycisk musisz w to miejsce wpisać słowo 'BUTTON'. Drugi parametr to tekst, który będzie widniał na przycisku. Trzeci parametr to flagi dla komponentu. Dalsza część jest już taka sama jak w przypadku tworzenia formy.

Pamiętaj, aby podczas tworzenia przycisku w parametrze hWndParent (czwarty od końca) podać uchwyt okna głównego. Uwaga! Powyższy kod należy umieścić w funkcji okienkowej!

Jeżeli chcesz, aby przycisk powstał podczas tworzenia się formy musisz obsłużyć komunikat WM_CREATE. Czyli teraz cały kod funkcji okienkowej powinien wyglądać tak:

function WndProc(Wnd: HWND; uMsg: UINT; wPar: WPARAM; lPar: LPARAM): LRESULT; stdcall;
begin
{ na początek zwracamy wartość 0 - meldunek jest przetwarzany }
  Result := 0;
  case uMsg of
    WM_CREATE:
      CreateWindow('BUTTON', 'Przycisk', WS_CHILD or WS_VISIBLE, 100, 100, 120, 25, Wnd, 0, hInstance, nil); 
    WM_DESTROY: PostQuitMessage(0);
    else Result := DefWindowProc(Wnd, uMsg, wPar, lPar);
  end;
end;

Jeżeli chcesz umieścić więcej niż jeden komponent w WM_CREATE musisz wstawić blok begin..end. Przykładowo umieszczanie 5 przycisków w pętli:

{...}
function IntToStr(Value : Integer) : String;
var
  Buffer : array[0..255] of char;  // bufor, w którym przechowywać będziemy dane
begin
  wvsprintf(Buffer, '%d', @Value); // tu następuje funkcja konwersji
  Result := Buffer; // zwracamy rezultat
end;

function WndProc(Wnd: HWND; uMsg: UINT; wPar: WPARAM; lPar: LPARAM): LRESULT; stdcall;
var
  i : Integer;
begin
{ na początek zwracamy wartość 0 - meldunek jest przetwarzany }
  Result := 0;
  case uMsg of
    WM_CREATE:
    begin
      for I := 1 to 5 do
        CreateWindow('BUTTON', PCHar('Przycisk nr: ' + IntToStr(i)), WS_CHILD or WS_VISIBLE, 100, 100 + i * 30, 
        120, 25, Wnd, 0, hInstance, nil);
    end;
    WM_DESTROY: PostQuitMessage(0);
    else Result := DefWindowProc(Wnd, uMsg, wPar, lPar);
  end;
end;
{...}

Mam nadzieje, że do tej pory wszystko jest jasne. Ten kod da taki rezultat jak widać na rysunku 1.

winapi_delphi1.jpg Rysunek 1. Formularz z przyciskami

Jak widzisz kod obsługi komunikatu WM_CREATE został zawarty pomiędzy blok begin..end. W pętli następuje utworzenie pięciu przycisków typu Button. Tworzonym przyciskom można dodatkowo nadawać różne flagi, które mają znaczenie jedynie w połączeniu z danym komponentem - w tym wypadku, z Buttnem. Przykładowo flaga BS_LEFT powoduje iż napisy nie będą scentrowane jak jest domyślnie, lecz będą ułożone po lewej stronie. Tych flag jest dosyć dużo i nie ma sensu ich wszystkich wymieniać. Szczegółowo opisane są one w pomocy Delphi pod hasłem 'CreateWindow'. I tak jest ze wszystkimi komunikatami - można oczywiście oprócz przycisku umieszczać inne kontrolki jak ComboBox, czy Edit i każdy z tych komponentów ma swój zestaw komunikatów wpływających na ich zachowanie, czy wygląd. O tym jaki zostanie stworzony komponent decyduje pierwszy parametr funkcji CreateWindow - inne możliwe do wpisania tam wartości znajdują się w tabeli 1.

NazwaKontrolka
EDITNic innego jak komponent TEdit.
COMBOBOXLista rozwijalna ComboBox.
LISTBOXKomponent TListBox
SCROLLBAR Pasek przewijania
STATIC Etykieta, czyli komponent TLabel.

Nie są to wszystkie możliwe do umieszenia kontrolki - możesz także umieszczać inne jak ProgressBar, czy ListView, ale tym zajmiemy się nieco później. Na razie skup się na tych, które są w tabeli 1.

Obsługa komunikatów dla komponentów

Idziemy dalej. Co w przypadku, gdy chcemy obsłużyć np. naciśnięcie danego przycisku? To także nie będzie trudne. Kontrolce trzeba nadać jakiś unikalny identyfikator, który będzie niepowtarzalny i dzięki niemu będziemy mogli identyfikować kontrolkę. Identyfikator ten nadaje się w funkcji CreateWindow, w parametrze trzecim od końca.
CreateWindow('BUTTON', PCHar('Przycisk nr: ' + IntToStr(i)), WS_CHILD or WS_VISIBLE, 100, 100 + i * 30, 120, 25, Wnd, 100, 
hInstance, nil);

W tym wypadku podczas tworzenia komponentu nadaliśmy mu identyfikator o numerze 100. Teraz w funkcji okienkowej, aby obsłużyć naciskanie tego przycisku będziemy musieli oprogramować komunikat WM_COMMAND. Do funkcji okienkowej identyfikator kontrolki dociera w parametrze wPar. W takim wypadku obsługa tego komunikatu będzie wyglądać tak:

WM_COMMAND:
  if wPar = 100 then MessageBox(Wnd, 'Nacisnołeś!', '', MB_OK);

Czyli mamy tu porównanie, czy parametr wPar zawiera określony identyfikator - jeżeli tak to następuje wyświetlenie okienka. Możesz skompilować program i sprawić, czy działa! Teraz coś trudniejszego - pamiętasz jak w poprzednich przykładach umieszczaliśmy w pętli kilka przycisków jednocześnie? Postaramy się teraz obsłużyć kliknięcie każdego z nich osobno. Tj. po tym jak użytkownik kliknie w któryś przycisk program wyświetli okienko z informacją o numerze klikniętego przycisku. Tak będzie wyglądać jego funkcja okienkowa, bo praktycznie tylko ona w całym programie ulega zmianie:

function WndProc(Wnd: HWND; uMsg: UINT; wPar: WPARAM; lPar: LPARAM): LRESULT; stdcall;
var
  i : Integer;
begin
{ na początek zwracamy wartość 0 - meldunek jest przetwarzany }
  Result := 0;
  case uMsg of
    WM_CREATE:
    begin
    { tutaj w pętli umieszczamy kilka przycisków każdemu z nich nadając kolejny identyfikator poczynając od 100 }
      for I := 1 to 5 do
        CreateWindow('BUTTON', PCHar('Przycisk nr: ' + IntToStr(i)), WS_CHILD or WS_VISIBLE, 100, 100 + i * 30, 120, 25, 
        Wnd, 100 + i, hInstance, nil);
    end;
    WM_COMMAND: // obsługa kliknięcia w przycisk
    begin
      case wPar of // sprawdź, czy w Par jest od 101 do 105
      101..105: MessageBox(Wnd, PChar('Witaj!, nacisnąłeś przycisk nr ' + IntToStr(wPar - 100) + '!'), ':-)', 
      MB_OK + MB_ICONINFORMATION);
      end;
    end;
    WM_DESTROY: PostQuitMessage(0);
    else Result := DefWindowProc(Wnd, uMsg, wPar, lPar);
  end;
end;

Zwróć uwagę na obsługę komunikatu WM_COMMAND. Za pomocą instrukcji case następuje porównanie, czy parametr wPar zawiera wartość od 101 do 105. Wówczas następuje wyświetlenie komunikatu informującego o naciśnięciu któregoś z przycisków. Mam nadzieje, że do tej pory wszystko jest jasne. Napiszmy kolejny program, kolejny przykład. Tym razem z wykorzystaniem uchwytu stworzonej kontrolki. Otóż po wywołaniu funkcji CreateWindow zwraca ona uchwyt utworzonego komponentu. Ten uchwyt można jakoś wykorzystać. Tworzenie kontrolki Edit może wyglądać tak:

Edit := CreateWindow('EDIT', '', WS_CHILD or WS_VISIBLE or WS_BORDER, 10, 10, 100, 25,
      Wnd, 0, hInstance, nil);

W tym wypadku zmienna Edit, typu HWND lub Integer będzie zawierać uchwyt do tej kontrolki. Dodatkowo nadałem jej flagę WS_BORDER, która powoduje iż komponent będzie zawierał obrzeża. Na formularzu stworzymy także przycisk - po jego naciśnięciu program pobierze zawartość Edita i wyświetli ją. Ażeby pobrać tekst z kontrolki potrzebna nam będzie funkcja GetWindowText. Jako pierwszy parametr tej funkcji należy podać uchwyt okna z którego ma być pobrany tekst. Będzie to mniej więcej wyglądać tak:

GetWindowText(Edit, Buffer, SizeOf(Buffer)); // pobierz tekst z edita

Gdzie Buffer to tablica znaków:

var
  Buffer : array[0..128] of char;

Po wywołaniu funkcji GetWindowText zmienna Buffer zawiera test znajdujący się w Edit. W sumie cały kod źródłowy programu wygląda tak:

{
   Copyright (c) 2002 by Adam Boduch
}

program PMsg;

uses
  Windows,
  Messages;

var
  Edit : HWND;

function WndProc(Wnd: HWND; uMsg: UINT; wPar: WPARAM; lPar: LPARAM): LRESULT; stdcall;
var
  Buffer : array[0..128] of char;
begin
{ na początek zwracamy wartość 0 - meldunek jest przetwarzany }
  Result := 0;
  case uMsg of
    WM_CREATE:
    begin
      Edit := CreateWindow('EDIT', '', WS_CHILD or WS_VISIBLE or WS_BORDER, 10, 10, 100, 25,
      Wnd, 0, hInstance, nil);
      CreateWindow('BUTTON', 'OK', WS_CHILD or WS_VISIBLE, 150, 10, 120, 25, Wnd, 101, hInstance, nil);
    end;
    WM_COMMAND:
      if wPar = 101 then
      begin
        GetWindowText(Edit, Buffer, SizeOf(Buffer)); // pobierz tekst z edita
        MessageBox(Wnd, Buffer, 'EDIT', MB_OK);   // wyświetl w okienku
        SendMessage(Wnd, WM_SETTEXT, 0, Longint(@Buffer)); // ustaw nową wartość Caption
      end;
    WM_DESTROY: PostQuitMessage(0);
    else Result := DefWindowProc(Wnd, uMsg, wPar, lPar);
  end;
end;

var
  Wnd: TWndClass;  // klasa okna
  Msg: TMsg;

begin
  with Wnd do
  begin
    lpfnWndProc := @WndProc; // funkcja okienkowa
    hInstance := hInstance; // uchwyt do zasobów
    lpszClassName := 'My1stApp'; // klasa
    hbrBackground := COLOR_WINDOW; // kolor tła
  end;

  RegisterClass(Wnd); // zarejestruj nowa klasę

// stwórz formę...
  CreateWindow('My1stApp', 'Server App',
	WS_VISIBLE or WS_TILEDWINDOW,
        300, 300, 300, 70,
	0, 0, hInstance, NIL);

  while GetMessage(msg, 0, 0, 0) do
  begin
    TranslateMessage(msg);
    DispatchMessage(msg);
  end;
end.

W tym programie w porównaniu do innych nastąpiła pewna zmiana. Otóż podczas przetwarzania komunikatów dodałem funkcję TranslateMessage, która jest funkcją niezbędną podczas przetwarzania komunikatów klawiaturowych. Bez tej funkcji wpisywanie znaków w kontrolce Edit nie byłoby możliwe.

Po tym jak tekst z komponentu Edit zostanie pobrany do zmiennej - zawartość zmiennej zostaje wysłana do okna aplikacji, tj. oznacza to, że tekst ten będzie teraz widniał w polu Caption okna. Do okienka trzeba w tym momencie wysłać komunikat WM_SETTEXT, który ustawi tekst, który jest w parametrze lPar w danym oknie.

Tworzenie bardziej zaawansowanych komponentów

Na samym starcie trzeba będzie do listy modułów - uses dodać moduł CommCtrl. To dzięki niemu będziemy mogli tworzyć bardziej zaawansowane kontrolki takie jak ProgressBar'y, ListView i inne komponenty. Operować tymi komponentami możemy oczywiście tylko wysyłając do nich odpowiednie komunikaty. Spis wszystkich komunikatów możesz znaleźć właśnie w pliku CommCtrl.pas - polecam jego przeglądniecie. Tych komunikatów jest dosyć dużo i nie ma sensu ich wszystkich opisywać, a nazwa jest raczej intuicyjna. Bo co może robić komunikat PBM_SETPOS. Oczywiście, że ustawiać nową pozycję, w tym wypadku dla ProgressBar'a.

Napiszmy jakiś prosty programik - na sam początek tylko umieszczenie komponentu ProgressBar na formularzu. Tworzenie takiego komponentu nie różni się praktycznie niczym od tworzenia zwykłego komponentu. Przykładowy kod, który stworzy komponent wygląda tak:

CreateWindow('msctls_progress32', '', WS_CHILD or WS_VISIBLE, 100, 10, 350, 20, Wnd, 0, hInstance, nil);

Uwaga! Jeżeli uruchomisz taki program, a na ekranie nadal widnieć będzie czysta forma, tzn. komponent nie zostanie utworzony to wówczas konieczne będzie wywołanie procedury InitCommonControls. Procedura ta inicjuje odpowiednią bibliotekę DLL. Najlepiej tę procedurę wywołać tuż po utworzeniu nowej klasy w sekcji begin..end.

Czyli decyduje tu pierwszy parametr - jeżeli jest to msctls_progress32 to oznacza iż zostanie utworzony komponent ProgressBar. Teraz możemy wprawić komponent w ruch. Jak wiesz jest to pasek postępu więc, aby uzyskać efekt postępowania jakiegoś procesu trzeba seryjnie wysyłać do tego komponentu komunikat PBM_SETPOS. Cała pętla będzie wyglądać tak:

      for i := 0 to 100 do
      begin
        Sleep(50);
        SendMessage(ProgressBar, PBM_SETPOS, i, 0);
      end;

Czyli pętla od jednego do stu z przerwami pomiędzy kolejnymi iteracjami wynoszące 50 milisekund. Podczas każdorazowego wykonania pętli do komponentu jest wysyłany komunikat PBM_SETPOS. Parametr lParam funkcji SendMessage zawiera nową wartość (pozycję) paska postępu. Cały ten kod wstawimy do obsługi komunikatu WM_PAINT. Cały kod programu będzie wyglądał tak:

{
   Copyright (c) 2002 by Adam Boduch
}

program Ctrl;

uses
  Windows,
  CommCtrl,
  Messages;

var ProgressBar : HWND;

function WndProc(Wnd: HWND; uMsg: UINT; wPar: WPARAM; lPar: LPARAM): LRESULT; stdcall;
var
  i : Integer;
begin
{ na początek zwracamy wartość 0 - meldunek jest przetwarzany }
  Result := 0;
  case uMsg of
    WM_CREATE:
    begin
    // umieść komponent ProgressBar i zwróć uchwyt
      ProgressBar := CreateWindow('msctls_progress32', '', WS_CHILD or WS_VISIBLE, 100, 10, 350, 20, Wnd, 
     0, hInstance, nil);
    end;
    WM_PAINT:  // obsługa komunikatu WM_PAINT
    begin
      for i := 0 to 100 do
      begin
        Sleep(50);   // odczekaj 50 milisekund
        SendMessage(ProgressBar, PBM_SETPOS, i, 0); // wyślij komunikat do komponentu
      end;
      Halt(1); // zamknij program
    end;                                                         
    WM_DESTROY: PostQuitMessage(0);
    else Result := DefWindowProc(Wnd, uMsg, wPar, lPar);
  end;
end;

var
  Wnd: TWndClass;  // klasa okna
  Msg: TMsg;

begin
  with Wnd do
  begin
    lpfnWndProc := @WndProc; // funkcja okienkowa
    hInstance := hInstance; // uchwyt do zasobów
    lpszClassName := 'My1stApp'; // klasa
    hbrBackground := COLOR_WINDOW; // kolor tła
    hIcon := LoadIcon(0, IDI_APPLICATION); // domyślna ikona
    hCursor := LoadCursor(0, IDC_ARROW); // domyślny kursor
  end;

  RegisterClass(Wnd); // zarejestruj nowa klasę
  InitCommonControls;

// stwórz formę...
  CreateWindow('My1stApp', 'Aplikacja z wykorzystaniem modułu CommCtrl.pas',
	WS_VISIBLE or WS_TILEDWINDOW,
        300, 300, 500, 300,
	0, 0, hInstance, NIL);

  while GetMessage(msg, 0, 0, 0) do
  begin
    TranslateMessage(msg);
    DispatchMessage(msg);
  end;  
end.

Czcionki

W sprawie czcionek będzie, przyznam trochę więcej roboty. Nie żeby to było jakoś specjalnie trudne, bo robi się to za pomocą jednego polecenia CreateFont, ale liczba parametrów z tego zdarzenia jest dość duża. Cała struktura tej funkcji przedstawia się następująco:
HFONT CreateFont(

    int nHeight,	// wysokość czcionki
    int nWidth,	// szerokość czcionki
    int nEscapement,	// angle of escapement 
    int nOrientation,	// base-line orientation angle 
    int fnWeight,	// szerokość (rodzaj) czcionki 
    DWORD fdwItalic,	// czcionka pochylona, czy nie? 
    DWORD fdwUnderline,	// podkreślenie 
    DWORD fdwStrikeOut,	// przekreślenie
    DWORD fdwCharSet,	// rodzaj czcionki 
    DWORD fdwOutputPrecision,	// output precision 
    DWORD fdwClipPrecision,	// clipping precision 
    DWORD fdwQuality,	// jakość 
    DWORD fdwPitchAndFamily,	// rodzina czcionek 
    LPCTSTR lpszFace 	// nazwa czionki 
   );

Sam więc widzisz, że liczba parametrów jest dość duża. Kod stworzenia przykładowej czcionki może wyglądać tak:

CreateFont(18, 0, 0, 0, FW_MEDIUM, 1, 0, 0, DEFAULT_CHARSET, OUT_CHARACTER_PRECIS,
      CLIP_DEFAULT_PRECIS, DEFAULT_QUALITY, DEFAULT_PITCH, 'Arial');

W tym wypadku stworzona zostanie czcionka Arial'a o rozmiarach 18 i pochyleniu. Ustawienie takiej czcionki dla jakiegoś komponentu odbywa się po wysłaniu komunikatu WM_SETFONT.

      DefaultFont := CreateFont(18, 0, 0, 0, FW_MEDIUM, 1, 0, 0, DEFAULT_CHARSET, OUT_CHARACTER_PRECIS,
      CLIP_DEFAULT_PRECIS, DEFAULT_QUALITY, DEFAULT_PITCH, 'Arial');
      SendMessage(Static, WM_SETFONT, DefaultFont, 0);

Obsługa zdarzeń - przypomnienie

Na samo zakończenie pragnę Wam przypomnieć o komunikatach, o dekodowaniu ich parametrów. Programując w API będziesz używał komunikatów zdecydowanie częściej niż w innych wypadkach (czytaj: podczas programowania w VCL). Często będziesz zmuszony korzystać z funkcji LoWord oraz HiWord, o których to mówiłem podczas omawiania komunikatów. Np. chcąc obsłużyć zdarzenie WM_MOSEMOVE, które występuje podczas poruszania kursorem myszki będziesz musiał rozkodować parametr lParam, za pomocą funkcji LoWord oraz HiWord właśnie.
    WM_MOUSEMOVE:
    begin
      MousePosition[0] := LoWord(lPar);
      MousePosition[1] := HiWord(lPar);

      SetWindowText(Static, PChar('Pozycja kursora: X: ' + IntToStr(MousePosition[0]) + ', Y: ' +
      IntToStr(MousePosition[1])));
    end;

MousePosition to tablica dwu-elementowa typu Integer. Natomiast funkcja SetWindowText powoduje ustawienie tekstu w wybranej kontrolce. Inaczej mówiąc wysyła ona do danego okna komunikat WM_SETTEXT. Z innymi zdarzeniami jest podobnie, lecz nie zawsze trzeba rozkodowywać:

    WM_KEYDOWN:
    begin
      SetWindowText(Static, PChar('Naciśnięto klawisz "' + Chr(wPar) + '"'));
    end;
    WM_KEYUP:
    begin
      SetWindowText(Static, PChar('Odciśnięto klawisz "' + Chr(wPar) + '"'));
    end;

Kod ACSII naciśniętego klawisza został zawarty w parametrze wPar - pozostaje go tylko rozkodować na znak przy pomocy funkcji Chr. Cały kod źródłowy przedstawia się następująco:

{
   Copyright (c) 2002 by Adam Boduch
}

program Message;

uses
  Windows,
  Messages;


function IntToStr(Value : Integer) : String;
var
  Buffer : array[0..255] of char;  // bufor, w którym przechowywać będziemy dane
begin
  wvsprintf(Buffer, '%d', @Value); // tu następuje funkcja konwersji
  Result := Buffer; // zwracamy rezultat
end;

var Static : HWND;

function WndProc(Wnd: HWND; uMsg: UINT; wPar: WPARAM; lPar: LPARAM): LRESULT; stdcall;
var
  MousePosition : array[0..1] of Integer;
begin
{ na początek zwracamy wartość 0 - meldunek jest przetwarzany }
  Result := 0;
  case uMsg of
    WM_CREATE:
    begin
      Static := CreateWindow('STATIC', '', WS_CHILD or WS_VISIBLE, 50, 50, 300, 25, Wnd,
      0, hInstance, nil); 
    end;
    WM_MOUSEMOVE:
    begin
      MousePosition[0] := LoWord(lPar);
      MousePosition[1] := HiWord(lPar);

      SetWindowText(Static, PChar('Pozycja kursora: X: ' + IntToStr(MousePosition[0]) + ', Y: ' +
      IntToStr(MousePosition[1])));
    end;
    WM_KEYDOWN: SetWindowText(Static, PChar('Naciśnięto klawisz "' + Chr(wPar) + '"'));
    WM_KEYUP: SetWindowText(Static, PChar('Odciśnięto klawisz "' + Chr(wPar) + '"'));
    WM_LBUTTONDOWN: SetWindowText(Static, 'Naciśnięto lewy klawisz myszki');
    WM_LBUTTONUP: SetWindowText(Static, 'Odciśnięto lewy klawisz myszki');
    WM_RBUTTONDOWN: SetWindowText(Static, 'Naciśnięto prawy klawisz myszy');
    WM_RBUTTONUP: SetWindowText(Static, 'Odciśnięto prawy klawisz myszy');

    WM_DESTROY: PostQuitMessage(0);
    else Result := DefWindowProc(Wnd, uMsg, wPar, lPar);
  end;
end;

var
  Wnd: TWndClass;  // klasa okna
  Msg: TMsg;

begin
  with Wnd do
  begin
    lpfnWndProc := @WndProc; // funkcja okienkowa
    hInstance := hInstance; // uchwyt do zasobów
    lpszClassName := 'My1stApp'; // klasa
    hbrBackground := COLOR_WINDOW; // kolor tła
    hIcon := LoadIcon(0, IDI_APPLICATION); // domyślna ikona
    hCursor := LoadCursor(0, IDC_ARROW); // domyślny kursor
  end;

  RegisterClass(Wnd); // zarejestruj nowa klasę

// stwórz formę...
  CreateWindow('My1stApp', 'Aplikacja z wykorzystaniem modułu CommCtrl.pas',
	WS_VISIBLE or WS_TILEDWINDOW,
        300, 300, 500, 300,
	0, 0, hInstance, NIL);

  while GetMessage(msg, 0, 0, 0) do
  begin
    TranslateMessage(msg);
    DispatchMessage(msg);
  end;  
end.

Podsumowanie

Właśnie tym rozdziałem wkroczyłeś w nieco trudniejszą dziedzinę programowania w Delphi. Taka jest przynajmniej moja opinia. W kolejnym rozdziale nadal pozostaniemy przy tematyce programowania w API. Pamiętaj jednak, że ten aspekt programowania jest na tyle obszerny, że w większości będziesz musiał uczyć się sam. Czytaj plik pomocy Delphi, analizuj inne przykłady programów i moduły. Cały czas poszerzaj swoje horyzonty.

11 komentarzy

Dobre, chociarz mało odkrywcze... a może tak art jak zrobić klasy oparte na API? Np. klasa TSmallForm, ktora dzialalaby podobnie jak TForm tyle ze 300 Kb execa mniej...

Ehh .. - cały artykuł o tym mówił, a Ty dalej nic z tego nie rozumiesz :)

Dobre, chociarz mało odkrywcze... a może tak art jak zrobić klasy oparte na API? Np. klasa TSmallForm, ktora dzialalaby podobnie jak TForm tyle ze 300 Kb execa mniej... ;)

Zgadza sie :) Ale w kodzie chociaz bylo dobrze :P

Adam cos ten art bardzo podobny do twojego kursu WinAPI :)
Pozdrawiam Kamil

Kurcze, trzeba sie wziac wreszcie za skrypt kolorowania skladni :/

faaaaaaaaaaaaaaaaaaaajneeeeeeee %>~ teraz wszystko stalo sie prostrze B)

Mam nadzieję że to faktycznie tylko wstęp, oraz że artykuł bedzie dalej rozwijana, a może wstęp do książki to byłoby coś.
Pozdrawiam.

Heh niektórym to się nudzi w domu :)

LiWord oraz HiWord, o których to mówiłem
^^^^^
powinno byc
loWord czyli w dowolnym tlumaczeniu
lower word
nie ma takiego czegos jak li word :)