Rozdział 12. WinAPI

Adam Boduch

Nie sposób było nie wspomnieć w tej książce o WinAPI. Z tym terminem zetknąłeś się już wielokrotnie podczas lektury niniejszej publikacji. Teraz chciałbym omówić ten temat nieco dogłębniej. Nie jest możliwe bowiem zawarcie wszystkich informacji dotyczących WinAPI w jednym rozdziale ? temat ten jest na tyle rozległy, że można by mu poświęcić osobną książkę. Więcej o WinAPI możesz dowiedzieć się ze strony http://msdn.microsoft.com lub z systemu pomocy Delphi (znajdziesz tam dokładny opis funkcji i procedur).

1 Czym tak naprawdę jest WinAPI?
     1.1 Zasady tworzenia programów za pomocą WinAPI
     1.2 Brak zdarzeń
     1.3 Brak komponentów
     1.4 Zalety wykorzystania WinAPI
2 Pierwszy program
3 Funkcja okienkowa
4 Rejestracja klasy
5 Tworzenie formularza
6 Komunikaty i uchwyty
7 Łańcuchy
     7.5 Konwersja łańcuchów
     7.6 Funkcje operujące na łańcuchach
          7.6.1 CharLower, CharUpper
          7.6.2 lstrlen
          7.6.3 lstrcpyn
8 Tworzenie kontrolek
     8.7 Umieszczanie kontrolek przy starcie programu
     8.8 Flagi kontrolek
9 Obsługa zdarzeń
10 Uchwyty do kontrolek
11 Tworzenie bardziej zaawansowanych kontrolek
     11.9 Pozostałe kontrolki
12 Wyświetlanie grafiki
     12.10 Rysowanie w WinAPI
     12.11 Kontekst urządzenia graficznego
     12.12 Obsługa WM_PAINT
          12.12.4 Zmiana koloru tła
     12.13 Ładowanie i wyświetlanie bitmapy
13 Ładowanie zasobów
     13.14 Skompilowane zasoby
     13.15 Wykorzystanie zasobów
          13.15.5 Wyświetlenie formularza
          13.15.6 Ustawianie wartości komponentów formularza
     13.16 LockResource, LoadResource, FindResource
     13.17 Zapisywanie plików na dysku
14 Podsumowanie

Można powiedzieć, że ten rozdział przeznaczony jest dla ?maniaków? Delphi w pozytywnym tego słowa znaczeniu. Programowanie w WinAPI nie jest bowiem łatwe i wygodne, ale umożliwia zachowanie większej kontroli nad programem.

Czym tak naprawdę jest WinAPI?

Pełna nazwa tego skrótu to Windows Application Programming Interface . Dla osób, które wcześniej tworzyły swoje programy w Turbo Pascalu, biblioteka wizualna i klasy mogą wydać się dość niezrozumiałe. Z kolei dla niektórych prostsze może okazać się rozpoczęcie pisania programów metodą API. Ty jednak jesteś już zapewne przyzwyczajony do stosowania klas i komponentów, a WinAPI może Ci się wydać trudne lub po prostu nieciekawe. Problem, którego rozwiązanie przy użyciu komponentów wymagało jednego wiersza kodu, przy wykorzystaniu WinAPI może oznaczać konieczność napisania nawet kilkudziesięciu wierszy! Dlatego nie zdziwię się, jeżeli ominiesz ten rozdział i przejdziesz od razu do kolejnego.

Zasady tworzenia programów za pomocą WinAPI

Wyobraź sobie pisanie programów bez wykorzystania formularzy, komponentów i wszystkich innych udogodnień oferowanych przez Delphi. Nasze programy będą oparte jedynie na podstawowych modułach Windows.pas i Messages.pas. Wszystkie funkcje, z których będziemy korzystać, zawarte są w bibliotekach DLL systemu Windows. Ich załadowanie do programu odbywa się w module Windows.pas.

Nam, użytkownikom tych bibliotek, potrzebna jest wiedza o ich budowie ? liczbie i typie parametrów, wartościach zwracanych przez funkcję itp. Stąd miej na uwadze perspektywę częstego zaglądania do pomocy Delphi.
Przy tej okazji warto wspomnieć o możliwości choć częściowego nauczenia się języka C. System Windows był pisany w tym języku, stąd opisy wszystkich funkcji API (deklaracje) są również w nim przedstawione. Jest to pewna okazja do poznania choćby w małym stopniu budowy języka C.

Gdy kiedyś zaczniesz pisać programy w języku C++, znajomość WinAPI bardzo Ci się przyda! Nazwy funkcji są takie same ? jedynie składnia nieco się różni.

Brak zdarzeń

Podczas pisania programów w ?czystym? API będziemy pozbawieni wygodnego mechanizmu, jakim są zdarzenia. Jak pamiętasz z rozdziału 5., mechanizm zdarzeń można zastąpić poprzez komunikaty Windows. Ten moment jest więc dobrą okazją, aby cofnąć się do rozdziału 5. i przypomnieć sobie zasadę funkcjonowania komunikatów.

Brak komponentów

W API będziemy musieli obyć się bez komponentów. Te ?klocki?, jakimi są komponenty, w dużym stopniu odciążały nas od mozolnego operowania komunikatami czy pamięcią. Nie będziemy jednak pozbawieni typowych kontrolek Windows, jak przycisk czy lista rozwijalna ? będziemy je tworzyć w kodzie programu za pomocą funkcji CreateWindow.

Zalety wykorzystania WinAPI

Zastanawiasz się może, co takiego zyskasz, używając mechanizmów WinAPI? Powiem szczerze: niewiele. Dużym plusem jest szybkość działania aplikacji oraz rozmiar. Programy tworzone w Delphi i wykorzystujące VCL mają duże rozmiary. Nawet prosty program w postaci ?czystego? formularza potrafi zajmować grubo ponad 300 kB. Aplikacje wykorzystujące jedynie WinAPI mogą zajmować nawet 16 kB i są wykonywane znacznie szybciej. Różnica jest znaczna, nieprawdaż?

Czytając ten rozdział, masz możliwość zaznajomienia się z dotąd nieznanymi funkcjami, z których być może będziesz musiał skorzystać w swoich programach, gdy VCL okaże się niewystarczający i zbytnio będzie Cię ograniczał.

Głównymi zaletami Delphi są przecież biblioteka VCL, klasy oraz formularze, dzięki którym tworzenie programów trwa znacznie krócej. Jeśli porzucisz te udogodnienia, pisanie aplikacji może zająć więcej czasu, a nie po to chyba tworzono Delphi, prawda?

Podsumowując: WinAPI jest okazją do głębszego zaznajomienia się z tematyką programowania w systemie Windows, lecz nie nadaje się do pisania dużych projektów.

Pierwszy program

