Tworzenie okna

W dzisiejszym odcinku kursu zajmiemy się tym, co w Windows najważniejsze, czyli oknami. Utwórz nowy plik w edytorze, wklej do niego szkielet, który podałem w drugiej części tutoriala i zapisz go pod nazwą tut3.asm.

Deklaracja bibliotek

Zasadniczo do tworzenia okien używa się funkcji API CreateWindowEx. Potrzebuje ona jednak szeregu parametrów, do uzyskania których będziemy musieli skorzystać również z innych funkcji. Znajdują się one w dwóch bibliotekach systemowych: user32 oraz kernel32. Aby można było ich używać, deklarujemy to zaraz na początku programu.

.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib

Przyjrzyjmy się teraz parametrom, które musimy dostarczyć funkcji CreateWindowEx:

  • DWORD dwExStyle – rozszerzony styl
  • LPCTSTR lpClassName – wskaźnik do zarejestrowanej klasy
  • LPCTSTR lpWindowName – tytuł
  • DWORD dwStyle – styl
  • int x – poziome położenie
  • int y – pionowe położenie
  • int nWidth – szerokość
  • int nHeight – wysokość
  • HWND hWndParent – uchwyt okna nadrzędnego
  • HMENU hMenu – uchwyt do menu lub numer kontrolki
  • HINSTANCE hInstance – uchwyt instancji
  • LPVOID lpParam – to nie będzie nam potrzebne, używane przy tworzeniu MDI

Zacznijmy może od końca. Z dobrodziejstw ostatniego parametru nie będziemy w ogóle korzystać, więc wpiszemy na jego miejsce 0. Przejdźmy do kolejnego. Co to jest uchwyt instancji? Uchwyt instancji (ang. handle to application instance) jest to liczba dzięki której Windows jest w stanie rozróżnić wszystkie uruchomione kopie tego samego programu. Jeżeli uruchomimy ten sam program dwa razy, Windows przydzieli każdej jego kopii osobny uchwyt. Uchwyt instancji dla naszego programu pobieramy za pomocą funkcji GetModuleHandle, która zwraca go w rejestrze eax (jak pamiętamy, wszystkie API zwracają wynik swoich działań w eax).

.code
invoke GetModuleHandle,0
mov    hInstance,eax

Ponieważ tą wartość będziemy wykorzystywać później, zapisujemy ją w zmiennej hInstance. Oczywiście musimy jeszcze powiadomić kompilator o tym ile pamięci ma zaalokować (zarezerwować, przydzielić). Robimy to w sekcji data?, gdzie deklarujemy zmienne, których wartości nie znamy na początku działania programu (wartości zmiennej hInstance nie znamy od początku, dowiadujemy się jej dopiero w trakcie działania programu, po wywołaniu GetModuleHandle).

.data?
hInstance HINSTANCE ?

HINSTANCE jest to specjalny typ zmiennej służący właśnie do przechowywania uchwytu instancji.

A więc dwa ostatnie parametry dla CreateWindowEx już mamy, kolejny to hMenu. Na razie żadnego menu nie przewidujemy – podamy 0. Uchwyt okna nadrzędnego również nie będzie nam potrzebny, ponieważ nasze okno będzie oknem głównym. Wysokość, szerokość, położenie oraz styl okna ustalimy przy wywoływaniu funkcji. Pozostał więc tylko ClassName – wskaźnik do zarejstrowanej klasy okna. Od razu uprzedzę Twoje pytanie – klasa jest to zestaw zmiennych (danych) oraz metod, czyli funkcji które mogą realizować wiele zadań, zależnie od potrzeb. Natomiast klasa okna (ang. window class) przechowuje informacje o stylu, ikonie, kursorze i tle okna oraz adres funkcji (tzw. WindowProc), w której reagujemy na różne zdarzenia generowane w oknie, np. na kliknięcie przycisku itp. Klasa okna musi zostać zarejestrowana w systemie, aby stała się dostępna dla aplikacji. Ponieważ zarejestrowanie klasy zajmie nam trochę miejsca, utwórzmy procedurę, w której zarejestrujemy klase, utworzymy okno i umieścimy tzw. message-loop, o którym za chwilę. Nowa procedura nazywać się będzie WinMain – jak nakazuje tradycja pisania w języku C. Zanim ją utworzymy, dopiszmy kod który ją wywoła. Do wywoływania podprogramów służy instrukcja call po której podajemy adres, gdzie rozpoczyna się dana procedura (nie musimy ręcznie obliczać tego adresu, kompilator zrobi to za nas jeżeli wpiszemy po prostu nazwę wywoływanej procedury).

call   WinMain             ;wywołanie procedury
invoke ExitProcess,0       ;zakończenie programu

Oczywiście wyjście z programu nie nastąpi od razu, ale dopiero po zakończeniu procedury WinMain.

Do definiowania procedury służy dyrektywa proc. Przed nią podajemy nazwę, a za – ewentualne parametry, używane rejestry itp. Kod procedury musi zakończyć się dyrektywą endp, przed którą ponownie podajemy nazwę podprogramu.

