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).
Dlaczego hInstance nadajemy wartosc poprzez stos a nie poprzez mov? wiem ze nie dziala ale czemu?
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.
Download nie działa
Download nie działa
Trzeba się trochę opisać, żeby prote okno pokazać ;/
Świetny kurs wszystko łopatologicznie wytłumaczone i dość dokładnie :D
Witam,
Bardzo dobrze ….wytłumaczone !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !
Pozdrawiam
Na utworzenie okna jest bardzo prosty sposób. Poprzez DialogBoxParam można wyświetlić okno, o którym informacje przechowujemy w zasobach.
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.
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.