Przypomnij sobie rozdział 2. Wówczas poznawałeś dopiero język Object Pascal, ale tworzone przez Ciebie programy także nie zawierały żadnych komponentów czy formularzy. Te programy po skompilowaniu także miały rozmiar kilkunastu kilobajtów ? można zatem powiedzieć, że już wtedy pisałeś programy WinAPI!
Zamknij formularz i Edytor kodu. Następnie z menu Project wybierz polecenie View Source. Kod źródłowy projektu (pliku *.dpr) doprowadź do takiej postaci:

program Project1;

uses
  Windows;

begin

end.

Na razie nie potrzebujemy pliku zasobów, więc usunąłem także dyrektywę {$R}. Ikonę naszego programu możemy dodać w następnej kolejności. Tak powstały kod źródłowy zapisz gdzieś na dysku. Następnie z menu Project wybierz polecenie Build, co spowoduje skompilowanie aplikacji i utworzenie pliku *.exe. Spójrz teraz na rozmiar aplikacji ? u mnie jest to 14 kB! Na razie co prawda program jest ?pusty?, ale już wkrótce co nieco do niego dodamy.

Funkcja okienkowa

Dotąd nasz program kończył pracę zaraz po uruchomieniu go ? w bloku begin nie ma przecież żadnej instrukcji. Naszym celem jest napisanie takiego programu w WinAPI, który zakończyłby pracę po interwencji użytkownika ? zamknięciu okna. Musimy więc napisać kod, który spowodowałby wyświetlenie formularza. Jednym z etapów tworzenia takiego formularza jest napisanie funkcji okienkowej. Funkcja taka będzie odpowiedzialna za odbieranie wszystkich komunikatów, które docierają do okna i ewentualną reakcję na dany komunikat. Zadeklaruj więc w programie taką funkcję:

function WndProc(Wnd: HWND; uMsg: UINT; wPar: WPARAM; lPar: LPARAM): LRESULT; stdcall;
begin
end;