WinMain proc

Tak wygląda kod rejestrujący klasę okna (opis do każdej linijki znajduje się pod nią):

LOCAL  wc:WNDCLASSEX

Zmienna lokalna (używana tylko w obrębie danej procedury), w której będziemy umieszczać wszystkie dane niezbędne do zarejestrowania klasy. Tą zmienną podamy następnie jako parametr dla funkcji RegisterClassEx.

mov    wc.cbSize,sizeof WNDCLASSEX

Rozmiar struktury WNDCLASSEX.

mov    wc.style,CS_HREDRAW or CS_VREDRAW

Styl klasy. W tym miejscu możemy podać kombinację jednej lub kilku wartości, połączonych za pomocą operatora or, które są dokładnie opisane w Win32 Programmer’s Reference. CS_HREDRAW oznacza, że okno ma zostać odmalowane po zmianie jego szerokości, natomiast CS_VREDRAW – długości. Na uwagę zasługuje również wartość CS_NOCLOSE – jej użycie spowoduje wyłączenie przycisku Zamknij w oknie.

mov    wc.lpfnWndProc,offset WndProc

Adres procedury obsługi zdarzeń (WindowProc), na razie jej nie mamy.

mov    wc.cbClsExtra,0
mov    wc.cbWndExtra,0

Ilość dodatkowej pamięci do zarezerwowania na potrzeby klasy i okna (w bajtach). Nie potrzebujemy jej do naszych celów.

push   hInstance
pop    wc.hInstance

Używając stosu umieszczamy zmienną hInstance w strukturze wc.

mov    wc.hbrBackground,COLOR_WINDOW+1

Kolor tła. COLOR_WINDOW jest to kolor okna, który jest ustawiony w Panelu Sterowania (Ekran, zakładka Wygląd). Pozostałe możliwości to (do każdej z nich należy dodać 1, jak w przykładzie):

  • 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

Niestety, kolor ten zależy od aktualnych ustawień Windows. Ustawianiem niezależnego od systemu tła zajmiemy się w jednej z przyszłych lekcji.

mov    wc.lpszMenuName,0

Menu – nie posiadamy go.

mov    wc.lpszClassName,offset Klasa

Adres zmiennej zawierającej nazwę klasy. Po tej nazwie klasa jest rozpoznawana, m.in. w funkcji CreateWindowEx. Zmienna Klasa jeszcze nie istnieje, musimy więc utworzyć ją w sekcji data (zawartość zmiennej może być dowolna, nie może jednak określać klasy, która już występuje w systemie):

.data
Klasa db "WinClass",0
invoke LoadIcon,0,IDI_APPLICATION
mov    wc.hIcon,eax
mov    wc.hIconSm,eax

Wczytanie ikony z zasobów systemowych i ustawienie jej jako ikonę okna. Zamiast IDI_APPLICATION możemy podać również:

  • IDI_APPLICATION
  • IDI_ASTERISK
  • IDI_EXCLAMATION
  • IDI_HAND
  • IDI_QUESTION
  • IDI_WINLOGO
invoke LoadCursor,0,IDC_ARROW
mov    wc.hCursor,eax

Wczytanie i ustawienie kursora. Wybrany tutaj kursor będzie ustawiany, gdy wskaźnik myszy znajdzie się wewnątrz okna. IDC_ARROW można zastąpić jedną z następujących wartości:

  • IDC_APPSTARTING
  • IDC_ARROW
  • IDC_CROSS
  • IDC_IBEAM
  • IDC_ICON
  • IDC_NO
  • IDC_SIZE
  • IDC_SIZEALL
  • IDC_SIZENESW
  • IDC_SIZENS
  • IDC_SIZENWSE
  • IDC_SIZEWE
  • IDC_UPARROW
  • IDC_WAIT

Ikonę i kursor możemy również narysować samodzielnie. Zajmiemy się tym w przyszłości.

invoke RegisterClassEx,addr wc

Wywołanie funkcji rejestrującej klasę okna.

invoke CreateWindowEx,\
       0,\                                  ;rozszerzony styl
       addr Klasa,\                         ;nazwa klasy
       addr Tytul,\                         ;tytuł
       WS_OVERLAPPEDWINDOW or WS_VISIBLE,\  ;styl
       100,\                                ;pozycja x
       100,\                                ;pozycja y
       320,\                                ;szerokość
       200,\                                ;wysokość
       0,\                                  ;uchwyt okna nadrzędnego
       0,\                                  ;uchwyt menu
       hInstance,\                          ;uchwyt instancji
       0                                    ;do MDI

Tworzenie okna. Wszystkie parametry zostały już opisane w tekscie. Możliwości doboru stylu okna jest tak wiele, że nie widzę sensu ich tutaj opisywać, zwłaszcza że wszystko jest dokładnie podane w Win32 Programmer’s Reference (hasła CreateWindow oraz CreateWindowEx w indeksie). Brakuje jedynie tytułu okna. Jest to napis, który zostanie wyświetlony na pasku tytułowym u góry okna. Zmienną Tytuł deklarujemy w sekcji data:

