Tìm hiểu về cửa sổ ứng dụng Trong bài viết này, chúng ta sẽ xây dựng một chương trình với đầy đủ chức năng của một cửa sổ ứng dụng trên desktop.. Chỉ một cửa sổ trong một thời điểm nhận
Trang 1Tìm hiểu về cửa sổ ứng dụng
Trong bài viết này, chúng ta sẽ xây dựng một chương trình với đầy đủ chức năng của một cửa sổ ứng dụng trên desktop
Tìm hiểu về cửa sổ, lớp cửa sổ
Trong một ứng dụng đồ họa 32-bit, cửa sổ (window) là một vùng hình chữ nhật
trên màn hình, nơi mà ứng dụng có thể hiện thị thông tin và nhận thông tin vào từ người dùng (users) Do vậy, nhiệm vụ đầu tiên của một ứng dụng đồ họa 32-bit là tạo một cửa
sổ
Một cửa sổ sẽ chia sẻ màn hình với các cửa sổ khác trong cùng một ứng dụng hay với các ứng dụng khác Chỉ một cửa sổ trong một thời điểm nhận được thông tin nhập từ người dùng thông qua bàn phím, thiết bị chuột hay các thiết bị nhập liệu khác để tương tác với cửa sổ và ứng dụng
Tất cả các cửa sổ đều đựơc tạo từ một cấu trúc được cung cấp sẵn gọi là lớp cửa sổ
(window class) Cấu trúc này là một tập hợp các mô tả các thuộc tính mà hệ thống dùng
như khuôn mẫu để tạo nên các cửa sổ Mỗi một cửa sổ phải là thành viên của một lớp cửa
sổ Tất cả các lớp cửa sổ này đều được xử lý riêng biệt
Trang 2Các chương trình trên Windows thường sử dụng các API để thiết kế giao diện
(GUI) cho chúng Bằng cách này, nó có lợi cho cả người sử dụng và cả lập trình viên Đối
với người sử dụng, họ không cần phải tìm hiểu làm thế nào để sử dụng giao diện đồ họa
cho mỗi chương trình mới, giao diện của các chương trình là tương tự nhau Đối với các
lập trình viên, mã lệnh để tạo nên giao diện chương trình đã sẵn, được test và sẵn sàng
cho việc sử dụng Nhưng mặt khác, việc giao diện được thiết kế sẵn theo khuôn mẫu của
HĐH sẽ làm tăng thêm sự tạp trong việc các lập trình muốn cài đặt hay thao tác trên các
đối tượng bất kỳ của giao diện như là hộp thoại , menus, icons, … vì họ phải tuân theo
một nguyên tắc chính xác, nhưng điều này có thể khắc phục bằng cách lập trình theo đơn
thể hoặc theo hướng đối tượng (OOP)
Tôi sẽ phát thảo những bước cần thiết để xây dựng cửa sổ ứng dụng trên desktop:
1 Lấy handle của thể hiện – chính là bản thân ứng dụng
Handle là một số nguyên không dấu 32bit do HĐH tạo ra để làm định danh cho mỗi đối
tượng
Còn lý do tại sao phải xin HĐH cấp một handle và vì sao gọi là instance handle
(handle của thể hiện) Vì Windows là HĐH đa nhiệm, có thể có nhiều bản của cùng một
chương trình cùng chạy vào cùng một thời điểm nên phải cần phải có 1 handle của chương
trình ngay lúc chúng đang được active (nghĩa là đang được thao tác) Ngoài ra, để quản lý
chặt chẽ hơn, HĐH còn có khái niệm hPrevinst là chỉ số của bản đã được khởi động trước đó
và chúng luôn có giá trị NULL
2 Lấy địa chỉ của xâu ký tự các đối số dòng lệnh (command line), không cần thiết
trừ khi chương trình thực hiện bằng dòng lệnh)
3 Định nghĩa lớp cửa sổ và đăng ký nó với HĐH (trừ khi bạn dùng loại cửa sổ đã
được HĐH định nghĩa trước như MessageBox hay Dialogbox)
4 Tạo lập cửa sổ làm việc cho ứng dụng
5 Hiển thị cửa sổ trên desktop (trừ khi bạn không muốn hiển thị cửa sổ ngay)
6 Luôn luôn vẽ (paint) và vẽ lại (repaint) vùng client của cửa sổ
Hiểu như thế nào là vẽ và vẽ lại? Có nghĩa là người dùng có thể di chuyển cửaa sổ của một hay nhiều
chương trình khác trên màn hình và sự di chuyển sẽ che một phần cửa sổ của ứng dụng HĐH sẽ
không lưu phần cửa sổ mà bị chương trình khác che Khi cửa sổ bị/được di chuyển, HĐH sẽ gửi yêu
cầu vẽ lại vùng client của cửa sổ
Trang 37 Nhận và xử lý thông điệp thông qua vòng lặp thông điệp
Chúng ta xét qua lược đồ quản lý sự kiện và thông điệp của HĐH :
Trình tự xử lý thông điệp được mô tả như sau:
Nhận một thông điệp thông qua hàm API GetMessage() từ hàng đợi thông điệp của ứng dụng Ngoại trừ thông điệp WM_QUIT, mỗi thông điệp sẽ trả về cho hàm GetMessage() một thông số khác 0 (TRUE) Khi nhận thông điệp WM_QUIT thì chương trình sẽ thoát khỏi vòng lặp và
chấm dứt hoạt động chương trình Hàm GetMessage() này nhận vào 4 tham số đầu vào trong đó tham số thứ 1 quan trọng là con trỏ trỏ về một cấu trúc kiểu MSG
Sau đó, giải mã thông điệp này thông qua hàm API TranslateMessage() Hàm này sẽ gọi trình
điều khiển bàn phím (Keyboard driver) để chuyển đổi những thông điệp từ bàn phím do người dùng gõ (ví dụ WM_KEYDOWN, WM_KEYUP, …) thành những ký tự để đưa các trị này vào hàng đợi thông điệp của ứng dụng dưới dạng thông điệp WM_CHAR Nghĩa là chương trình của
Trang 4bạn có đủ khả năng phân biệt chữ A (a hoa) và a (a thường) mà không cần biết có phím SHIFT đang hoạt động
Rồi sau đó, phân công giải quyết bởi một hàm thích hợp thông qua hàm API DispatchMessage()
để xem dữ kiện chứa trong cấu trúc MSG để gọi cho đúng hàm giải quyết thông điệp Hàm này sẽ push thông điệp vào Window Procedure để bắt đầu xử lý thông điệp
Vòng lặp While tiếp tục chạy cho đến khi nào GetMessage() trả về giá trị 0 cho biết là không còn cái
thông điệp nào trong hàng đợi thông điệp và lúc này thông điệp chấm dứt
Hệ điều hành Windows sẽ phân công giải quyết các thông điệp thông qua một thủ tục gọi là Windows
Procedure () và thường được lập trình viên đặt tên là WinProc() Đoạn code trong thủ tục WndProc() này
là phần mà bạn phải nhọc công lập trình
Các thông số bên trong các lớp Window (Window Class - WNDCLASS), các hàm API như GetMessage(), ShowWindow(), … bạn có thể tự tìm hiểu thông qua MSDN hoặc ebook Microsoft® Win32® Programmer's Reference
8 Nếu thông điệp được gởi tới cửa sổ, chúng sẽ được xử lý bởi một hàm đặc biệt (được gọi là Window Procedure) của cửa sổ
Trong mỗi chương trình trên nền Windows, bộ phận trung tâm là 1 hoặc nhiều hàm nhận và xử lý
các thông điệp Các hàm này được gọi là Window Procedure() viết tắt là WndProc() Đứng về mặt lập
trình, thì cấu trúc của WndProc thường là lệnh switch() với những trường hợp cho những thông điệp khác
nhau Trong trường hợp của chúng ta, chỉ có hai case là xử lý thông điệp WM_PAINT và thông điệp WM_DESTROY Trong thực tế các ứng dụng thường chứa một switch khổng lồ với số lượng trường hợp
nhiều kinh khủng
Giá trị trả về của WndProc() tùy thuộc vào loại thông điệp Mỗi WndProc() đều cần 4 tham số đầu vào :
hwnd : handle của thông điệp msg và 2 thông số khác là wparam và lparam Tham số hwnd này
rất quan trọng vì một WndProc() có thể hỗ trợ nhiều cửa sổ khác nhau trên màn hình cùng một
lúc Do đó, tham số hwnd sẽ nhận diện cửa sổ nào đã gọi WndProc() của chúng ta
msg : cho số thứ tự của thông điệp Nếu hwnd cho biết ai gọi thì msg cho biết nó muốn gì Có thể
Có thể nó muốn nói là cửa sổ mới tạo xong (WM_CREATE) hoặc di chuyển đến vị trí mới (WM_MOVE) hoặc thay đổi kích thước (WM_SIZE) hoặc vẽ lại ứng dụng (WM_PAINT) hoặc khi người dùng nhấn nút Close đóng cửa sổ ứng dụng lại (WM_DESTROY)
9 Thoát chương trình nếu người dùng đóng cửa sổ
Như bạn thấy, cấu trúc của một chương trình chạy trên nền Windows khá phức tạp so với chương trình chạy trên nền Dos Nhưng trong thế giới Windows khác hoàn toàn so với thế giới Dos Những
chương trình Winodws có thể “chung sống” êm thắm với nhau Chúng phải tuân thủ theo các nguyên tắc
Bạn - một lập trình viên - càng phải nghiêm ngặt hơn trong thói quen và phong cách lập trình của mình
Trang 5Xây dựng một ứng dụng Win32 ASM đơn giản
Dưới đây là source code xây dựng một cửa sổ ứng dụng đơn giản Trước khi đi vào các chi tiết phức tạp trong việc lập trình Win32 ASM , tôi sẽ điểm qua một số điểm quan trọng để cho bạn lập trình được dễ dàng
Bạn nên sẽ đặt tất cả các hằng số, cấu trúc và các khai báo hàm (prototypes) trong một file include và include nó ngay lúc bắt đầu code của file asm của bạn Nó sẽ giúp bạn tránh được lỗi do gõ sai Hiện thời, file include hòan chỉnh nhất cho
MASM là windows.inc Bạn cũng có thể định nghĩa những hằng số cho riêng bạn
và những định nghĩa cấu trúc ,nhưng bạn sẽ đặt chúng vào trong một file include
riêng biệt với windows.inc
Dùng chỉ thị includelib để chỉ rõ thư viện nhập nào được sử dụng trong chương trình của bạn Ví dụ, nếu chương trình của bạn gọi MessageBox, bạn sẽ gọi chỉ thị includelib với cú pháp như sau: includelib user32.lib tại vị trí bắt đầu trong file .asm của bạn Chỉ thị này nói cho MASM biết rằng chương trình của bạn có sử
dụng những hàm trong thư viện nhập có tên là user32.lib Nếu chương trình gọi
những hàm mà những hàm này nằm trong nhiều hơn một thư viện nhập, chỉ cần thêm một includelib cho mỗi thư viện bạn dùng Bằng cách sử dụng chỉ thị includelib, bạn sẽ không phải lo lắng về các thư viện nhập này trong lúc liên kết chương trình để tạo ra file thực thi (file EXE) Bạn có thể dùng /LIBPATH để nói cho trình liên kết biết tất cả các thư viên đó ở đâu
Khi khai báo prototypes các hàm API, cấu trúc hay hằng số trong file include của bạn, hãy cố bám sát các tên gốc được dùng trong file include của HĐH Điều này
giúp bạn đỡ phải tra cứu một vài mục Win32 API reference
Sử dụng makefile để trình biên dịch tự động dịch chương trình của bạn Điều này
sẽ giúp bạn tiết kiệm thời gian thay vì phải gõ lệnh biên dịch
.386
.model flat,stdcall
option casemap:none
include windows.inc
include user32.inc
includelib user32.lib ; Goi cac ham trong user32.lib
; va kernel32.lib
include kernel32.inc
includelib kernel32.lib
; Khai bao prototype ham WinMain
WinMain proto :DWORD,:DWORD,:DWORD,:DWORD
.DATA ; Nhung du lieu duoc khoi tao truoc
ClassName db "SimpleWinClass",0 ; Ten cua lop cua so
AppName db "Our First Window",0 ; Ten cua cua so
Trang 6.DATA? ; Nhung du lieu khong duoc khoi tao
hInstance HINSTANCE ? ; handle cua the hien cua ung dung
CommandLine LPSTR ?
.CODE ; Here begins our code
start:
invoke GetModuleHandle, NULL ; Lay handle cua the hien cua ung dung
hInstance,eax
mov hInstance,eax
invoke GetCommandLine ; Lay dia chi cua xau ky tu chua
; cac doi so dong lenh
; Ban khong can goi ham nay neu nhu
mov CommandLine,eax
invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT ; Goi ham
; WinMain
invoke ExitProcess, eax ; Thoat khoi ung dung
; Gia tri tra ve cua ExitProcess duoc
WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,
CmdLine:LPSTR,CmdShow:DWORD LOCAL wc:WNDCLASSEX ; Tao cac bien cuc bo trong ngan xep
LOCAL msg:MSG
LOCAL hwnd:HWND
mov wc.cbSize,SIZEOF WNDCLASSEX ; Dien day du gia tri cho
; cac bien cua lop cua so
mov wc.style, CS_HREDRAW or CS_VREDRAW
mov wc.lpfnWndProc, OFFSET WndProc
mov wc.cbClsExtra,NULL
mov wc.cbWndExtra,NULL
push hInstance
pop wc.hInstance
mov wc.hbrBackground,COLOR_WINDOW+1
mov wc.lpszMenuName,NULL
mov wc.lpszClassName,OFFSET ClassName
invoke LoadIcon,NULL,IDI_APPLICATION
mov wc.hIcon,eax
mov wc.hIconSm,eax
invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,eax
invoke RegisterClassEx, addr wc ; Dang ky lop cua so voi HDH
invoke CreateWindowEx,NULL,\
ADDR ClassName,\
ADDR AppName,\
WS_OVERLAPPEDWINDOW,\
CW_USEDEFAULT,\
CW_USEDEFAULT,\
CW_USEDEFAULT,\
CW_USEDEFAULT,\
NULL,\
NULL,\
hInst,\
NULL
mov hwnd,eax
invoke ShowWindow, hwnd,CmdShow ; Hien thi cua so ra desktop
invoke UpdateWindow, hwnd ; Ve va ve lai vung client cua cua so
Trang 7.WHILE TRUE ; Vong lap thong diep
invoke GetMessage, ADDR msg,NULL,0,0
BREAK IF (!eax)
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.ENDW
mov eax,msg.wParam ; Tra gia tri trong thanh ghi eax
ret
WinMain endp
WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
.IF uMsg==WM_DESTROY ; Neu nguoi dung dong cua so dong cua so invoke PostQuitMessage,NULL ; thi thoat ung dung
.ELSE
invoke DefWindowProc,hWnd,uMsg,wParam,lParam
; Nguoc lai thi xu ly thong diep theo mac dinh cua HDH
ret
.ENDIF
xor eax,eax
ret
WndProc endp
end start
Phân tích mã lệnh
Bạn đã thấy một chương trình tạo một cửa sổ ứng dụng đơn giản cần phải code rất nhiều mã lệnh như trên Nhưng phần lớn những đoạn code đó chỉ là một template code (mã nguồn mẫu) mà bạn có thể copy từ mã lệnh của file này đến mã lệnh của một file khác Hay nếu bạn thích, bạn có thể dịch một vài đoạn mã nguồn đó vào trong một thư viện để dùng như một đọan mở đầu hay đọan code cuối Bạn có thể chỉ viết code trong hàm WinMain Thật vậy, đây là những gì mà trình biên dịch C đã làm sẵn Chúng cho phép bạn viết code hàm WinMain không cần phải lo làm những công việc “vặt vảnh” khác Khác với trình biên dịch C là bạn phải có một hàm có tên là WinMain, nếu không
sẽ không thể kết hợp code của bạn với đoạn đầu hay đọan cuối của code Bạn không bị hạn chế vấn đề này trong ngôn ngữ ASM Bạn có thể dùng bất kỳ tên hàm nào không nhất thiết là WinMain hoặc không có hàm nào cũng không sao
Tiếp theo là những giải thích về đoạn code đã viết ở trên Nó rất dài, chúng ta hãy cùng nhau phân tích chương trình này để làm rõ vấn đề xây dựng một ứng dụng Win32 ASM không đến nổi quá phức tạp như bạn nghĩ
.386
.model flat,stdcall
option casemap:none
WinMain proto :DWORD,:DWORD,:DWORD,:DWORD
include windows.inc
include user32.inc
include kernel32.inc
includelib user32.lib
includelib kernel32.lib
Trang 8Ba dòng trước tiên rất cần thiết vì:
.386: nói cho MASM biết rằng chúng ta có ý định sử dụng cấu trúc bộ lệnh 80386 trong chương trình
.model flat,stdcall: nói cho MASM rằng kiểu bộ nhớ đang sử dụng cho chương trình là
kiểu flat memory Cũng như bạn sẽ dùng tham số stdcall để quy ướ cách truyền tham số cho hàm như thế nào
option casemap:none nói cho MASM biết rằng tên các nhãn trong chương trình phân biệt chữ hoa, chữ thường, vì vậy ExitProcess sẽ khác với exitprocess
Kế đến là khai báo prototype của hàm WinMain Sau này, khi chúng ta gọi hàm WinMain, chúng ta ta phải định nghĩa lại prototype của hàm để có thể sử dụng được hàm invoke
Chúng ta phải include file windows.inc phần bắt đầu trong mã nguồn Nó chứa các cấu
trúc quan trọng, các hằng số được dùng trong chương trình của chúng ta File include
windows.inc chỉ là một file text Bạn có thể mở nó với bất kỳ trình sọan thảo vănbản nào Xin chú ý rằng file windows.inc không chứa tất cả các cấu trúc và các hằng số Bạn có
thể thêm các đối tượng mới (cấu trúc, hằng số, …) mới nếu không có trong file
Chương trình ứng dụng gọi các hàm API chứa trong user32.dll (CreatWindowEx, RegisterWindowClassEx và gọi hàm API chứa trong kernal32.dll (ExitProcess), vì thế
chúng ta phải liên kết chương trình với các thư viện nhập này Câu hỏi kế tiếp là: “Làm thế nào tôi biết được thư viện nhập nào được liên kết với chương trình của chúng ta?” Bạn phải biết API nằm ở đâu để có thể gọi các hàm API Ví dụ: nếu bạn gọi một hàm API
trong gdi32.dll , bạn phải liên kết với với gdi32.lib
Đây là cách mà MASM giải quyết vấn đề Còn đối với TASM, cách liên kết với các thư viện nhập thì đơn giản hơn, chỉ cần liên kết đến một và chỉ một file mà thôi:
import32.lib
.DATA
ClassName db "SimpleWinClass",0
AppName db "Our First Window",0
.DATA?
hInstance HINSTANCE ?
CommandLine LPSTR ?
Kế đến là section “DATA”
Trong section .DATA, chúng ta khai báo những chuỗi kết thúc bằng zero
Classname là tên của lớp cửa sổ (Window class)
Appname là tên cửa sổ ứng dụng của chúng ta
Trang 9Chú ý rằng: hai biến này được khởi tạo giá trị từ ban đầu
Trong section .DATA?, hai biến được khai báo:
hInstance: viết tắt của 2 từ instance handle – là định danh của thể hiện của chương trình được HĐH cấp phát
CommandLine : địa chỉ của xâu ký tự dòng lệnh của chương trình
Bạn để ý biến hInstance chúng ta báo kiểu dữ liệu là HINSTANCE và biến CommandLine khai báo kiểu LPSTR Đây là 2 kiểu dữ liệu mới, nhưng thật ra chính là một tên khác cho kiểu DWORD, là số nguyên 32-bit không dấu Bạn có thể nhìn thấy chúng trong windows.inc
HINSTANCE typedef DWORD
LPSTR typedef DWORD
Chú ý rằng tất cả các biến trong section DATA? không được gán giá trị khởi tạo ban
đầu, đó là do chúng không lưu giữ một giá trị nào khi chương trình được load lên bộ nhớ, nhưng chúng ta muốn dành một vùng nhớ để sau này sử dụng
.CODE
start:
invoke GetModuleHandle, NULL
mov hInstance,eax
invoke GetCommandLine
mov CommandLine,eax
invoke WinMain, hInstance,NULL,CommandLine,
SW_SHOWDEFAULT
invoke ExitProcess,eax
end start
Section .CODE chứa tất cả các chỉ thị lệnh Các mã lệnh của bạn phải nằm giữa nhãn
<nhãn>: và end <nhãn> Tên của nhãn không quan trọng Bạn có thể đặt tên cho nó bất
kỳ và miễn sao không trùng với các từ khóa, toán tử của MASM
Chỉ thị đầu tiên của chúng ta là gọi hàm GetModuleHandle để lấy instance handle của
cửa sổ ứng dụng Dưới Win32, handle của thể hiện của chương trình và handle của đơn
thể là một và giống nhau (tức instance handle = module handle) Bạn có thể tưởng tượng rằng instance handle như là một ID của cửa sổ chương trình Nó được sử dụng
như một tham số cho các hàm API khác mà chương trình bạn cần gọi đến, vì vậy thông thường một ý tưởng tốt là copy ID này và lưu nó (hay lưu giữ nó) với một tham số khác
có tên là module handle ngay lúc chương trình bắt đầu thực thi
Chú ý: thực tế dưới platform Win32, instance handle là một địa chỉ tuyến tính
(linear address) của chương trình trong bộ nhớ
Trang 10Vậy địa chỉ tuyến tính là gì ?
Lập trình Win32 dựa trên cơ sở flat memory model, mỗi chương trình hoạt động một cách độc lập trên 1
đoạn địa chỉ cơ sở được gọi là linear address space (chứa đựng code, data, và đoạn mã con trong stack -
lưu ý: mỗi địa địa chỉ linear address space chứa tối đa khoảng 2^32-1 ô nhớ, bất kỳ địa chỉ nào trong linear address space gọi là linear address)
Trở lại đoạn code trên, ta có thể tìm giá trị trả về của hàm trong Win32 trong thanh ghi EAX Tất cả những giá trị khác được trả về qua những biến được chuyển vào danh sách tham số của hàm mà bạn đã định nghĩa cho lời gọi hàm
Một hàm Win32 mà bạn gọi, chúng sẽ luôn cất giữ giá trị của các thanh ghi đoạn và các thanh ghi EBX,EDI,ESI và EBP Trái lại, ECX và EDX được xem xét là thanh ghi dùng
để xóa giá trị luôn luôn không được dùng để lưu trữ giá trị trả về của một hàm
Chú ý: Không có gì chắc rằng giá trị của các thanh ghi EAX, ECX và EDX được cất giữ thông qua lời gọi hàm API
Dòng tiếp theo (mov hInstance,eax) có nghĩa là: khi gọi một hàm API, luôn bảo đảm rằng giá trị trả về của hàm được chứa trong thanh ghi EAX Nếu bất cứ hàm nào của bạn được gọi bởi HĐH, bạn phải làm đúng theo nguyên tắc sau: cất giữ và phục hồi giá trị của các thanh ghi đoạn và EBX, EDI, ESI, EBP trước khi hàm trả về giá trị Nếu không, chương trình của bạn sẽ bị lỗi ngay lập tức, nguyên tắc này áp dụng bao gồm cả Windows
Procedure (WndProc) và hàm CALLBACK
Lời gọi hàm GetCommandLine không cần thiết nếu chương trình của bạn không thực thi bằng dòng lệnh Trong ví dụ này, tôi sẽ chỉ cho bạn gọi nó như thế nào trong trường hợp bạn cần nó trong chương trình
Kế đến là lời gọi hàm WinMain
Trong các chương trình C/C++ viết trên môi trường DOS, thì thường hàm main() được xem là điểm vào chính thức của chương trình (tức là khi bạn nhấn CTRL + F5 để thực thi chương trình thì hệ điều hành sẽ gọi hàm WinMain() này thực hiện trước, và hệ điều hành sẽ load ứng dụng của bạn lên trên bộ nhớ RAM
Do đó, WinMain còn được gọi với tên là Program Entry Point hoặc là Orginal Entry Point
int WINAPI WinMain(
HINSTANCE hInstance, // handle to current instance
HINSTANCE hPrevInstance, // handle to previous instance
LPSTR lpCmdLine, // pointer to command line
int nCmdShow // show state of window
);