Znaczenie parametrów tej funkcji jest następujące:
*Wnd ? uchwyt do okna.
*uMsg ? komunikat.
*wPar ? pierwsza wartość komunikatu.
*lPar ? druga wartość komunikatu.
Taka budowa jest nieprzypadkowa ? aby cały program został prawidłowo skompilowany, funkcja okienkowa musi wyglądać tak, jak to przedstawiłem powyżej. Pierwszym komunikatem, jaki będzie obsługiwany przez funkcję okienkową, jest WM_DESTROY. Program musi odpowiednio zareagować na próbę zamknięcia programu.

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
   { 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;

Jak widzisz, instrukcja case sprawdza, jaki komunikat został odebrany przez funkcję okienkową. W przypadku odebrania komunikatu WM_DESTROY program kończy pracę ? PostQuitMessage. Na samym jednak początku przypisujemy funkcji wartość zwrotną ? cyfrę 0. W przeciwnym wypadku ? jeżeli nadesłany komunikat ?nas interesuje? ? przekazujemy go dalej, do domyślnego okna. Realizuje to funkcja DefWindowProc; parametry muszą być identyczne z parametrami funkcji okienkowej.

Aby program został prawidłowo skompilowany, na liście uses musi znaleźć się moduł Messages.pas.

Rejestracja klasy

Aby cały formularz mógł zostać stworzony, uprzednio należy zarejestrować klasę. Rejestracja klasy następuje poprzez wywołanie funkcji RegisterClass z modułu Windows.pas.

function RegisterClass(const lpWndClass: TWndClass): ATOM; stdcall;

W parametrze owej funkcji należy podać zmienną wskazującą rekord TWndClass:

 TWndClass = packed record
    style: UINT;
    lpfnWndProc: TFNWndProc;
    cbClsExtra: Integer;
    cbWndExtra: Integer;
    hInstance: HINST;
    hIcon: HICON;
    hCursor: HCURSOR;
    hbrBackground: HBRUSH;
    lpszMenuName: PAnsiChar;
    lpszClassName: PAnsiChar;
  end;

Powyższy rekord określa wygląd formularza, kolor tła, styl i kursor. Znacznie parametrów jest następujące:
*style ? parametr ów określa styl wyświetlanego okna. Możliwe jest mieszanie stylów za pomocą operatora or.
*lpfnWndProc ? jest to wskazanie na funkcję okienkową.
*cbClsExtra ? liczba dodatkowych bajtów alokowanych wraz z rekordem.
*cbWndExtra ? liczba dodatkowych bajtów alokowanych wraz z instancją okna.
*hInstance ? uchwyt do zasobów.
*hIcon ? identyfikacja formularza.
*hCursor ? kursor używany w czasie wyświetlania formularza.
*hbrBackground ? tło formularza. Możliwe jest zastosowanie jednej z poniższych 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.
*lpszMenuName ? wskazanie na łańcuch określający menu używane w programie.
*lpszClassName ? wskazanie na nazwę klasy (wartość typu PChar).
Rejestracja nowej klasy może być wykonana w poniższy sposób:

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 nową klasę

end;

W moim przypadku nie było konieczne wypełnianie wszystkich pól rekordu TWndClass. Przypisałem jedynie te pola, które wydawały się konieczne do uzyskania przynajmniej podstawowego wyglądu formularza.

Tworzenie formularza

Na szczęście tworzenie samego formularza nie jest czynnością zbytnio skomplikowaną. Realizuje to bowiem jedna instrukcja ? CreateWindow:

function CreateWindow(lpClassName: PChar; lpWindowName: PChar;
  dwStyle: DWORD; X, Y, nWidth, nHeight: Integer; hWndParent: HWND;
  hMenu: HMENU; hInstance: HINST; lpParam: Pointer): HWND;

Przyznasz, że ilość parametrów jest spora:
*lpClassName ? nazwa klasy (wartość PChar). Wartość ta musi się równać wartości wpisanej w rekordzie TWndClass.
*lpWindowName ? łańcuch określający tekst, który będzie wyświetlany na formularzu. Znaczenie parametru można porównywać do właściwości Caption komponentów.
*dwStyle ? styl okna (tabela 12.1).
*x ? położenie formularza w poziomie. Wstawienie w to miejsce stałej CW_USEDEFAULT powoduje automatyczne dopasowanie położenia przez system (nowe okno będzie przesunięte lekko w lewą stronę).
*y ? położenie formularza w pionie. Wstawienie w to miejsce stałej CW_USEDEFAULT powoduje automatyczne dopasowanie położenia przez system.
*nWidth ? szerokość formularza. Tutaj także stała CW_USEDEFAULT powoduje automatyczne dopasowanie szerokości.
*nHeight ? wysokość formularza. Stała CW_USEDEFAULT powoduje dopasowanie wysokości formularza.
*hWndParent ? uchwyt do okna rodzica.
*hMenu ? wskazanie do menu, które ma być użyte w programie.
*hInstance ? określa instancję modułu, który ma być kojarzony z programem.
Na podstawie tych danych utworzenie formularza może wyglądać tak:

  CreateWindow('My1stApp', 'Pierwszy program w WinAPI',
  WS_VISIBLE or WS_TILEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT,
  CW_USEDEFAULT, CW_USEDEFAULT, 0, 0, hInstance, NIL);

Tabela 12.1. Najczęstsze wartości określające styl okna

WartośćOpis
WS_OVERLAPPEDOkno posiada pasek tytułowy oraz obramowanie
WS_CHILDPotomne okno, które nie może ?wyjść? poza okno rodzicielskie
WS_POPUPOkno dialogowe
WS_CAPTIONOkno ma pasek tytułu
WS_SYSMENUOkno ma menu systemowe
WS_MINIMIZEBOXOkno ma przycisk minimalizacji
WS_MAXIMIZEBOXOkno ma przycisk maksymalizacji
WS_VISIBLEOkno jest widoczne
WS_HIDEOkno jest ukryte
WS_DISABLEDNieaktywne okno ? nie reaguje na zdarzenia
WS_BORDEROkno posiada ramkę

Listing 12.1. Pierwszy program napisany w WinAPI

{
   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
   { 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 nową klasę

  CreateWindow('My1stApp', 'Pierwszy program 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.

W listingu 12.1 zaprezentowano cały kod źródłowy programu. Nie omawiałem jeszcze ostatnich instrukcji tego listingu. Są one bardzo ważne ? bez nich program nie będzie mógł zostać uruchomiony. Operacje te muszą być wykonane, 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 z kolei przekazuje meldunek funkcji okienkowej.

Komunikaty i uchwyty

Pisząc programy w WinAPI, będziemy posługiwali się wyłącznie komunikatami jako formą zastępującą zdarzenia (funkcje SendMessage i PostMessage). Warto więc przypomnieć sobie informacje na temat wysyłania komunikatów. Ich odbieranie będzie następowało tylko w funkcji okienkowej.
Odświeżmy zatem pamięć ? komunikaty można podzielić na następujące kategorie:
*komunikaty klawiaturowe (użytkownik nacisnął lub zwolnił jakiś klawisz),
*komunikaty myszy (użytkownik wykonał jakąś czynność myszą),
*komunikaty zegara, oznaczające upływ określonego odcinka czasu,
*komunikaty systemu ? tworzenie okna, zmiana jego rozmiaru i położenia, 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 należy 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 kontrolkom. Nazwa typu reprezentującego uchwyt zaczyna się od litery H, czyli np. HWND, HBRUSH, HFONT czy HBITMAP. Dzięki temu łatwo jest rozpoznać, czy dana zmienna jest uchwytem.

Łańcuchy

Podczas pisania programów w WinAPI będziemy używali jedynie łańcuchów typu PChar lub łańcuchów w formie tablicy. Stosowanie typu String powoduje spowolnienie działania programu i zużywanie większej ilości pamięci.

Typ PChar ma jeszcze jedną zaletę ? można dokonywać na nim takich operacji:

 P2 := 'To jest Delphi';
  P1 := P2 + 8;

Pozornie wygląda to tak, jakby do zmiennej P2 dodawana była cyfra 8. W rzeczywistości do typu P1 przypisujemy wartość zmiennej P2, tyle że bez pierwszych 8 znaków. Podczas lektury dalszej części książki możesz spotkać się także z deklaracjami zmiennych w postaci tablicy:

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

Do takiej zmiennej można następnie przypisać dane w zwykły sposób, tyle że ich wielkość będzie ograniczona do 255 znaków.

  Variable := 'Adam Boduch';

Konwersja łańcuchów

W VCL ten problem nie istniał ? moduł SysUtils posiadał odpowiednie funkcje, umożliwiające konwersję typów. Pisząc programy API, nie będziemy mogli z nich skorzystać ? pozostaje nam użycie funkcji zastępczych, np. wvsprintf.

Oto możliwy sposób wykonania funkcji zastępczej:

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;

Funkcja wvsprintf służy do konwertowania tekstu. Pierwszym parametrem musi być wskazanie zwracanego przez funkcję ciągu. Ja zadeklarowałem Buffer ? 256-elementową tablicę typu Char. Drugi parametr to tzw. maska. Jeśli wstawimy w to miejsce znak %d, zostanie on zastąpiony liczbą typu Integer. Owa liczba to zmienna Value, przekazywana jako trzeci parametr. Przykładowy program prezentujący działanie łańcuchów, zamieściłem w listingu 12.2.

W powyższej funkcji IntToStr zadeklarowałem tablicę 256-elementową (standardowo), chociaż tak naprawdę aż tak duża wartość nie jest konieczna.

Wyjątkowo w powyższej funkcji jako zwracanego rezultatu użyłem typu String. Zrobiłem to tylko po to, aby upodobnić budowę funkcji do rzeczywistego wyglądu funkcji IntToStr z modułu SysUtils.

Listing 12.2. Pełny kod źródłowy programu

{
  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(12) + ' części książki!'), 'Witaj!', MB_OK); // wyświetl wartość zmiennej
end.

Inny przykład wykorzystania funkcji wvsprintf:

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 w drugim parametrze w łańcuchu znajdują się dwa znaki ? %s i %d. Zostaną one po konwersji zastąpione danymi w postaci liczby Integer oraz łańcucha String.

Funkcje operujące na łańcuchach

Poniżej przedstawiam kilka funkcji WinAPI operujących na łańcuchach. Mogą one okazać się przydatne podczas pisania aplikacji w API.

CharLower, CharUpper

function CharLower(lpsz: PChar): PChar; stdcall;
function CharUpper(lpsz: PChar): PChar; stdcall;

Obie funkcje powodują zamianę znaków odpowiednio na małe lub wielkie litery. Pierwsza z nich (CharLower) zamienia litery z wielkich na małe, a CharUpper ? z małych na wielkie.

program main;

uses Windows;


begin
  MessageBox(0, CharLower('TO JEST PROGRAM W WINAPI'), '', MB_OK);
  MessageBox(0, CharUpper('to jest program w winapi'), '', MB_OK);
end.

Warto się zainteresować także funkcją CharLowerBuff i CharUpperBuff. Obie także powodują zamianę znaków, lecz posiadają także dodatkowy parametr, który określa liczbę znaków, które mają zostać zamienione.

lstrlen

function lstrlen(lpString: PChar): Integer; stdcall;

Funkcja lstrlen podaje długość łańcucha określonego w parametrze lpString. Długość podawana jest w znakach.

Writeln(lstrlen('Adam'));

Powyższa instrukcja wyświetli na ekranie liczbę 4.

lstrcpyn

function lstrcpyn(lpString1, lpString2: PChar; iMaxLength: Integer): PChar; stdcall;

Funkcja służy do kopiowania części łańcucha do drugiej zmiennej. Pierwszy parametr musi być wskazaniem łańcucha, do którego zostaną skopiowane dane. Drugi parametr ? lpString2 ? to miejsce, z którego dane zostaną pobrane. Ostatni parametr ? iMaxLength ? określa liczbę znaków do skopiowania:

program main;

uses Windows;

{$APPTYPE CONSOLE}

var
  P1 : array[0..50] of char;

begin
  lstrcpyn(P1, 'Delphi jest narzędziem typu RAD', 7);
  Writeln(P1);
  Readln;
end.

Powyższy kod źródłowy spowoduje wyświetlenie na ekranie napisu Delphi.

Tworzenie kontrolek

Zarówno tworzenie komponentów, jak i różnych kontrolek odbywa się za pośrednictwem funkcji CreateWindow. W przypadku komponentów nie będzie konieczna rejestracja nowych klas itp. elementów. Do stworzenia nowego komponentu wystarczy napisanie jednego wiersza kodu. Komponent należy utworzyć z flagą WS_CHILD oraz WS_VISIBLE. Stworzenie przycisku będzie więc wyglądało następująco:

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 komponentu. Dalsza część jest już taka sama, jak w przypadku tworzenia formularza. W tabeli 12.2 umieściłem wartości, jakie może przyjmować pierwszy parametr funkcji CreateWindow.

Tabela 12.2. Możliwe wartości pierwszego parametru funkcji CreateWindow

WartośćOpis
BUTTONPrzycisk ? odpowiednik komponentu TButton
COMBOBOXLista rozwijalna ? odpowiednik TComboBox
EDITKontrolka edycyjna ? jedno liniowa. Odpowiednik komponentu TEdit
LISTBOXKontrolka wielowierszowa. Odpowiednik TListBox
MDICLIENTOkno potomne ? MDI
SCROLLBARPasek przewijania ? inaczej TScrollBar
STATICEtykieta tekstowa. Odpowiednik TLabel

Taka kontrolka będzie więc ?dzieckiem? w stosunku do formularza (WS_CHILD) i będzie także widoczna (WS_VISIBLE).

Jeżeli chcesz, aby kontrolka na starcie była niewidoczna, użyj flagi WS_HIDE.

Pamiętaj, aby podczas tworzenia nowej kontrolki w parametrze hWndParent (czwarty od końca) podać uchwyt okna głównego. Parametr ów określa uchwyt okna ?rodzica? ? w tym wypadku formularza.

Umieszczanie kontrolek przy starcie programu

Jeżeli chcemy, aby kontrolki były tworzone na starcie programu, kod należy umieścić w funkcji okienkowej. Konieczne jest także obsłużenie komunikatu WM_CREATE. Oto kod:

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;

W przypadku zastosowania takiej funkcji okienkowej, jaką przedstawiono powyżej, na formularzu w punkcie 100,100 zostanie umieszczony przycisk. W listingu 12.3 znajduje się kod źródłowy programu, którego efektem jest umieszczenie 5 przycisków na raz.

Listing 12.3. Umieszczanie kilku przycisków

{
   Copyright (c) 2002 by Adam Boduch
}

program ChildApp;

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;

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;

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

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

  RegisterClass(Wnd); // zarejestruj nową klasę

  CreateWindow('My1stApp', 'Pierwszy program 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.

W kodzie wykorzystaliśmy wcześniej napisaną funkcję IntToStr. Umieszczenie 5 kontrolek następuje w pętli, dlatego też za każdą iteracją należy zmieniać położenie przycisku w pionie, wykonując takie działanie:

100 + I * 30;

Powoduje to dodanie do liczby 100 wartości z mnożenia ? np. 30, 60, 90 itd. Działanie programu prezentuje rysunek 12.1.

12.1.jpg
Rysunek 12.1. Działanie programu

Flagi kontrolek

Poszczególne kontrolki umieszczone na formularzu mogą posiadać dodatkowe flagi, określające zachowanie lub wygląd komponentu. Owe flagi można podawać jako trzeci parametr polecenia CreateWindow, łącząc je operatorem or. W tabelach 12.3 ? 12.6 przedstawiam najczęściej używane flagi.

Tabela 12.3. Flagi używane z kontrolką BUTTON

FlagaKrótki opis
BS_3STATEKontrolka (przycisk) stanie się komponentem ? la TCheckBox
BS_AUTO3STATEFlaga podobna do BS_3STATE, tyle że komponent może przybierać wartość ?zaznaczony?
BS_AUTORADIOBUTTONKontrolka (przycisk) stanie się komponentem ? la `TRadioButton `(rysunek 12.2)
BS_DEFPUSHBUTTONPowoduje, że przycisk zostanie wyświetlony z czarną, pogrubioną obwódką
BS_GROUPBOXKomponent zostanie wyświetlony z obwódką (rysunek 12.3)
BS_BITMAPUmożliwia wyświetlanie bitmapy na kontrolce
BS_BOTTOMUstawia tekst na samym dole komponentu
BS_CENTERCentruje tekst w poziomie
BS_ICONUmożliwia wyświetlanie ikony na komponencie
BS_LEFTTekst będzie wyrównany do lewej strony
BS_MULTILINEFlaga umożliwia wyświetlanie kilku wierszy tekstu
BS_RIGHTTekst będzie wyrównany do prawej strony
BS_TOPTekst zostanie umieszczony u góry kontrolki
BS_VCENTERTekst zostanie wyśrodkowany w pionie

12.2.jpg
Rysunek 12.2. Przyciski w formie komponentu TRadioButton

12.3.jpg
Rysunek 12.3. Przyciski w formie kontrolek TGroupBox

Tabela 12.4. Flagi używane z kontrolką COMBOBOX

FlagaKrótki opis
CBS_DISABLENOSCROLLPasek przewijania zostanie zablokowany
CBS_DROPDOWNLista rozwijalna zostanie aktywna
CBS_DROPDOWNLISTNie będzie możliwe edytowanie listy rozwijalnej (zaznaczona pozycja nie będzie mogła być zmieniana)
CBS_LOWERCASEKonwertuje tekst wpisany w kontrolce na małe litery
CBS_UPPERCASEKonwertuje tekst wpisany w kontrolce na wielkie litery
CBS_SORTAutomatyczne sortowanie danych wpisanych w kontrolce

Tabela 12.5. Flagi używane z kontrolką EDIT

FlagaKrótki opis
ES_AUTOHSCROLLAutomatycznie przewiń tekst w kontrolce w poziomie, jeżeli użytkownik wpisał więcej znaków niż może być w niej wyświetlone
ES_CENTERWyśrodkuj tekst, jeżeli kontrolka zawiera wiele wierszy
ES_LEFTWyrównaj tekst do lewej strony
ES_LOWERCASEKonwertuj wpisany tekst na małe litery
ES_MULTILINEFlaga umożliwia wpisywanie w kontrolce wielu wierszy tekstu
ES_NUMBERZezwalaj na wpisywanie jedynie liczb
ES_PASSWORDTekst wpisany w kontrolce zostanie zastąpiony znakami *
ES_READONLYTekst wpisany w kontrolce będzie przeznaczony jedynie do odczytu
ES_RIGHTTekst zostanie wyrównany do prawej strony, jeżeli kontrolka została stworzona z flagą ES_MULTILINE
ES_UPPERCASEWpisane w kontrolce litery konwertuj na wielkie
ES_WANTRETURNDotyczy kontrolek wielowierszowych. Po zastosowaniu tej flagi naciśnięcie klawisza Enter przenosi kursor do kolejnego wiersza

Tabela 12.6. Flagi używane z kontrolką LISTBOX

FlagaKrótki opis
LBS_DISABLENOSCROLLWyświetla nieaktywny, pionowy pasek przewijania
LBS_EXTENDEDSELZezwala na zaznaczenie wielu wierszy z użyciem klawisza Shift
LBS_MULTICOLUMNZezwala na wyświetlanie w komponencie kilku kolumn
LBS_SORTAutomatyczne sortowanie kolumn

Więcej informacji na temat flag znajdziesz w pomocy WinAPI pod hasłem CreateWindow.

Obsługa zdarzeń

Umiemy już tworzyć formularze i umieszczać na nich kontrolki WinAPI. Kolejnym krokiem jest obsługa zdarzeń (np. kliknięcia obiektu). Pierwszym krokiem będzie nadanie kontrolce jakiegoś unikalnego identyfikatora.

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 nadaliśmy kontrolce identyfikator nr 100. Od tego momentu podczas kliknięcia przycisku będziemy musieli odbierać komunikat WM_COMMAND i ? zależnie od numeru ID ? odpowiednio reagować:

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

Na takiej samej zasadzie możesz kontrolować naciśnięcie wszystkich przycisków ? ważne jest tylko, aby numery ID różniły się.

W powyższym przykładzie skorzystałem z instrukcji if, lecz przy większej liczbie instrukcji wygodniej będzie zastosować case.

Oto zmodyfikowana procedura okienkowa z poprzedniego programu:

function WndProc(Wnd: HWND; uMsg: UINT; wPar: WPARAM; lPar: LPARAM): LRESULT; stdcall;
var
  i : Integer;
begin
{ na początku zwracamy wartość 0 ? meldunek jest przetwarzany }
  Result := 0;
  case uMsg of
    WM_CREATE:
    begin
    { 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 przycisku
    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;

Jak widać, za każdą iteracją pętli nowa kontrolka zostaje utworzona ze zmienionym numerem ID. Po uruchomieniu aplikacji i naciśnięciu przez użytkownika przycisku do programu zostaje wysłany komunikat WM_COMMAND z parametrem wPar, który zawiera numer ID przycisku. Na tej podstawie możemy odpowiednio zareagować ? w tym wypadku poprzez wyświetlenie komunikatu.

Pełen kod źródłowy powyższego programu możesz znaleźć na płycie CD-ROM w katalogu ..listingi/12/Wm_Command.

Uchwyty do kontrolek

Po prawidłowym utworzeniu kontrolki funkcja CreateWindow zwraca jej uchwyt w postaci typu HWND. Np.:

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

Teraz mając uchwyt takiej kontrolki, możemy wysyłać do niej komunikaty. Przykładowo chcąc pobrać tekst z kontrolki EDIT, musimy skorzystać z funkcji GetWindowText:

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

Pierwszym parametrem tej funkcji musi być uchwyt kontrolki, z której chcemy pobrać tekst. Drugi parametr ? Buffer ? to np. łańcuch o takiej postaci:

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

Pełny kod programu znajduje się w listingu 12.4.

Listing 12.4. Pełny kod programu

{
   Copyright (c) 2002 by Adam Boduch
}

program PMsg;

uses
  Windows,
  Messages;

var
  Edit : THandle;

function WndProc(Wnd: HWND; uMsg: UINT; wPar: WPARAM; lPar: LPARAM): LRESULT; stdcall;
var
  Buffer : array[0..128] of char;
begin
{ na początku 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 parametru Edit
        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; 
    lpszClassName := 'My1stApp'; // klasa
    hbrBackground := COLOR_WINDOW; // kolor tła
  end;

  RegisterClass(Wnd); // zarejestruj nową klasę

  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 pierwszej kolejności po naciśnięciu przycisku pobierana zostaje wartość wpisana w kontrolce EDIT. Teraz wystarczy już tylko wyświetlić zawartość zmiennej Buffer. Następnie program ustawia nową wartość dla okna formularza (można powiedzieć, że to jest właściwość Caption):

SendMessage(Wnd, WM_SETTEXT, 0, Longint(@Buffer));

Chciałem przy okazji zaprezentować sposób wysyłania komunikatów do kontrolek. W tym celu do okna należy przekazać komunikat WM_SETTEXT. Drugi parametr natomiast musi być wskazaniem tekstu, który ma zostać umieszczony w oknie.

Tworzenie bardziej zaawansowanych kontrolek

Aby możliwe było tworzenie bardziej zaawansowanych kontrolek (takich, jak komponenty typu TProgressBar czy TListView), należy do listy uses dodać moduł CommCtrl. Operować tymi komponentami możemy tylko poprzez komunikaty. Ich spis możesz znaleźć w pliku CommCtrl.pas. Znaczenie poszczególnych komunikatów jest bardzo intuicyjne. Cóż bowiem wykonuje komunikat PBM_SETPOST? Można się domyśleć, że ustawia nową pozycję w komponencie.

Jeżeli uruchomisz program z użyciem biblioteki CommCtrl, a na ekranie nadal widnieć będzie ?czysty? formularz (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.

Przykładowo ? utworzenie nowej kontrolki ? la TProgressBar wygląda następująco:

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

Decydujące znaczenie ma tutaj parametr msctls_progress32. Spis wszystkich parametrów kluczowych dla utworzenia komponentu możesz znaleźć w pliku CommCtrl.pas. Po utworzeniu komponentu można wysyłać do niego komunikaty ? np. dotyczące zmiany pozycji:

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

Jest to zatem pętla od jednego do stu z przerwami pomiędzy kolejnymi iteracjami, wynoszącymi 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 umieścimy w programie obsługi komunikatu WM_PAINT (listing 12.5.)

Listing 12.5. Pełny kod programu

{
   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ątku 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;    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 nową klasę
  InitCommonControls;

// stwórz formularz...
  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.

Pozostałe kontrolki

Nazwy pozostałych kontrolek, z jakich możesz skorzystać w swoich programach, zawarte są w pliku CommCtrl.pas. Tam również możesz znaleźć listę komunikatów związanych z konkretnym komponentem. W tabeli 12.7 prezentuję komponenty, których możesz użyć w swoich programach w API, wraz z ich odpowiednikami w VCL.

Tabela 12.7. Kontrolki z biblioteki CommCtrl.dll

Identyfikator kontrolkiNazwa kontrolkiOdpowiednik VCL
ToolbarWindow32Pasek narzędziowyTToolBar
msctls_statusbar32Pasek aplikacjiTStatusBar
msctls_trackbar32Pasek przewijaniaTTrackBar
msctls_updown32Pasek góra-dółTUpDown
msctls_progress32Pasek postępuTProgressBar
SysListView32Lista pozycjiTListView
SysTreeView32Drzewo obiektówTTreeView
ComboBoxEx32Kontrolka ComboTComboBoxEx
SysTabControl32System zakładekTTabControl
SysAnimate32Animacje systemoweTAnimate
SysMonthCal32KalendarzTMonthCalendar
SysDateTimePick32Prezentuje datę i czasTDateTimePicker
SysIPAddress32Podaje adres IP komputera.brak odpowiednika
SysPagerSystem stronTPageControl

Wyświetlanie grafiki

We wcześniejszych rozdziałach miałeś okazję zapoznać się z funkcjami operującymi na grafice czy też wyświetlającymi tekst. Poznałeś także klasę TCanvas, która owe zadanie znacznie upraszczała. Funkcje API umożliwiające rysowanie lub wyświetlanie grafiki są bardzo podobne do funkcji z klasy TCanvas. Różnica polega jedynie na liczbie parametrów.

Rysowanie w WinAPI

Przypominam, że rysowanie czegokolwiek powinno odbyć się po wywołaniu komunikatu WM_PAINT. Wiadomo, że okno w Windows może podlegać różnym zdarzeniom, takim jak: minimalizacja, możliwość zasłonięcia przez inne okno itp. System nie przechowuje obrazu ekranu w pamięci, lecz odpowiedzialna jest za to sama aplikacja.

Po odsłonięciu okna i przywróceniu go na pierwszy plan do aplikacji wysyłany jest komunikat WM_PAINT ? do nas należy obsłużenie tego komunikatu.

Kontekst urządzenia graficznego

Wszystkie funkcje operujące na WinAPI wymagają podania pierwszego parametru, który jest tzw. kontekstem urządzenia (Device Context). Jest to pewna struktura danych, opisująca różne parametry rysowania ? czcionkę, grubość i kolor linii itp. Takie ustawienia są różne dla każdego programu uruchomionego w systemie, a my w swoich aplikacjach będziemy musieli pobierać uchwyt do owego kontekstu:

DCHandle := GetDC(Wnd);

Od tej pory mamy już uchwyt, który będzie trzeba podawać przy każdej funkcji operującej na grafice. Po zakończeniu malowania należy ten uchwyt zwolnić, korzystając z polecenia ReleaseDC:

ReleaseDC(Wnd, DCHandle);

Za pomocą funkcji GetDC uzyskujemy uchwyt, dzięki któremu możemy malować po całym obszarze roboczym programu. Nie mamy natomiast możliwości rysowania na pasku tytułowym okna ? do tego będzie nam potrzebna funkcja GetWindowDC.

Obsługa WM_PAINT

Istnieje lepsza metoda dostarczania kontekstu urządzenia ? rekord TPaintStruct. Rekord TPaintStruct dostarcza oprócz kontekstu urządzenia także informacje, które można wykorzystać podczas obsługi komunikatu WM_PAINT. Namalowanie czegoś na formularzu opiera się na wywołaniu metody BeginPaint oraz ? po zakończeniu ? EndPaint:

WM_PAINT:
    begin
      DC := BeginPaint(Wnd, PS);
      TextOut(DC, 10, 10, 'Delphi 7', Length('Delphi 7'));
      EndPaint(Wnd, PS);
    end;

Wcześniej jednak należy zadeklarować zmienne PS oraz DC:

var
  PS : TPaintStruct;
  DC  : HDC; // uchwyt

Funkcja TextOut realizuje ? podobnie jak funkcja o tej samej nazwie z klasy TCanvas ? rysowanie tekstu. Pierwszym parametrem musi być uchwyt do kontekstu urządzenia. Kolejne dwa parametry to współrzędne rysowanego tekstu. Trzeci parametr to tekst, który zostanie narysowany na formularzu, a ostatni ? długość tekstu. Rysunek 12.4 prezentuje program po uruchomieniu.

12.4.jpg
Rysunek 12.4. Tekst narysowany metodą TextOut

Zmiana koloru tła

Na rysunku 12.4 widzisz, że dotychczasowy efekt działania funkcji TextOut nie jest zbyt interesujący. Napis jest wyświetlany na białym tle. Aby to zmienić, można ustawić przezroczyste tło, używając w tym celu funkcji SetBkMode:

SetBkMode(DC, TRANSPARENT);   

Pierwszy parametr musi być kontekstem urządzenia, a drugi to flaga informująca o tym, że tło będzie przezroczyste (TRANSPARENT).

W dość prosty sposób można zmienić również kolor wyświetlanego tekstu. Wystarczy zastosować funkcję SetTextColor. Pierwszym parametrem tej funkcji musi być oczywiście kontekst urządzenia. Drugi parametr musi określać kolor tekstu w postaci RGB (Reed Green Blue), czyli kombinacji trzech kolorów: czerwonego, zielonego i niebieskiego ? np.:

SetTextColor(DC, RGB(0, 100, 150));

Ładowanie i wyświetlanie bitmapy

Załóżmy, że w zasobach programu umieszczona jest bitmapa o nazwie ID_BITMAP. Przy wykorzystaniu VCL załadowanie bitmapy do komponentu TImage zajęłoby chwilę ? wystarczyłby jeden wiersz kodu:

Image.Picture.Bitmap.LoadFromResourceName(hInstance, 'ID_BITMAP');

Chcąc skorzystać z funkcji API, musimy poświęcić na to nieco więcej czasu i napisać więcej kodu:
WM_PAINT:

    begin
      DC := BeginPaint(Wnd, PS);

      Bitmap := LoadBitmap(hInstance, 'ID_BITMAP');
      _Bitmap := CreateCompatibleDC(DC);
      SelectObject(_Bitmap, Bitmap);
      BitBlt(dc, 10, 10, 14, 14, _Bitmap, 0, 0, SRCCOPY);
      DeleteDC(_Bitmap);

      EndPaint(Wnd, PS);

    end;

Samo załadowanie jest proste, gdyż używamy tutaj funkcji LoadBitmap, która zwraca bitmapę w postaci zmiennej HBITMAP (odpowiednik klasy TBitmap). Następnym krokiem jest stworzenie pamięciowego kontekstu urządzenia (CreateCompatibleDC), na którym zostanie narysowana bitmapa. Kolejny krok to wybranie właściwego kontekstu za pomocą funkcji SelectObject. Wreszcie samo narysowanie bitmapy następuje poprzez funkcję BitBlt. Pierwszy parametr owej funkcji to uchwyt kontekstu, na którym zostanie narysowana bitmapa. Kolejne dwa parametry to X i Y, współrzędne miejsca wyświetlania obrazka. Rozmiar rysowanej bitmapy określają dwa kolejne parametry. Jeszcze inne dwa parametry określają pozycję X i Y lewego górnego rogu obrazka oraz stopień jego wyświetlania. Ostatni parametr określa sposób przedstawienia bitmapy ? w tym wypadku kopiowanie ze źródła do miejsca przeznaczenia.

Najlepszym sposobem sprawdzenia działania owych parametrów jest przetestowanie ich w praktyce.
Nim skompilujesz swój program, zadeklaruj w nim następujące zmienne:

var
  Bitmap : HBITMAP;
  _Bitmap : HDC;

Ładowanie zasobów

W rozdziale 10. była mowa o wykorzystaniu zasobów do przechowywania w pliku wykonywalnym różnych danych ? począwszy od grafiki, a na innych plikach wykonywalnych skończywszy. W tym podpunkcie pokażę, w jaki sposób skorzystać z tych zasobów, nie używając przy tym klasy TResourceStream.

Korzystając z zasobów, można w WinAPI napisać swój własny instalator, który będzie ?przechowywał? w sobie pliki instalacyjne. Taki przykładowy instalator możesz znaleźć na płycie CD-ROM w katalogu ../listingi/12/Install. W katalogu z projektem znajdziesz również skompilowaną wersję projektu ? plik Install.exe (rysunek 12.5). Po uruchomieniu programu instalacyjnego na dysku zostanie zainstalowany program, który kiedyś napisałem (lecz to jest w tej chwili nie istotne).

12.5.jpg
Rysunek 12.5. Instalator wykonany w WinAPI

Skompilowane zasoby

Tworzenie zasobów za pomocą programu brcc32.exe wygląda tak samo, jak to przedstawiałem w rozdziale 10. ? np. w przypadku mojego instalatora plik files.rc wygląda tak:

MAILBOXES RCDATA "Mailboxes.exe"
SETUP RCDATA "setup.dll"
MAILCNT RCDATA "Mailboxes.cnt"
MAILHLP RCDATA "Mailboxes.hlp"
README RCDATA "Readme.html"
SAMPLE RCDATA "sample.txt"
DEFAULT RCDATA "default.mbx"
UNINSTALL RCDATA "Odinstaluj.exe" 

Tak skonstruowany plik .rc pozwoli na włączenie do gotowego zasobu (.res) powyżej przedstawionych plików.

W rozdziale 10. nie wspomniałem o jednej kwestii ? mianowicie o możliwości tworzenia bardziej zaawansowanych zasobów, np. formularza:

LICENCJA DIALOGEX 42, 8, 271, 133
STYLE DS_MODALFRAME | DS_CENTER | DS_3DLOOK | DS_SETFOREGROUND | WS_POPUP | 
    WS_VISIBLE | WS_CAPTION | WS_SYSMENU
EXSTYLE WS_EX_CLIENTEDGE
CAPTION "Instalacja programu"
FONT 8, "MS Sans Serif"
BEGIN
    ICON            "a", a, 11, 9, 21, 20
    LTEXT           "Najnowszą wersję programu MailBoxes możesz zawsze znaleźć na stronie www.4programmers.net", 
                    a, 37, 11, 221, 20
    DEFPUSHBUTTON   "Instaluj", 102, 101, 100, 101, 14
    PUSHBUTTON      "Wyjście", 103, 208, 100, 47, 14
    GROUPBOX        "Gdzie zainstalować program?", a, 10, 59, 247, 32
    EDITTEXT        106, 16, 72, 233, 14, ES_AUTOHSCROLL
    CONTROL         "BAR", BAR, "msctls_progress32", 0x0 | WS_CLIPSIBLINGS, 
                    14, 42, 241, 11
END

Pisanie tego ręcznie raczej nie ma sensu ? ja używałem pakietu Microsoft Visual Studio, który oferuje tworzenie zasobów. Ty jednak możesz skorzystać z darmowych narzędzi dostępnych w Internecie.

Wykorzystanie zasobów

Ażeby skorzystać z naszych zasobów, będziemy zmuszeni zastosować funkcje WinAPI, których do tej pory nie używaliśmy ? np. LoadResource i FindRecource. Najpierw jednak należy zająć się przedstawieniem formularza instalacyjnego.

Wyświetlenie formularza

Za wyświetlenie formularza znajdującego się w zasobach odpowiada funkcja DialogBox:

function DialogBox(hInstance: HINST; lpTemplate: PChar;
  hWndParent: HWND; lpDialogFunc: TFNDlgProc): Integer;

Pierwszy parametr musi być wskazaniem modułu, w którym znajdują się zasoby ? my w tym miejscu podajemy wartość hInstance. Drugi parametr musi być nazwą szablonu, który chcemy wyświetlić. Parametr trzeci (hWndParent) stanowi uchwyt do okna macierzystego; jako że nasze okno będzie oknem macierzystym, wpisujemy w tym miejscu cyfrę 0. Ostatni parametr to wskazanie procedury, która będzie obsługiwać zdarzenia naszego formularza:

DialogBox(hInstance, 'LICENCJA', 0, @DlgWindowProc);

Funkcja DlgWindowProc, którą podałem w ostatnim parametrze, jest zwykłą funkcją okienkową ? musi odpowiadać na przychodzące do aplikacji komunikaty.

Zamknięcie okna (zakończenie działania aplikacji) zrealizujemy za pomocą polecenia EndDialog:

EndDialog(wnd,0);

Ustawianie wartości komponentów formularza

Mimo że formularz wykorzystany w programie jest jedynie skryptem, posiada on także komponenty. Podczas ustawiania tych komponentów przydzieliłem każdemu z nich osobny identyfikator (ID), do którego będziemy się odwoływać podczas wykonywania funkcji:

SetDlgItemText(wnd, 106, 'C:\Mailboxes 1.53');

Przykładowo funkcja SetDlgItemText powoduje ustawienie nowej wartości kontrolki. W pierwszym parametrze wpisujemy uchwyt okna, a w drugim ID kontrolki. Parametr ostatni to tekst, który ma zostać umieszczony w obiekcie.

LockResource, LoadResource, FindResource

Załadowanie zasobów (tekstu licencji) do kontrolki może się wydać trochę skomplikowane, gdyż trzeba się posłużyć aż trzema funkcjami naraz: LockResource, LoadResource i FindResource.

SetDlgItemText(wnd, 101, LockResource(LoadResource(hinstance,FindResource(hinstance,'LICENCJA','TXT'))));

Pierwsza funkcja ? LockResource ? zwraca wskaźnik do pierwszego bajtu zasobów, jednak wymaga podania uchwytu do zasobów. Ten globalny uchwyt jest natomiast zwracany przez funkcję LoadResource, która w drugim parametrze wymaga podania wskazania ładowanego zasobu. Tutaj pomoże nam funkcja FindResource, w której parametrach wystarczy podać typ oraz nazwę zasobu.

Zapisywanie plików na dysku

Chcąc zastąpić klasę TResourceStream, jesteśmy zmuszeni skorzystać z funkcji LoadResource oraz FindResource. Najpierw w swoim instalatorze zadeklaruj tablicę plików, które są instalowane:

{  oto elementy umieszczone w zasobach }
const Tablica : array[0..7] of _DATA =
  ((Files: 'Mailboxes.exe'; FName: 'MAILBOXES'),
  (Files: 'setup.dll'; FName: 'SETUP'),
  (Files: 'Mailboxes.cnt'; FName: 'MAILCNT'),
  (Files: 'Mailboxes.hlp'; FName: 'MAILHLP'),
  (Files: 'Readme.html'; FName: 'README'),
  (Files: 'Sample.txt'; FName: 'SAMPLE'),
  (Files: 'default.mbx'; FName: 'DEFAULT'),
  (Files: 'Odinstaluj.exe'; FName: 'UNINSTALL'));

Pierwszy element tej tablicy to nazwa pliku, który ma zostać utworzony na dysku; drugi to wskazanie nazwy zasobu. Instalacja (wyodrębnianie) poszczególnych elementów może wyglądać tak:

    For I:= Low(Tablica) to High(Tablica) do
    begin
      AssignFile(Ouff, PC + Tablica[i].Files);  // stwórz plik...
      Rewrite(Ouff, 1);
  {  odnajdź w zasobach zasób i przypisz go zmiennej Fres }
      Fres := FindResource(hInstance, Tablica[i].FName, RT_RCDATA);
   {  do pliku zapisz dane z wyciągniętych zasobów }
      BlockWrite(Ouff, LockResource(LoadResource(hInstance, Fres))^, SizeofResource(hinstance, Fres));
      Closefile(ouff);  // zamknij plik
    end;

Posłużyłem się tutaj funkcjami operującymi na plikach, które szczegółowo omówiłem w rozdziale 7. Po zlokalizowaniu konkretnego zasobu (FindResource) następuje jego zapisanie do pliku (BlockWrite). Pełny kod źródłowy znajduje się w listingu 12.6.

Listing 12.6. Kod źródłowy instalatora

{
  Copyright (c) 2001 by Adam Boduch [http://programowanie.of.pl]
}

program setup;

uses
  Windows,
  Messages;

{$R FILES.RES} // <--- pliki, które zostaną zainstalowane
{$R RESOURCE.RES} // <---- bitmapa ( dodatkowe pliki )

type
{
  rekord zawiera dwa elementy. Pierwszym jest nazwa pliku umieszczonego w zasobach
  ? np. SFP, a drugim elementem jest nazwa pliku, który zostanie zapisany na
  dysku ? np: sfp.jpg
}
 _DATA = packed record
  Files: String;
  FName: PChar;
 end;


{  oto elementy umieszczone w zasobach }
const Tablica : array[0..7] of _DATA =
  ((Files: 'Mailboxes.exe'; FName: 'MAILBOXES'),
  (Files: 'setup.dll'; FName: 'SETUP'),
  (Files: 'Mailboxes.cnt'; FName: 'MAILCNT'),
  (Files: 'Mailboxes.hlp'; FName: 'MAILHLP'),
  (Files: 'Readme.html'; FName: 'README'),
  (Files: 'Sample.txt'; FName: 'SAMPLE'),
  (Files: 'default.mbx'; FName: 'DEFAULT'),
  (Files: 'Odinstaluj.exe'; FName: 'UNINSTALL'));
  

var KeyHandle : HKEY;
    Uninstall : PChar;

const Name : PChar = 'Mailboxes v. 1.5.3';


(*************************************************************************)


function DlgWindowProc (Wnd: hWnd; Msg: UINT; DlgWParam: WPARAM; DlgLParam: LPARAM): boolean; stdcall;
var
  Fres: Integer;
  Ouff: File;
  Buff: array[0..254] of char;
  PC : String;
  I : Integer;
label Next;
begin
  result := true;
  case Msg of
    WM_INITDIALOG:
    begin
      SetDlgItemText(wnd, 101, LockResource(LoadResource(hinstance,FindResource(hinstance,'LICENCJA','TXT'))));
      SetDlgItemText(wnd, 106, 'C:\Mailboxes 1.53');
    end;
    WM_CLOSE: EndDialog(wnd,0);
    wm_activate: SendDlgItemMessage(wnd, 101, EM_SETSEL, ?1, 0);
    WM_COMMAND:
    begin
      if LOWORD(DlgWParam) = 103 then EndDialog(wnd,0);
      if LOWORD(DlgWParam) = 102 then
      begin
      { pobranie ścieżki, w której ma zostać zainstalowany program }
        GetDlgItemText(wnd, 106, Buff, SizeOf(Buff));
        PC := Buff;

        { sprawdzenie, czy na końcu znajduje się znak \ }
        if PC[Length(PC)] <> '\' then PC := PC + '\' else PC := PC;

        Uninstall := PChar(PC + 'Odinstaluj.exe');

      { otwarcie rejestru i klucza }
        RegOpenKeyEx(HKEY_LOCAL_MACHINE, PChar('Software\Microsoft\Windows\CurrentVersion\Uninstall'), 0,
        KEY_ALL_ACCESS, KeyHandle);
      { stworzenie nowej wartości }
        RegCreateKey(KeyHandle, 'Mailboxes', KeyHandle);   
        RegSetValueEx(KeyHandle, 'DisplayName', 0, REG_SZ, Name, SizeOf(Name));
        RegSetValueEx(KeyHandle, 'UninstallString', 0, REG_SZ, Uninstall, SizeOf(Uninstall));


     { sprawdzenie, czy użytkownik nie wpisał tylko litery partycji ? jeżeli nie, należy utworzyć dodatkowo katalog }
        if Length(PC) <= 3 then goto Next else CreateDirectory(PChar(PC), nil);

        Next:


                { instaluj poszczególne elementy }
        For I:= Low(Tablica) to High(Tablica) do
        begin
          AssignFile(Ouff, PC + Tablica[i].Files);  // stworz plik...
          Rewrite(Ouff, 1);
       {  odnajdź w zasobach zasób i przypisz go do zmiennej Fres }
          Fres := FindResource(hInstance, Tablica[i].FName, RT_RCDATA);

    {  do pliku zapisz dane z "wyciągniętych" zasobów }
          BlockWrite(Ouff, LockResource(LoadResource(hInstance, Fres))^, SizeofResource(hinstance, Fres));

          Closefile(ouff);  // zamknij plik
         end;
          MessageBox(0, 'Instalacja przebiegła pomyślnie!', ':)', MB_OK + MB_ICONINFORMATION);
          PostQuitMessage(0); // zakończ program...
      end;
    end;
   else result:=false;
  end;
end;


begin
  if MessageBox(0, 'Za chwilę odbędzie się instalacja programu Mailboxes v. 1.53. Czy chcesz kontynuować?',
    'Mailboxes ? instalacja', MB_YESNO + MB_ICONINFORMATION) = ID_Yes then
    begin
      DialogBox(hInstance, 'LICENCJA', 0, @DlgWindowProc);
    end;
end.

Podsumowanie

Programowanie w czystym WinAPI może być niewygodne, a nawet dość trudne, ale nie da się ukryć, że zapewnia większą kontrolę nad programem i daje większe możliwości, których niekiedy brak w bibliotece VCL.

Załączniki:

de25kp.jpg Więcej informacji

Delphi 2005. Kompendium programisty
Adam Boduch
Format: B5, stron: 1048
oprawa twarda
Zawiera CD-ROM
[[Delphi/Kompendium|Spis treści]]

[[Delphi/Kompendium/Prawa autorskie|©]] Helion 2003. Autor: Adam Boduch. Zabrania się rozpowszechniania tego tekstu bez zgody autora.

0 komentarzy