Tytul db "Kurs MASM32 - Pierwsze okno",0

Teraz możemy się zająć się tzw. message-loop. Jest to pętla, która odczytuje informacje o zaistniałych w oknie zdarzeniach (są to tzw. messages) i przekazuje je do WindowProc, chyba że otrzyma message WM_QUIT – wtedy powoduje zakończenie procedury WinMain i kontynucje wykonywania programu od miejsca w którym nastąpiło wywołanie tej procedury (czyli w naszym przypadku skoczy w miejsce za instrukcją „call WinMain” i spowoduje zakończenie programu).

.WHILE TRUE
  invoke GetMessage,addr msg,0,0,0
  .BREAK .IF (!eax)
  invoke TranslateMessage,addr msg
  invoke DispatchMessage,addr msg
.ENDW
ret

Teraz możemy już zakończyć procedurę WinMain i przejść do WndProc czyli procedury obsługi zdarzeń (WindowProc).

WinMain endp

WndProc proc hWnd:HWND,uMsg:UINT,wParam:WPARAM,lParam:LPARAM

Procedura ta jest wywoływana w momencie wystąpienia jakiegoś zdarzenia. Może to być np. ruch myszą, kliknięcie przycisku, zamknięcie systemu itp. Za słowem proc znajdują się zmienne (i ich typy), które „dostajemy” od systemu w momencie wywołania procedury.

hWnd jest to uchwyt naszego okna. Podobnie jak uchwyt instancji, uchwyt okna jest to liczba której Windows używa do identyfikacji okna. Jest ona różna dla każdego z nich.

Zmienna uMsg identyfikuje zdarzenie które spowodowało wywołanie procedury. Przyjmuje ona różną wartość w zależności od niego, np. w momencie kliknięcia przycisku uMsg jest równe WM_COMMAND, a niszczenia okna – WM_DESTROY. Pozwala ona odpowiednio zareagować programiście na zaistniałe zdarzenie.

wParam i lParam są to parametry dla uMsg, a ich wartość również zależy od zdarzenia – np. w momencie kliknięcia myszą w oknie zawierają one aktualne współrzędne kursora.

Aby sprawdzić jakie zdarzenie wystąpiło i odpowiednio na nie zareagować stosujemy poznaną już pseudo-instrukcję IF.

.IF uMsg==WM_DESTROY
  invoke PostQuitMessage,0

W momencie zniszczenia okna wyślij message WM_QUIT, które spowoduje zamknięcie okna i wyjście z procedury WinMain.

.ELSE
  invoke DefWindowProc,hWnd,uMsg,wParam,lParam
  ret
.ENDIF

W innym przypadku wywołaj standardową procedurę obsługi okna.

xor   eax,eax
ret
WndProc endp
end start

Zakończenie procedury WndProc i zakończenie pisania kodu („end start”).

Dzisiejsza lekcja może wydać Ci się nieco skomplikowana, ale wiedz że tak nie jest – spora ilość kodu wymagana do utworzenia okna pozwala bardzo dokładnie skonfigurować jego parametry i wygląd, a także reagować na wysyłane messages. A program który dzisiaj stworzyliśmy powtarza się właściwie w każdej Windowsowej aplikacji, tak więc możesz używać go jako bazę dla swoich przyszłych projektów.

Cały kod (plik ASM) oraz wersję skompilowaną programu (EXE) można pobrać tutaj (1 380 bajtów).

10 komentarzy do “Tworzenie okna

  1. Yak

    Dlaczego hInstance nadajemy wartosc poprzez stos a nie poprzez mov? wiem ze nie dziala ale czemu?

    Odpowiedz
  2. Czajnick

    Ponieważ procesor nie ma takiej instrukcji jak mov adres,adres. W zwiazku z tym nie mozemy napisac mov wc.hInstance,hInstance poniewaz procesor po prostu nie będzie wiedział jak to wykonać. Zamiast tego musimy skorzystac z rejestru pomocniczego albo stosu.

    Odpowiedz
  3. Neom323

    Witam,

    Bardzo dobrze ….wytłumaczone !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !

    Pozdrawiam

    Odpowiedz
  4. mo4x

    Na utworzenie okna jest bardzo prosty sposób. Poprzez DialogBoxParam można wyświetlić okno, o którym informacje przechowujemy w zasobach.

    Odpowiedz
  5. dave

    Dzień dobry,
    Pamiętam Pana z dawnych czasów.
    Na stronie widzę HTB Installer. Ponadczasowy old school :)
    Używa Pan nadal Asemblera?
    Teraz jest już MASM64, nowszy od MASM32.

    Odpowiedz
    1. robinski Autor wpisu

      Dzień dobry :). Ja również kojarzę ksywkę :). Dzięki za przypomnienie dawnych czasów. Niestety assemblera nie używam już od wielu lat. Teraz to głównie robię w PHP (tak, wiem, „Programista PHP hehehe”). Może kiedyś jeszcze do asma wrócę, kto wie. Pozdrawiam.

      Odpowiedz

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *