You are on page 1of 80

LẬP TRÌNH HỆ THỐNG

3.2. LẬP TRÌNH HỆ THỐNG TRÊN WINDOWS


3.2.1. Giới thiệu về lập trình trên Windows
Hệ điều hành Windows được phát triển bởi hãng Microsoft, hiện nay được đại
đa số người dùng máy tính cá nhân sử dụng và khai thác. Như
một hệ điều hành bất kỳ, Windows cung cấp nhiều tính năng
cho người dùng rất thuận tiện và đặc biệt là rất nhiều dịch vụ để
các chương trình ứng dụng thực thi công việc.
Sau đây chúng ta xem xét một số nét chính của Windows.
a) Các đặc trưng cơ bản của Windows
 Tính đa nhiệm : tại một thời điểm máy có thể thực hiện được nhiều công

việc khác nhau (tức chạy nhiều chương trình đồng thời). Theo đó, tài
nguyên của máy được phân rã thành nhiều đơn vị nhỏ và phân phối đến các
chương trình đang chạy để xử lý, có thể minh họa bằng sơ đồ hình dưới.
Có 3 công việc hay chương trình (task1, task2, task3) đang thực hiện, khi
đó theo thời gian máy sẽ luân chuyển việc thực hiện các công việc theo đơn
vị thời gian đã phân bổ.

task task1

task2

task3

time

Như vậy ở góc độ lập trình, chúng ta cần biết một chương trình khi chạy
được phân phối tài nguyên theo nguyên tắc ưu tiên. Trong đó, một số mức
ưu tiên thường dùng như sau:
 Realtime: ưu tiên thời gian thực
 High: mức ưu tiên cao
 Normal: mức ưu tiên bình thường
 Low: mức ưu tiên thấp,...

1
LẬP TRÌNH HỆ THỐNG

Vậy khi chạy một chương trình, bản thân chương trình đó phải đăng ký
mức ưu tiên để hệ thống biết phân phối tài nguyên thích hợp.
 Môi trường giao diện đồ hoạ: đây là một đặc trưng đem đến cho người
dùng sự thuận tiện, dễ dùng và trực quan khi sử dụng Windows. Điều này
đòi hỏi các chương trình ứng dụng phải áp dụng xử lý đồ họa để kết quả
giao diện đơn giản, thuận tiện cho người dùng. Tuy vậy, cũng có những
trường hợp phần mềm chỉ chạy trên giao diện cửa sổ dạng văn bản (text)
hoặc kết hợp cả giao diện đồ họa và text.
 Cung cấp quản lý hầu hết các thiết bị ngoại vi: một thuật ngữ hình thành
trên Windows đó là “plug & play”, đây chính là sự hỗ trợ tự động cài đặt,
quản lý các thiết bị ngoại vi khi được đấu nối với máy tính. Về phía lập
trình, các chương trình thường ít khi thao tác trực tiếp với các thiết bị mà
thông qua một giao diện lập trình ứng dụng của Windows để khai thác tài
nguyên vật lý. Điều này tạo sự thuận lợi, dễ dàng đối với các chương trình
có khai thác nhiều về các thiết bị vật lý.
 Tương thích với các ứng dụng của DOS: khi thực hiện một chương trình
trên DOS thì Windows sẽ tạo ra một môi trường DOS giả lập. Tuy nhiên,
các phiên bản Windows mới sau này bỏ qua việc cung cấp môi trường DOS
giả lập, mà chúng ta có thể dùng các phần mềm khác như PC-DOS,
DosBox,...
b) Khái niệm luồng và tiến trình (Thread, Process)
Tiến trình (process) là quá trình thực hiện một chương trình từ khi khởi động
cho đến khi kết thúc. Windows cho phép thực hiện nhiều
chương trình cùng một lúc (cơ chế đa nhiệm), tức là sẽ có nhiều
process cùng chạy trên máy. Điều này dễ thấy khi chúng ta mở
của sổ “Task Manager” và chọn thẻ “Processes” trên Windows.
Luồng (thread) là quá trình thực hiện một đơn vị nào đó trong chương trình,
như vậy trong một process có ít nhất một thread để thực hiện
chương trình chính và có thể có nhiều thread khác nhau. Thông
thường để đơn giản chúng ta coi một thread được gắn với việc
thực hiện một hàm (là đơn vị chương trình cơ bản).
Thread được tạo ra từ một process nào đó, thậm chí có thể được tạo ra bởi
một thread khác, và nó có thể kết thúc sau process, hoặc kết
thúc tại thời điểm bất kỳ (như trên hình sau).

2
LẬP TRÌNH HỆ THỐNG

thread2
thread1

Process (thread
chính) thread3
thread4

c) Tương tác giữa Windows và chương trình ứng dụng (CTƯD)


Khi một chương trình ứng dụng chạy trên Windows sẽ chịu sự quản lý theo cơ
chế của Windows, trên đó diễn ra các tương tác (hình vẽ) và
chúng ta chia làm hai nhóm sau.
 Tương tác từ chương trình đến Windows:
Rõ ràng, chương trình ứng dụng khi chạy cần thực hiện xử lý các công việc
theo yêu cầu của người dùng, chẳng hạn lưu dữ liệu vào ổ đĩa, in dữ liệu ra
máy in, điều khiển một đối tượng trên màn hình,.... Để thực hiện các yêu
cầu này, chương trình cần xử lý đến các thiết bị vật lý như màn hình, ổ đĩa,
máy in,...

App2 App4
App1
App3

OS: Windows Tương tác


giữa CTƯD
Hardware và Windows

Windows cung cấp giao diện lập trình ứng dụng (Application Programming
Interface - API) để truy cập, khai thác, xử lý đến các thiết bị vật lý trên máy
tính. Giao diện này được thể hiện qua hệ thống các hàm, gọi là hàm API.
Các hàm API này còn được gọi là các dịch vụ (services) cung cấp cho lập
trình ứng dụng (hình vẽ). Điều này khác với hệ điều hành DOS, các dịch vụ
cung cấp cho lập trình được thể hiện dưới dạng các ngắt (interrupt) và chức
năng ngắt.

3
LẬP TRÌNH HỆ THỐNG

Các dịch Gọi sử dụng


Lõi của hệ điều Chương
vụ API dịch vụ API
hành Windows trình ứng
(core) dụng

Trong thư viện hàm API, tùy theo từng phiên bản của Windows sẽ được
cung cấp không gian địa chỉ 16 bít đối với Win16, hoặc 32 bít đối với
Win32,....
Trong chương trình, chúng ta cần phải viết lệnh gọi các dịch vụ này, thực
chất là lệnh gọi các hàm API theo quy tắc gọi hàm (hay chương trình con)
của ngôn ngữ lập trình.
 Tương tác từ Windows đến chương trình ứng dụng:
Đối với hệ điều hành MS-DOS, vì là môi trường làm việc đơn nhiệm, do đó
tại một thời điểm có một chương trình chạy và chương trình có thể chủ
động khai thác các tài nguyên máy tính cung cấp tính năng cho người dùng.
Điều này ngược lại với Windows, trong môi trường đa nhiệm có nhiều
chương trình cùng chạy một lúc dẫn đến chúng ta không thể chủ động khai
thác tài nguyên trên máy mà phải thông qua có chế quản lý của Windows.
Các sự kiện (events) phát sinh (do người dùng yêu cầu, do nội tại bên trong
hệ thống,...) sẽ do Windows nắm giữ và quản lý. Các sự kiện này được
chuyển hóa thành các thông điệp và lưu trong một cấu trúc hàng đợi gọi là
message queue (hàng đợi thông điệp). Sau đó Windows sẽ phân phối các
thông điệp này đến các chương trình đang chạy tương ứng và chương trình
ứng dụng sẽ phải tiếp nhận xử lý các thông điệp để đáp ứng yêu cầu của
người dùng cũng như của hệ thống (hình vẽ).

4
LẬP TRÌNH HỆ THỐNG

Windows

Chương trình
thông điệp

USERS Int
erf
Us

ac
er

e
kết quả
xử lý

Chương trình ứng dụng có 2 nhiệm vụ trong cơ chế này:


(1) Tiếp nhận các thông điệp mà Windows gửi đến khi có sự kiện phát sinh từ
hệ thống hoặc người dùng tác động;
(2) Thực hiện xử lý các thông điệp nhận được (nếu cần) để đáp ứng cho yêu
cầu tác động của người dùng. Để xử lý, rõ ràng cần phải gọi đến các dịch
vụ API của Windows.
Các bước lập trình chi tiết về hai nhiệm vụ trên sẽ được trình bày chi tiết ở
sau.
d) Cách lập trình trên Windows
Trong tài liệu này chúng ta sẽ thực hiện lập trình trên Windows bằng ngôn
ngữ C/C++. Trước hết, chúng ta xem xét các thành phần cơ bản
sau.
 Các kiểu dữ liệu cơ bản trên Windows:
Ngoài việc sử dụng kiểu dữ liệu chuẩn của ngôn ngữ C/C ++ cùng với các
phép toán xử lý, trong hệ thống thư viện API cung cấp các kiểu mở rộng
(định nghĩa từ các kiểu chuẩn) gồm một số kiểu hay dùng như sau:
Tên kiểu Mô tả, định nghĩa và ví dụ sử dụng
BOOL Kiểu lô-gíc (TRUE hoặc FALSE).

5
LẬP TRÌNH HỆ THỐNG

typedef int BOOL;


BYTE Kiểu byte (8 bít) không có bít dấu.
typedef unsigned char BYTE;
CALLBACK Kiểu hàm để có thể gọi ngược từ hệ thống.
#define CALLBACK __stdcall
COLORREF Kiểu mô tả giá trị màu được pha trộn từ 3 màu cơ bản
(red, green, blue (RGB)) gồm 32 bit.
typedef DWORD COLORREF;
DWORD Số nguyên 32-bit không có bít dấu.
typedef unsigned long DWORD;
DWORDLONG Số nguyên 64-bit không có bít dấu. Miền giá trị từ 0
đến 18446744073709551615.
typedef ULONGLONG DWORDLONG;
DWORD_PTR Kiểu con trỏ đến số nguyên 32 bít không dấu.
typedef ULONG_PTR DWORD_PTR;
FLOAT Kiểu số thực.
typedef float FLOAT;
HACCEL Kiểu định danh đến bảng gia tố.
typedef HANDLE HACCEL;
HANDLE Kiểu định danh đến các đối tượng (là con trỏ không
định kiểu).
typedef PVOID HANDLE;
HBITMAP Định danh đến đối tượng ảnh bitmap.
typedef HANDLE HBITMAP;
HBRUSH Định danh đến đối tượng tô màu.
typedef HANDLE HBRUSH;
HCURSOR Định danh đến đối tượng biểu tượng con trỏ.
typedef HICON HCURSOR;
HDC Định danh đến đối tượng đồ họa.
typedef HANDLE HDC;
HFILE Định danh đến đối tượng tệp tin để xử lý (số nguyên).
typedef int HFILE;

6
LẬP TRÌNH HỆ THỐNG

HFONT Định danh đến đối tượng phông chữ.


typedef HANDLE HFONT;
HHOOK Định danh đến đối tượng xử lý HOOK:
typedef HANDLE HHOOK;
HICON Định danh đến ảnh biểu tượng.
typedef HANDLE HICON;
HINSTANCE Định danh đến đối tượng chương trình.
typedef HANDLE HINSTANCE;
HKEY Định danh đến đối tượng “key” trong hệ thống.
typedef HANDLE HKEY;
HMENU Định danh đến đối tượng menu (thực đơn).
typedef HANDLE HMENU;
HMODULE Định danh đến đối tượng mô-đun thư viện chương
trình.
typedef HINSTANCE HMODULE;
HPEN Định danh đến đối tượng bút vẽ đồ họa.
typedef HANDLE HPEN;
HRESULT Định danh đến kiểu trả về (số nguyên).
typedef LONG HRESULT;
HRGN Định danh đến đối tượng giới hạn đồ họa.
typedef HANDLE HRGN;
HWND Định danh đến đối tượng cửa sổ trên màn hình.
typedef HANDLE HWND;
WCHAR Kiểu ký tự dạng 2 byte.
typedef wchar_t WCHAR;
TCHAR Kiểu ký tự dạng 1 hoặc 2 byte.
#ifdef UNICODE
typedef WCHAR TCHAR;
#else
typedef char TCHAR;
#endif
UINT Kiểu số nguyên (int) không dấu. Miền giá trị từ 0 đến
4294967295.

7
LẬP TRÌNH HỆ THỐNG

typedef unsigned int UINT;


WINAPI Kiểu hàm để gọi từ hệ thống (có ở tất cả các hàm API).
#define WINAPI __stdcall
WPARAM Tham số con trỏ kiểu LONG của chương trình.
typedef UINT_PTR WPARAM;
LPARAM Tham số con trỏ kiểu LONG của chương trình.
typedef LONG_PTR LPARAM;

Chú ý: Các tên kiểu này đều định nghĩa bằng chữ in hoa. Tên có chữ ‘H’ ở
đầu là kiểu định danh (con trỏ không định kiểu), tên có chữ ‘LP’ ở đầu là
kiểu con trỏ.
Ngoài ra còn có rất nhiều kiểu mở rộng trên Windows, bạn đọc có thể tham
khảo ở phần phụ lục của tài liệu [4].
 Cấu trúc các thành phần cơ bản của chương trình:
Trong ngôn ngữ C/C++ (sử dụng môi trường Visual C/C++ hoặc DevC) chúng
ta có 2 phương pháp lập trình đó là: lập trình hướng thủ tục, hoặc lập trình
hướng đối tượng. Trong tài liệu này chúng tôi giới thiệu phần lập trình
hướng thủ tục, tức là chương trình được tổ chức thành các hàm con và
chúng gọi lẫn nhau để thực hiện chức năng được thiết kế.
Trong chương trình hướng thủ tục có các thành phần cơ bản sau:
(1) Nạp các thư viện sử dụng trong chương trình
Thông thường chúng ta phải nạp thư viện “windows.h” để sử dụng các
hàm, biến nhớ, hằng và kiểu dữ liệu trong API.
#include<windows.h>
(2) Hàm chính của chương trình
Đó là hàm WinMain được khai báo như sau:
int WINAPI WinMain( HINSTANCE, HINSTANCE, LPSTR, int );
Tất cả các chương trình trên Windows đều bắt đầu thực hiện bằng cách gọi
hàm WinMain, nó như là một lối vào chính cho mọi ứng dụng trên
Windows. Các tham số gồm:
 HINSTANCE: số hiệu định danh chương trình khi chạy (lần hiện tại và
lần trước đó nếu có);
 LPSTR: con trỏ xâu ký tự tham số dòng lệnh;
 int: trạng thái cửa sổ chương trình, gồm các giá trị:

8
LẬP TRÌNH HỆ THỐNG

Giá trị Ý nghĩa


SW_HIDE Ẩn cửa sổ sau khi khởi động chương trình.
SW_MAXIMIZE Hiển thị ở chế độ lớn nhất.
SW_MINIMIZE Hiển thị ở chế độ thu nhỏ.
SW_RESTORE Hiển thị theo chế độ của lần đóng trước đó.
SW_SHOW Hiển thị theo chế độ thiết kế của cửa sổ .
Vậy, một chương trình đơn giản sẽ có dạng như sau:
#include<windows.h>
...
int WINAPI WinMain( HINSTANCE t1, HINSTANCE, LPSTR, int ) {
/* NỘI DUNG CHƯƠNG TRÌNH CHÍNH */
return 0;
}
Ngoài ra, chúng ta có thể tạo một ứng dụng dạng Console được viết theo
kiểu cấu trúc của chương trình C/C++. Khi chạy máy sẽ tự động tạo một cửa
sổ Console (dạng text) để làm giao diện tương tác với người dùng. Chương
trình này có dạng như sau:
#include<windows.h>
...
void main() {
/* NỘI DUNG CHƯƠNG TRÌNH CHÍNH */
}
Các bước sử dụng môi trường lập trình (Visual C ++ 6.0 hoặc DevC) xem
phần phụ lục của tài liệu [4].
3.2.2. Các dịch vụ API và xử lý cơ bản
a) Một số API đơn giản
 Hiện thông báo lên màn hình:
int MessageBox(HWND t1,LPCTSTR t2,LPCTSTR t3,UINT t4);
 t1: số hiệu cửa sổ chứa hộp thông báo (=0 nếu không có);
 t2: nội dung thông báo;
 t3: tiêu đề hộp thông báo;
 t4: kiểu thông báo {MB_OK, MB_OKCANCEL, MB_YESNO,...} +
{MB_ICONQUESTION,MB_ICONINFORMATION,
MB_ICONERROR,…}

9
LẬP TRÌNH HỆ THỐNG

 Tạm dừng chương trình:


VOID Sleep(DWORD t1);
 t1: khoảng thời gian cần tạm dừng (tính bằng mili giây).
 Lấy đối tượng cửa sổ đang hoạt động (trả về kiểu HWND):
HWND GetForegroundWindow();
 Đặt tiêu đề cửa sổ:
BOOL SetWindowText(HWND t1,LPCTSTR t2);
 t1: số hiệu cửa sổ cần đặt
 t2: xâu ký tự mới đặt lên tiêu đề cửa sổ
 Lấy hoặc đặt thời gian cục bộ của hệ thống:
void GetLocalTime(LPSYSTEMTIME t1);
BOOL SetLocalTime(SYSTEMTIME *t1);
 t1: địa chỉ biến nhớ chứa thời gian kiểu cấu trúc
SYSTEMTIME{ wYear, wMonth, wDay, wHour, wMinute, wSecond,
wMiliseconds}.
 Đặt giá trị của vùng nhớ về 0:
void ZeroMemory(PVOID t1,SIZE_T t2);
 t1: địa chỉ vùng nhớ cần đặt giá trị 0
 t2: kích thước vùng (tính bằng byte, thường dùng toán tử “sizeof”).
 Lấy độ phân giải màn hình (theo chiều ngang và dọc):
int GetSystemMetrics( SM_CXSCREEN );
int GetSystemMetrics( SM_CYSCREEN );
 Lấy tên người dùng đang làm việc trên Windows:
BOOL GetUserName(LPTSTR t1,LPDWORD t2);
 t1: xâu ký tự chứa tên người dùng lấy được;
 t2: số ký tự tối đa có thể chứa trong xâu.
 Lấy hoặc đặt tên máy của Windows:
BOOL GetComputerName(LPTSTR t1,LPDWORD t2);
BOOL SetComputerName(LPCTSTR t1);
 t1: xâu ký tự chứa tên máy (lấy ra hoặc đặt vào);
 t2: độ dài tối đa của xâu.
 Lấy phiên bản Windows (trả về giá trị 4 byte - DWORD):
DWORD GetVersion();

10
LẬP TRÌNH HỆ THỐNG

 Kết quả từ thấp (LOWORD) cho 2 byte gồm byte thấp (LOBYTE) là
số phiên bản chính (major) và byte cao (HIBYTE) là số phiên bản phụ
(minor).
Ví dụ 3.2.1: Lập trình hiển thị thời gian của máy lên một hộp thông báo.
#include <windows.h>
#include <stdio.h>
int WINAPI WinMain(HINSTANCE,HINSTANCE,LPSTR,int)
{
SYSTEMTIME a;
GetLocalTime( &a );
char s[100];
sprintf(s, “Date is %d / %d / %d, Time is %d : %d : %d”,
a.wDay, a.wMonth, a.wYear, a.wHour,
a.wMinute, a.wSecond);
MessageBox( 0, s, “Date & Time”, 0 );
return 0;
}

Trong ví dụ trên có sử dụng lệnh “sprintf” của thư viện <stdio.h> trong C/C ++,
lệnh này chuyển các dữ liệu theo định dạng về một chuỗi ký tự.
Ví dụ 3.2.2: Lập trình lấy cửa sổ hiện thời và đặt lại tiêu đề cho cửa sổ đó là
họ tên bạn. Lặp lại quá trình 300 lần, mỗi lần sau 1 giây.
#include <windows.h>
int WINAPI WinMain(HINSTANCE,HINSTANCE,LPSTR,int)
{
HWND a;
char ht[] = “Nguyen Van An”;
for( int i=0; i<300; i++ ){
a = GetForegroundWindow();
SetWindowText( a, ht );
Sleep(1000);
}
return 0;
}
Ví dụ 3.2.3: Lập trình lấy phiên bản của Windows và in ra số phiên bản chính,
bản phụ và bản xây dựng (bằng chương trình dạng Console).
#include <windows.h>
#include <stdio.h>
void main()
{
DWORD dwVersion = 0;

11
LẬP TRÌNH HỆ THỐNG

DWORD dwMajorVersion = 0;
DWORD dwMinorVersion = 0;
DWORD dwBuild = 0;
dwVersion = GetVersion();
// Get the Windows version.
dwMajorVersion = (DWORD)(LOBYTE(LOWORD(dwVersion)));
dwMinorVersion = (DWORD)(HIBYTE(LOWORD(dwVersion)));
// Get the build number.
if (dwVersion < 0x80000000)
dwBuild = (DWORD)(HIWORD(dwVersion));
printf("Version is %d.%d (%d)\n",
dwMajorVersion,
dwMinorVersion,
dwBuild);
}
b) Lập trình xử lý cửa sổ
Cửa sổ là một đối tượng quan trọng trong Windows, các hoạt động trên mọi
thành phần giao diện (chẳng hạn các nút lệnh, các ô nhập liệu,
các mục menu,...) đều theo nguyên lý cửa sổ. Như đã biết, một
cửa sổ có một số hiệu định danh (HWND) và được tạo từ một
lớp cửa sổ bên trong hệ thống.
Lớp cửa sổ (window class) là một tập các định nghĩa mô tả cấu tạo và hoạt
động cơ bản cho cửa sổ. Windows sử dụng lớp cửa sổ như là
một mẫu để tạo ra đối tượng cửa sổ trên màn hình. Các lớp cửa
sổ có thể được đăng ký bởi chương trình hoặc lấy từ các lớp có
sẵn trong hệ thống, mỗi lớp có một tên (dạng xâu ký tự) để định
danh.
Một số lớp cửa sổ có sẵn gồm:
Giá trị Ý nghĩa
static Cửa sổ dạng Label (hiển thị văn bản tĩnh)
edit Cửa sổ để nhập dữ liệu văn bản
button Cửa sổ dạng nút bấm
combobox Cửa sổ dạng hộp chọn
listbox Cửa sổ dạng danh sách chọn,...
Để đăng ký một lớp cửa sổ mới ta thực hiện các bước sau:

12
LẬP TRÌNH HỆ THỐNG

Bước 1) Định nghĩa các thành phần cho lớp cửa sổ, sử dụng cấu trúc sau:
typedef struct _WNDCLASS {
UINT style;
WNDPROC lpfnWndProc;
int cbClsExtra;
int cbWndExtra;
HINSTANCE hInstance;
HICON hIcon;
HCURSOR hCursor;
HBRUSH hbrBackground;
LPCTSTR lpszMenuName;
LPCTSTR lpszClassName;
} WNDCLASS, *PWNDCLASS;
Trong đó:
 style: quy định kiểu lớp cửa sổ, bao gồm các giá trị với ý nghĩa như sau

Style Action
CS_DBLCLKS sẽ nhận được thông điệp nhấn đúp chuột trên cửa sổ
CS_HREDRAW được vẽ lại khi thay đổi kích thước chiều ngang
CS_NOCLOSE không hiển thị nút lệnh đóng cửa sổ trên tiêu đề
CS_VREDRAW được vẽ lại cửa sổ khi đổi kích thước chiều dọc
 lpfnWndProc: tên hàm cửa sổ để nhận và xử lý các thông điệp cho cửa sổ
 cbClsExtra, cbWndExtra: số byte của lớp cửa sổ và bản thân cửa sổ mở
rộng, thường đặt bằng 0.
 hInstance: số hiệu của chương trình đang chạy trên máy, được lấy từ tham
số của hàm WinMain
 hIcon: biểu tượng của cửa sổ, nếu không đặt bằng NULL. Có thể sử dụng
hàm để nạp biểu tượng như sau:
HICON LoadIcon(
HINSTANCE hInstance, // số hiệu của chương trình
LPCTSTR lpIconName // tên hoặc số hiệu của biểu tượng
);
Chúng ta có thể nạp các biểu tượng có sẵn trong window bằng cách đặt
hInstance=NULL, và lpIconName có thể là:

13
LẬP TRÌNH HỆ THỐNG

Giá trị Mô tả ý nghĩa


IDI_APPLICATION biểu tượng của chương trình ứng dụng
IDI_ASTERISK biểu tượng thông tin
IDI_ERROR biểu tượng lỗi
IDI_EXCLAMATION biểu tượng cảnh báo
IDI_HAND biểu tượng dừng
IDI_INFORMATION biểu tượng thông tin
IDI_QUESTION biểu tượng trợ giúp
IDI_WARNING biểu tượng cảnh báo
IDI_WINLOGO biểu tượng lôgô của window
 hCursor: con trỏ chuột hiển thị trên cửa sổ, đặt bằng NULL sẽ lấy kiểu con
trỏ ngầm định là mũi tên. Có thể nạp biểu tượng chuột từ window bằng
lệnh sau:
HCURSOR LoadCursor(
HINSTANCE hInstance, // số hiệu của chương trình đang chạy
LPCTSTR lpCursorName // tên hoặc số hiệu biểu tượng chuột
);
Để nạp các biểu tượng từ window ta đặt hInstance = NULL, và số hiệu
biểu tượng có thể là:

Giá trị Ý nghĩa


IDC_APPSTARTING mũi tên và có thêm đồng hồ cát
IDC_ARROW mũi tên bình thường
IDC_CROSS sợi tóc
IDC_HAND bàn tay
IDC_HELP trợ giúp
IDC_IBEAM chữ I
IDC_NO vòng tròn
IDC_SIZEALL mũi tên bốn chiều
IDC_SIZENESW mũi tên chéo góc trái trên
IDC_SIZENS mũi tên chéo góc trái dưới
IDC_SIZENWSE mũi tên chéo góc phải trên

14
LẬP TRÌNH HỆ THỐNG

IDC_SIZEWE mũi tên chéo góc phải dưới


IDC_UPARROW mũi tên đứng
IDC_WAIT đồng hồ cát
 hbrBackground: màu nền cửa sổ, có thể chọn một các màu có sẵn trên
window như sau:

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
Tuy nhiên chúng ta phải chuyển các giá trị này về kiểu nền bằng lệnh sau
(ép kiểu):
( HBRUSH ) tên_màu_chọn_ở_trên
 lpszMenuName: thực đơn lệnh trên cửa sổ, nếu không đặt bằng NULL.
 lpszClassName: là một xâu xác định tên lớp cửa sổ, thực hiện để đăng ký,
mỗi lớp đăng ký với một tên duy nhất.
Bước 2) Đăng ký vào hệ thống bằng hàm sau:

15
LẬP TRÌNH HỆ THỐNG

ATOM RegisterClass(
WNDCLASS *t1 // dữ liệu của lớp cửa sổ
);
Hàm này sử dụng các dữ liệu của lớp cửa sổ đã được thiết lập và đưa vào
tham số lpWndClasss dưới dạng địa chỉ. Nếu không thành công
hàm sẽ trả về giá trị bằng 0.
Ví dụ 3.2.4: Thiết lập các tham số cho một lớp cửa sổ và đăng ký vào
windows.
WNDCLASS wc;
ZeroMemory( &wc, sizeof(wc) );
wc.hbrBackground= (HBRUSH)COLOR_WINDOW;
wc.hInstance = hInstance;
wc.lpfnWndProc = (WNDPROC)WindowProc;
wc.lpszClassName = "WC1";
wc.style = 1;
RegisterClass(&wc);
Ở đây, trước hết ta sử dụng lệnh “ZeroMemory” để xóa dữ liệu trong biến
“wc”, sau đó thiết lập các thành phần. Sử dụng hàm cửa sổ có
tên “WindowProc” để đăng ký lớp cửa sổ, tuy nhiên ta có thể
đặt bằng NULL thì sẽ không có hàm cửa sổ. Lớp cửa sổ được
đăng ký với tên lớp là “WC1”.
 Tạo cửa sổ mới:
Sau khi đăng ký lớp thành công chúng ta sử dụng để tạo một cửa sổ tương
ứng với lớp đó bằng lệnh sau:
HWND CreateWindow(
LPCTSTR lpClassName, // tên lớp cửa sổ đã đăng ký
LPCTSTR lpWindowName, // tên tiêu đề cửa sổ
DWORD dwStyle, // kiểu cửa sổ
int x, // tọa độ chiều ngang của góc trái trên
int y, // tọa độ chiều dọc của góc trái trên
int nWidth, // độ rộng cửa sổ
int nHeight, // độ cao cửa sổ
HWND hWndParent, // số hiệu cửa sổ chứa nó
HMENU hMenu, // số hiệu thực đơn của cửa sổ
HINSTANCE hInstance, // số hiệu chương trình đang thực hiện
LPVOID lpParam // các dữ liệu khác
);

16
LẬP TRÌNH HỆ THỐNG

Trong đó:
 dwStyle: kiểu xác định lớp cửa sổ, có thể được kết hợp từ các giá trị với
ý nghĩa như sau.
Kiểu Ý nghĩa
WS_BORDER cửa sổ có đường viền nhỏ
WS_CAPTION cửa sổ có thanh tiêu đề
WS_CHILD cửa sổ con của một cửa sổ khác
WS_DISABLED không nhận thông điệp của người sử
dụng
WS_DLGFRAME cửa sổ giống với hộp thoại
WS_HSCROLL có thanh cuộn ngang
WS_MAXIMIZE cửa sổ ở chế độ phóng to khi tạo
WS_MAXIMIZEBOX có nút lệnh phóng to
WS_MINIMIZE cửa sổ thu nhỏ khi tạo ra
WS_MINIMIZEBOX có nút lệnh thu nhỏ
WS_OVERLAPPEDWINDOW cửa sổ có đầy đủ các thành phần
WS_POPUPWINDOW cửa sổ nằm trên mọi cửa sổ khác
WS_SIZEBOX có nút lệnh điều chỉnh kích thước
WS_SYSMENU có thực đơn hệ thống của cửa sổ
WS_TABSTOP có thể dừng bằng phím Tab
WS_TILEDWINDOW cửa sổ có đủ các thành phần
WS_VISIBLE cửa sổ ở dạng hiện
WS_VSCROLL có thanh cuộn dọc
 x, y, nWidht, nHeight: x,y là tọa độ góc trái trên của cửa sổ, nWidth là
chiều rộng, nHeight là chiều cao, có thể tự xác định theo màn hình hoặc
lấy mặc định của window bởi hằng sau CW_USEDEFAULT. Khi đó ta
chỉ cần đặt x=CW_USEDEFAULT và nWidht=CW_USEDEFAULT còn
y và nHeight đều đặt bằng 0.
 hWndParent, hMenu: Cửa sổ chứa (hWndParent) và thực đơn lệnh
(hMenu) có thể đặt bằng NULL nếu không cần thiết.
 hInstance: là số hiệu của chương trình đang chạy.
 lpParam: là con trỏ của các dữ liệu mở rộng, thường đặt bằng NULL.

17
LẬP TRÌNH HỆ THỐNG

Ví dụ 3.2.5: Tạo một cửa sổ đã được đăng ký lớp với tên lớp là “WC1”
HWND hWnd = CreateWindow( "WC1", "Chuong trinh vi du 1.3",
WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, 0,
CW_USEDEFAULT, 0, NULL, NULL,
<số_hiệu_chương_trình>, NULL );
Lệnh tạo cửa sổ trên sử dụng lớp đã đăng ký với tên là “WC1”, để lệnh này
thực hiện thành công chúng ta phải có hàm cửa sổ và đặt nó khi
đăng ký lớp cửa sổ.
 Hiển thị lên màn hình bằng lệnh sau:
BOOL ShowWindow(
HWND hWnd, // số hiệu cửa sổ để hiển thị
int nCmdShow // trạng thái hiển thị
);
và hàm cập nhật lại nội dung trên cửa sổ (vẽ lại mới hoàn toàn) bằng lệnh
sau:
BOOL UpdateWindow(
HWND hWnd // số hiệu cửa sổ
);
 Một số hàm API xử lý cơ bản khác trên cửa sổ:
Cú pháp hàm Ý nghĩa
CloseWindow( HWND t1 ); Thu nhỏ cửa sổ
DestroyWindow( HWND t1 ); Đóng (hủy) cửa sổ
FindWindow( LPCTSTR t1, Tìm kiếm cửa sổ theo tên lớp (t1) hoặc tiêu
LPCTSTR t2 ); đề cửa sổ (t2), trả về số hiệu
HWND
GetWindowText( HWND t1, Lấy nội dung tiêu đề cửa sổ t1, đưa tiêu đề
LPTSTR t2, int lấy được vào xâu ký tự t2 với
t3); độ dài tối đa t3
MoveWindow( HWND t1, int Di chuyển cửa sổ t1 đến tọa độ x,y và độ
x,int y, int w,int rộng, cao mới là w,h. t6 là có
h, BOOL t6); vẽ lại cửa sổ?
GetWindowRect( HWND t1, Lấy tọa độ cửa sổ t1 cho vào biến t2 (kiểu
LPRECT t2); RECT) gồm {left, top, right,
bottom}

18
LẬP TRÌNH HỆ THỐNG

GetClientRect(HWND t1, Lấy tọa độ nền cửa sổ t1 cho vào biến t2


LPRECT t2); (kiểu RECT) gồm {left, top,
right, bottom}
...
Ví dụ 3.2.6: Lập trình tạo một cửa sổ từ lớp “static” có tiêu đề là tên bạn, sau
đó cho cửa sổ chuyển động theo cơ chế phản xạ trên màn hình
trong khoảng thời gian là 5 phút.
#include <windows.h>
int WINAPI WinMain(HINSTANCE t1,HINSTANCE,LPSTR,int)
{
int cx = GetSystemMetrics(SM_CXSCREEN);
int cy = GetSystemMetrics(SM_CYSCREEN);
int x=100, y=100, w=100, h=20, dx=1, dy=1;
HWND a = CreateWindow("static","Nguyen Van An",WS_CAPTION,
x,y,w,h,0,0,t1,0);
ShowWindow(a,1);
UpdateWindow(a);
for(int i=0;i<600;i++){
MoveWindow(a,x,y,w,h,1);
Sleep(500);
x += dx; y += dy;
if(x<0 || x+w>cx) dx=-dx;
if(y<0 || y+h>cy) dy=-dy;
}
return 0;
}
Ở trên chúng ta dùng lệnh MoveWindow để di chuyển cửa sổ với tọa độ x, y.
Sau mỗi lần di chuyển ta thay đổi x,y này theo dx,dy (giá trị
dx,dy quy định phương chuyển động, dấu của dx,dy quy định
hướng chuyển động). Nếu vượt khỏi màn hình (bằng lệnh if để
kiểm tra điều kiện) thì thay đổi dấu của dx,dy làm cho hướng
chuyển động thay đổi (xem hình vẽ). Với khoảng thời gian 5
phút, chúng ta tạm dừng mỗi lần lặp là 500 mili giây, vậy cần
600 lần lặp để đạt 300000mili giây/1000/60 = 5 phút.

19
LẬP TRÌNH HỆ THỐNG

dx>0
dy>0

3.2.3. Thông điệp và cơ chế xử lý thông điệp


a) Phương pháp xử lý thông điệp
Như đã trình bày, thông điệp dùng để biểu diễn các sự kiện trên máy do người
dùng tác động hoặc phát sinh nội tại trong máy. Các thông điệp
được chuyển đến chương trình từ Windows khi chúng được
phát sinh, chương trình cần thực hiện hai nhiệm vụ:
(1) Biên nhận thông điệp mà máy gửi đến. Điều này được thực hiện bởi một
vòng lặp thông điệp (message loop) trong chương trình chính.
(2) Xử lý các thông điệp đã nhận (nếu cần). Chúng ta phải lập trình các hàm
cửa sổ (hay gọi là hàm xử lý thông điệp) và đăng ký chúng vào lớp cửa sổ
như trên đã trình bày.
Mỗi thông điệp được mã hóa thành một giá trị nguyên, đi kèm là các thông tin
liên quan đến thông điệp. Chẳng hạn, khi nhấn phím ta có thông
điệp WM_KEYDOWN và các thông tin cho biết mã phím được
nhấn, số lần phát sinh thông điệp này nếu giữ phím,.... Mỗi sự
kiện trên máy có thể phát sinh nhiều thông điệp liên tiếp nhau.
Giá trị của thông điệp được Windows định nghĩa thành các hằng có dạng:
WM_<tên sự kiện> (hai chữ WM là viết tắt của Windows
Message).
 Lập trình vòng lặp nhận thông điệp:
MSG m;
while( GetMessage(&m,0,0,0) ){
TranslateMessage(&m);
DispatchMessage(&m);

20
LẬP TRÌNH HỆ THỐNG

}
Trong đó:
 MSG: là kiểu cấu trúc lưu giữ thông tin của thông điệp, chúng ta khai
báo biến ‘m’ để chứa;
 GetMessage: lệnh nhận thông điệp gửi từ máy đến, tham số ‘m’ dạng
địa chỉ để chứa thông tin thông điệp nhận, ba tham số sau để quy định
giới hạn cửa sổ nhận, và miền giá trị thông điệp sẽ nhận. Ba tham số
này bằng 0 sẽ bỏ qua quy định giới hạn này, tức sẽ nhận tất cả các
thông điệp trên tất cả các cửa sổ của chương trình;
 TranslateMessage: lệnh chuyển đổi các thông tin của thông điệp bàn
phím từ dạng mã phím ảo về dạng mã phím ký tự.
 DispatchMessage: phân phối thông điệp đến các hàm xử lý tương ứng.
Ở đây, mỗi cửa sổ được tạo ra từ một lớp cửa sổ và lớp đó đã được
đăng ký vào máy cùng với hàm xử lý thông điệp tương ứng nên thông
qua lệnh này máy sẽ tìm gọi hàm đã đăng ký trong lớp cửa sổ.
Vòng lặp sẽ dừng khi điều kiện lặp (trong lệnh while) bị sai, như vậy khi
lệnh ‘GetMessage’ cho giá trị sai thì dừng lặp. Ở đây, lệnh ‘GetMessage’
cho giá trị sai khi nó nhận được thông điệp có tên ‘WM_QUIT’. Như vậy,
trong bất kỳ chỗ nào của chương trình, nếu chúng ta muốn dừng chương
trình (dừng vòng lặp thông điệp) thì phải phát sinh thông điệp trên bằng
lệnh sau:
PostQuitMessage(0);
 Lập trình hàm xử lý thông điệp: là một hàm đặc biệt, được gọi từ hệ thống
khi chúng ta đăng ký vào lớp cửa sổ và phân phối thông điệp đến nó (lệnh
‘DispatchMessage’) - nó có kiểu CALLBACK. Cú pháp khai báo của hàm
như sau:
LRESULT CALLBACK tên_hàm(HWND t1,UINT t2,WPARAM t3,
LPARAM t4);
Trong đó:
 HWND: là tham số nhận số hiệu cửa sổ khi thông điệp trên đó được
phát sinh và xử lý;
 UINT: tham số nhận giá trị của thông điệp. Chúng ta sử dụng tham số
này để kiểm tra với giá trị thông điệp cần xử lý (bằng lệnh ‘if’ hoặc
‘switch’) và thực hiện câu lệnh tương ứng.

21
LẬP TRÌNH HỆ THỐNG

 WPARAM, LPARAM: hai tham số nhận thông tin liên quan đến thông
điệp. Có thông tin chứa trong 2 byte của WPARAM, có thông tin chứa
trong 4 byte của LPARAM và những thông tin lớn hơn được chứa một
nơi khác với địa chỉ được lưu bên trong LPARAM.
Hàm này trả về giá trị số nguyên kiểu LRESULT (tức kiểu LONG) nên
trong hàm phải có lệnh ‘return’. Thường chúng ta chỉ xử lý những thông
điệp cần thiết và trả về giá trị 0 (return 0), còn lại các thông điệp khác sẽ
nhờ hàm có sẵn trong máy xử lý hộ có tên ‘DefWindowProc’ bằng lệnh
sau:
return DefWindowProc(t1,t2,t3,t4);
Hình vẽ sau minh họa cơ chế phát sinh, biên nhận, phân phối và xử lý
thông điệp trong chương trình.
Hệ điều hành Windows

Window gửi thông điệp đến hàm cửa sổ


Người dùng

Thông điệp của hệ thống


(nhận và xử lý
Chương trình

thông điệp)

Thông điệp của người dùng

Thông điệp của bản thân ct

Người dùng tác động sinh thông điệp,


hoặc chương trình sinh thông điệp bằng lệnh

22
LẬP TRÌNH HỆ THỐNG

b) Một số thông điệp đơn giản

Tên thông điệp Ý nghĩa


WM_CLOSE Đóng cửa sổ (thu nhỏ)
WM_DESTROY Hủy bỏ cửa sổ
WM_SIZE Thay đổi kích thước
WM_SIZING Đang thay đổi kích thước
WM_MOVE Di chuyển cửa sổ
WM_MOVING Đang di chuyển cửa sổ
WM_SHOWWINDOW Hiển thị cửa sổ lên màn hình
WM_TIMER Thông điệp phát sinh theo chu kỳ thời gian
...

Các thông tin liên quan gồm:


 WM_SIZE, WM_SIZING: 2 byte thấp LOWORD() của tham số
LPARAM cho biết độ rộng nền cửa sổ, 2 byte cao HIWORD(LPARAM)
cho biết độ cao nền cửa sổ;
 WM_MOVE, WM_MOVING: tham số LPARAM chứa con trỏ đến một
cấu trúc (struct) lưu tọa độ cửa sổ trên màn hình (kiểu RECT gồm {left,
top, right, bottom}). Chúng ta sử dụng cơ chế ép kiểu để xác định dữ liệu
này như sau:
RECT *p = (RECT*)<tham số kiểu LPARAM>;
p->left / p->top / p->right / p->bottom
 WM_SHOWWINDOW: tham số kiểu WPARAM cho biết cửa sổ ở trạng
thái hiển thị hay trạng thái ẩn, tham số kiểu LPARAM lý do cửa sổ thay
đổi trạng thái hiển thị gồm: SW_OTHERUNZOOM (cửa sổ khác thay
đổi), SW_OTHERZOOM (cửa sổ khác phóng to),
SW_PARENTCLOSING (cửa sổ chứa bị đóng) hay
SW_PARENTOPENING (cửa sổ chứa được mở).
 WM_TIMER: thông điệp được kích hoạt khi chúng ta đặt chu kỳ thời gian
bằng lệnh sau,
SetTimer( HWND, IDEvent, Elapse, TimerProc );
với tham số HWND là định danh cửa sổ cần đặt, IDEvent là chỉ số chu kỳ
(=1), Elapse là khoảng thời gian trong 1 chu kỳ (mili giây), TimerProc là

23
LẬP TRÌNH HỆ THỐNG

hàm xử lý thay thế cho thông điệp WM_TIMER (=0). Chỉ số chu kỳ dùng
để quản lý và khi cần hủy ta dùng lệnh,
KillTimer( HWND, IDEvent );
Ví dụ 3.2.7: Lập trình đăng ký lớp cửa sổ (có hàm xử lý thông điệp), tạo và
hiện cửa sổ lên màn hình. Xử lý thông điệp đóng cửa sổ
(WM_CLOSE) để hiện thông báo hỏi có chắc chắn không và
thoát khỏi chương trình. Chương trình gồm hàm chính
(WinMain) và hàm cửa sổ (xử lý thông điệp) sẽ như sau:
#include<windows.h>
LRESULT CALLBACK HCS(HWND t1,UINT t2,WPARAM t3,LPARAM t4){
if(t2==WM_CLOSE){
int k = MessageBox(t1,"Co chac chan khong?",
"Thoat chuong trinh",MB_YESNO);
if(k==IDYES) PostQuitMessage(0);
return 0;
}
return DefWindowProc(t1, t2, t3, t4);
}
int WINAPI WinMain(HINSTANCE t1,HINSTANCE,LPSTR,int){
/*ĐĂNG KÝ LỚP CỬA SỔ*/
WNDCLASS wc;
ZeroMemory(&wc,sizeof(wc));
wc.hbrBackground= (HBRUSH)COLOR_WINDOW;
wc.hInstance = t1;
wc.lpfnWndProc = HCS; /*(1)*/
wc.lpszClassName = "WC1";
wc.style = 1;
RegisterClass(&wc);

/*TẠO VÀ HIỆN CỬA SỔ LÊN MÀN HÌNH*/


HWND a = CreateWindow("WC1","Chuong trinh vi du 1.3",
WS_SYSMENU, 100, 100, 300, 200, 0, 0, t1, 0);
ShowWindow(a,1);
UpdateWindow(a);

/*VÒNG LẶP XỬ LÝ THÔNG ĐIỆP*/


MSG m;
while (GetMessage(&m,NULL,0,0)){
TranslateMessage(&m);
DispatchMessage(&m);
}
return m.wParam;

24
LẬP TRÌNH HỆ THỐNG

}
Trong chương trình trên, hàm xử lý thông điệp có tên ‘HCS’ và được khai báo
lập trình trước chương trình chính vì trong chương trình chính
(hàm WinMain) có dùng hàm này (tên hàm) để đăng ký một lớp
cửa sổ (xem lệnh (1)). Nếu không chúng ta có thể khai báo hàm
trước, lập trình sau hàm WinMain.
Kết quả chạy chương trình khi đóng cửa sổ (nhấn chọn nút ‘x’ - close) sẽ là:

c) Thông điệp bàn phím:


Khi gõ phím (không cùng với phím ALT), máy sẽ phát sinh 3 thông điệp gồm:
 WM_KEYDOWN: phát sinh thời điểm nhấn phím xuống;
 WM_CHAR: phát sinh khi gõ phím ký tự;
 WM_KEYUP: phát sinh thời điểm nhả phím.
Trường hợp gõ phím có kèm theo giữ phím ALT thì máy sẽ hiểu đó là các
hành động được định nghĩa và xử lý mặc định của hệ thống.
Các thông điệp phát sinh trong trường hợp này tương ứng với 3
thông điệp trên là:
 WM_SYSKEYDOWN; WM_DEADCHAR; WM_SYSKEYUP
Trong cả 6 thông điệp trên, chúng ta có tham số WPARAM chứa giá trị mã
của ký tự gõ hoặc mã phím nếu là phím điều khiển (danh sách
mã ở bảng dưới), 2 byte thấp LOWORD(LPARAM) chứa số
lần phát sinh thông điệp liên tiếp khi giữ phím, 2 byte cao
HIWORD(LPARAM) cho biết trạng thái một số phím khác.

25
LẬP TRÌNH HỆ THỐNG

Chú ý: Macro LOWORD(lParam) để lấy giá trị của 2 byte thấp của
LPARAM, HIWORD(LPARAM) lấy giá trị 2 byte cao của
LPARAM.
Danh sách một số mã phím được định nghĩa trong API gồm:
Macro Ý nghĩa phím
VK_CANCEL Ctrl-Tab
VK_TAB Tab
VK_SHIFT Shift
VK_CONTROL Ctrl
VK_LEFT Left arrow
VK_UP Up arrow
VK_RIGHT Right arrow
VK_DOWN Down arrow
VK_INSERT Insert
VK_CAPITAL CapsLock
VK_F1 F1
… …
VK_F12 F12
VK_PRIOR Page Up
VK_NEXT Page Down,...
VK_LBUTTON Phím trái chuột
VK_LCONTROL Phím Ctrl bên trái
VK_RSHIFT Phím Shift bên phải
...

Ví dụ 3.2.8: Lập trình đăng ký lớp cửa sổ, tạo và hiện cửa sổ lên màn hình. Xử
lý thông điệp gõ phím (WM_KEYDOWN) để hiện thông báo
mã phím được bấm. Chương trình chính (hàm WinMain) giống
ví dụ trước, hàm cửa sổ sẽ như sau:

26
LẬP TRÌNH HỆ THỐNG

/*HÀM CỬA SỔ XỬ LÝ THÔNG ĐIỆP CỦA CHƯƠNG TRÌNH*/


LRESULT CALLBACK HCS(HWND t1, UINT t2, WPARAM t3, LPARAM t4) {
switch (t2)
{
case WM_KEYDOWN:
char str[100];
sprintf(str,"Gia tri ma phim la : %d",t3);
MessageBox(t1, str, "Xu ly phim", MB_OK);
break;
case WM_DESTROY:
PostQuitMessage(0);
break;
default:
return DefWindowProc(t1, t2, t3, t4);
}
return 0;
}
Trong hàm xử lý thông điệp ‘HCS’, chúng ta xử lý thêm thông điệp
WM_DESTROY để thoát khỏi chương trình bằng cách gửi
thông điệp WM_QUIT (để kết thúc vòng lặp thông điệp như đã
trình bày) bằng lệnh ‘PostQuitMessage’.
Kết quả màn hình chạy chương trình khi nhấn phím ‘Enter’ là:

Ngoài ra, muốn biết trạng thái các phím chúng ta có thể dùng hàm sau:
SHORT GetKeyState (int nVirtKey);
SHORT GetAsyncKeyState (int nVirtKey);
với tham số ‘nVirtKey’ là mã phím cần kiểm tra (ở bảng phía trên). Hàm sẽ
cho giá trị trạng thái của phím thông qua bít trạng thái, chúng ta
sử dụng kết quả hàm này kết hợp với giá trị ‘0x8000’ bằng phép

27
LẬP TRÌNH HỆ THỐNG

‘và’ bít (&) sẽ cho biết phím đó được giữa hay không. Ví dụ
kiểm tra trạng thái phím Ctrl ở bên trái bàn phím như sau:
if( GetKeyState(VK_LCONTROL)&0x8000 )
/* điều kiện giữ phím Ctrl bên trái */

d) Thông điệp chuột


Thông điệp chuột được sinh ra khi chúng ta nhấn hoặc nhả chuột (tất cả các
nút), gồm có một số thông điệp liên quan đến thiết bị chuột như
sau:
Giá trị thông điệp Ý nghĩa
WM_LBUTTONDOWN nhấn nút trái chuột
WM_LBUTTONUP nhả nút trái chuột
WM_LBUTTONDBLCLK nhấn đúp nút trái
WM_MBUTTONDOWN nhấn nút giữa
WM_MBUTTONUP nhả nút giữa
WM_MBUTTONDBLCLK nhấn đúp nút giữa
WM_RBUTTONDOWN nhấn nút phải
WM_RBUTTONUP nhả nút phải
WM_RBUTTONDBCLK nhấn đúp nút phải
WM_MOUSEMOVE cho biết chuột di chuyển đến vị trí khác
Khi nhận được thông điệp chuột, vị trí của chuột được xác định như sau:
 LOWORD(LPARAM) - cho biết toạ độ x của chuột (chiều ngang)
 HIWORD(LPARAM) - cho biết toạ độ y (chiều dọc) của chuột.
Và để nhận được các thông điệp nhấn đúp chuột …DBLCLK, trong thành
phần style của lớp của sổ khi đăng ký phải có thêm kiểu
CS_DBLCLKS, cụ thể là:
<biến_lớp_cs>.style = CS_HREDRAW | CS_VREDRAW | CS_DBLCLKS;
Ngoài ra, Windows cung cấp một số phím được ấn nằm trong tham số
WPARAM khi nhận được các thông điệp chuột bao gồm như
sau:

Giá trị kiểm tra phím Ý nghĩa


MK_CONTROL nếu phím Ctrl được ấn cùng với nút chuột
MK_SHIFT nếu phím Shift được ấn cùng với nút chuột

28
LẬP TRÌNH HỆ THỐNG

MK_RBUTTON nếu phím mũi tên phải được ấn


MK_LBUTTON nếu phím mũi tên trái được ấn
MK_MBUTTON nếu phím giữa được ấn
Chúng ta thấy thiết bị chuột rất quan trọng trong Windows, làm cho mọi việc
trở nên dễ dàng hơn, do vậy chúng ta muốn biết máy tính có nối
với chuột không ta dùng hàm nhận biết các tham số hệ thống
như sau:
int GetSystemMetrics(int nIndex);
hàm trả về giá trị của tham số tương ứng (nIndex) nếu tham số đó có trong hệ
thống, ngược lại hàm cho giá trị không (0), tham số nIndex có
thể là:
Giá trị Ý nghĩa
SM_CMOUSEBUTTONS số lượng nút chuột.
SM_CXBORDER, độ rộng và chiều cao tính bằng pixel của một
SM_CYBORDER cửa sổ.
SM_CXCURSOR, độ rộng và chiều cao của con trỏ chuột tính
SM_CYCURSOR bằng pixel.
SM_CXFULLSCREEN, độ rộng và chiều cao của một cửa sổ ở trạng
SM_CYFULLSCREEN thái phóng to.
SM_CXSCREEN, độ rộng và chiều cao của màn hình
SM_CYSCREEN
SM_MOUSEPRESENT TRUE hoặc khác 0 nếu có thiết bị chuột,
ngược lại là FALSE hoặc bằng
0.
SM_SWAPBUTTON TRUE hoặc khác 0 nếu chúng ta hoán đổi tác
dụng của 2 nút chuột trái và
phải, ngược lại FALSE hoặc
bằng 0.
Ví dụ 3.2.9: Hãy viết chương trình đơn giản đăng ký, tạo cửa sổ và xử lý
thông điệp để in lên tiêu đề cửa sổ vị trí của chuột và trạng thái
của các phím chuột (được ấn hay nhả).
Ở đây đề bài không yêu cầu cụ thể thông điệp, do đó chúng ta phải phân tích
chọn thông điệp thích hợp để xử lý. Để biết được vị trí chuột

29
LẬP TRÌNH HỆ THỐNG

ngay khi tọa độ của nó thay đổi ta sử dụng thông điệp


WM_MOUSEMOVE, tiếp theo xử lý thông điệp
WM_LBUTTONDOWN & WM_LBUTTONUP để kiểm tra
nút trái chuột được giữ hay không và tương tự với cặp thông
điệp WM_RBUTTONDOWN & WM_RBUTTONUP.
#include<windows.h>
#include<stdio.h>

#define wt "Xử lý thông điệp chuột - Mouse message"/*tieu de cua so*/


#define wn "W1" /*ten lop cua so*/
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
/**********Ham WinMain cua chuong trinh*************/
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE, LPSTR, int ) {
MSG msg;
WNDCLASS wc;
HWND hWnd;

/*Dinh nghia lop cua so*/


ZeroMemory(&wc,sizeof(wc));
wc.style = CS_HREDRAW | CS_VREDRAW |
CS_DBLCLKS;
wc.lpfnWndProc = (WNDPROC)WndProc;
wc.hInstance = hInstance;
wc.hIcon = LoadIcon(NULL,IDI_INFORMATION); /*(1)*/
wc.hbrBackground= (HBRUSH)(COLOR_WINDOW+1);
wc.lpszClassName = wn;

/*Dang ky lop cua so*/


if (!RegisterClass(&wc)) return 0;

/*Tao cua so thuc su*/


hWnd = CreateWindow(wn, wt, WS_OVERLAPPEDWINDOW
| WS_HSCROLL | WS_VSCROLL, CW_USEDEFAULT,
CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL,
NULL, hInstance, NULL);
/*Hien thi va cap nhat noi dung cua so*/
ShowWindow(hWnd,1);
UpdateWindow(hWnd);
/* Vong lap thong diep */
while (GetMessage(&msg, NULL, 0, 0)) {
TranslateMessage(&msg);
DispatchMessage(&msg);

30
LẬP TRÌNH HỆ THỐNG

}
return msg.wParam;
}

/********* Ham cua so cua chuong trinh ************/


LRESULT CALLBACK WndProc(HWND t1, UINT t2, WPARAM t3, LPARAM
t4) {
static char sXY[250] = "";
static char sLM[100] = "Left mouse is not pressing";
static char sRM[100] = "Right mouse is not pressing";
static char sKQ[500];
static int x,y;
switch (t2)
{
case WM_MOUSEMOVE:
x=LOWORD(t4); y=HIWORD(t4);
sprintf(sXY,"Position of mouse is : (x,y)=(%d,%d)",x,y);
break;
case WM_LBUTTONDOWN:
sprintf(sLM,"Left mouse is pressing");
break;
case WM_LBUTTONUP:
sprintf(sLM,"Left mouse is not pressing");
break;
case WM_RBUTTONDOWN:
sprintf(sRM,"Right mouse is pressing");
break;
case WM_RBUTTONUP:
sprintf(sRM,"Right mouse is not pressing");
break;
case WM_DESTROY:
PostQuitMessage(0);
break;
default:
return DefWindowProc(t1, t2, t3, t4);
}
sprintf(sKQ, "%s; %s; %s", sXY, sLM, sRM );
SetWindowText( t1, sKQ );
return 0;
}
Trong chương trình có sử dụng biểu tượng (icon) đăng ký vào lớp cửa sổ bằng
lệnh (1). Hàm xử lý thông điệp (tên ‘WndProc’) sử dụng các

31
LẬP TRÌNH HỆ THỐNG

biến xâu ký tự (mảng kiểu ‘char’) chứa thông báo in lên tiêu đề
cửa sổ, xâu này được thiết lập giá trị là tọa độ chuột hay trạng
thái chuột tại các trường hợp xẩy ra thông điệp tương ứng
(‘case’). Các biến này được dùng lại giá trị nhiều lần nên phải
khai báo dạng ‘static’, hoặc đưa lên thành biến toàn cục. Cuối
hàm chuyển các xâu ký tự thành một xâu kết quả (sKQ) để hiện
lên tiêu đề cửa sổ.
Chương trình có sử dụng lệnh ‘sprintf’ của thư viện <stdio.h> trong C/C ++.
Lệnh này chuyển các dữ liệu về dạng chuỗi ký tự theo định
dạng in. Ví dụ minh hoạ kết quả chương trình tại một thời điểm
có giữ chuột phải như sau:

3.2.4. Lập trình xử lý đồ họa


a) Cơ chế lập trình đồ họa
Windows là hệ điều hành có giao diện đồ họa, lập trình trên windows là
thường phải làm việc với đồ họa trên mọi thành phần của
chương trình. Thư viện các hàm đồ họa cung cấp cho người lập
trình rất lớn và khá mềm dẻo, với khuôn khổ tài liệu này chúng
tôi cung cấp một số cách làm việc đơn giản.
Hệ tọa độ đồ họa trên cửa sổ được xác lập bởi gốc ở góc trái trên, trục ngang
là chiều X tăng từ trái sang phải và trục dọc là chiều Y tăng từ
trên xuống. Đơn vị chia trên trục X và Y là đơn vị lôgic, tương
đương với điểm ảnh (pixels), tuy nhiên có thể thay đổi đơn vị
lôgic này (xem lệnh ở sau).

32
LẬP TRÌNH HỆ THỐNG

Trên cửa sổ đồ họa luôn tồn tại một điểm vẽ hiện thời, điểm này không được
hiển thị trên cửa sổ và có thể thay đổi bởi các hàm vẽ. Lúc hiển
thị cửa sổ điểm này đặt tại gốc 0,0.
Windows cung cấp cơ chế đồ họa thông qua thiết bị ảo (Device Context -
DC), nó là một cấu trúc bộ nhớ lưu giữ các yếu tố, tham số liên
quan đến việc xử lý đồ họa trên thiết bị vật lý, tạo cho người lập
trình đơn giản hóa công việc xử lý. Mỗi thời điểm vẽ có một
đối tượng DC tương ứng với thiết bị vậy lý hay đối tượng mà ta
xử lý đồ họa. Chẳng hạn, khi vẽ lên một cửa sổ ta có một DC,
khi vẽ ra giấy cho máy in ta lại có một DC khác,... (hình vẽ
minh họa).

Mỗi đối tượng DC được định danh bởi kiểu dữ liệu HDC (Handle of Device
Context). Các lệnh xử lý đồ họa đều phải thực hiện qua đối
tượng có định danh kiểu này, và nó luôn là tham số đầu của các
lệnh đồ họa.
Thông thường chúng ta có 3 bước sau để lập trình xử lý đồ họa nói chung trên
Windows:
/*Bước 1) Lấy đối tượng đồ họa HDC */
HDC  = GetWindowDC( HWND ); /*Đồ họa trên cửa sổ*/
/*Bước 2) Thiết lập các tham số (nếu cần) cho HDC */
...

33
LẬP TRÌNH HỆ THỐNG

/*Bước 3) Thực hiện các lệnh đồ họa trên HDC */


...
Trong đó, lệnh ‘GetDC’ để lấy đối tượng đồ hoạ HDC để vẽ trên nền cửa sổ
có số hiệu định danh kiểu HWND, còn ‘GetWindowDC’ lấy
HDC cho việc đồ họa trên toàn bộ cửa sổ.
Sau đây chúng ta xem xét một số thao tác xử lý đơn giản.
b) Một số xử lý đồ họa cơ bản
 Các lệnh vẽ đơn giản
 Vẽ một điểm ảnh:
COLORREF SetPixel( HDC, int x, int y, COLORREF crColor );
Phải đưa vào tọa độ x,y điểm cần đặt và màu cho điểm đó. Giá trị trả về
là màu cũ của điểm vẽ.
 Dịch chuyển điểm vẽ hiện thời (sẽ không nhìn thấy điểm này)
BOOL MoveToEx( HDC, int x, int y, NULL );
Hàm trả về giá trị tọa độ điểm kiểu POINT, đó là cấu trúc điểm khai
báo theo mẫu sau:
typedef struct tagPOINT
{
LONG x;
LONG y;
} POINT, *PPOINT, NEAR *NPPOINT, FAR *LPPOINT;

 Vẽ đoạn thẳng (từ điểm hiện tại):


BOOL LineTo( HDC, int x, int y );
Chúng ta thường dùng cặp lệnh MoveTo và LineTo để vẽ đoạn thẳng có
điểm đầu, điểm cuối mong muốn.
 Vẽ hình cung
BOOL Arc( HDC, int x1, int y1, int x2, int y2,
int x3, int y3, int x4, int y4 );
BOOL Arc( HDC, LPCRECT lpRect,POINT ptStart,POINT ptEnd );
Cung tức là một phần của elíp hoặc hình tròn, để vẽ hình cung chúng ta
phải xác định các tham số gồm một hình chữ nhật ngoại tiếp elip hoặc
hình tròn của cung đó, điểm xác định đường thẳng nối với tâm tạo
thành góc chắn cung thứ nhất, điểm xác định đường thẳng nối với tâm
tạo thành góc chắn cung thứ hai. Xác định theo hình vẽ sau:

34
LẬP TRÌNH HỆ THỐNG

Ngoài ra có lệnh vẽ cung tròn theo góc chắn cung như sau:
BOOL AngleArc(HDC,int x,int y, int nR, float fS,float fE );
Trong đó x,y là tâm của hình tròn chứa cung, nR là bán kính, fS là góc bắt
đầu vẽ, fE là góc chắn cung cần vẽ.

 Vẽ hình chữ nhật


BOOL Rectangle( HDC, int x1, int y1, int x2, int y2 );
BOOL Rectangle( HDC, LPCRECT lpRect );
Vẽ hình chữ nhật được xác định tọa độ hai điểm ở hai góc đối diện, góc
trái trên là x1,y1 và góc phải dưới là x2,y2. Có thể xác định bằng kiểu
RECT ở mẫu 2. Hình chữ nhật sẽ được tự động tô theo kiểu tô đã chọn
sẵn.
Nếu cần vẽ hình chữ nhật có vát góc bằng cung elíp ta sử dụng lệnh
sau:
BOOL RoundRect( HDC, int x1, int y1, int x2, int y2,
int x3, int y3 );
BOOL RoundRect( HDC, LPCRECT lpRect, POINT point );
Với góc được vát xác định bằng độ rộng và độ cao của elíp cần thực
hiện, đó là giá trị x3 và y3 hoặc tham số point ở mẫu 2.
Ví dụ với mẫu vẽ bằng 2 lệnh sau sẽ cho kết quả như hình bên dưới
(trong đó ‘dc’ là biến chứa định danh đối tượng đồ họa DC):
Rectangle( dc, 50,50,450,250);
RoundRect( dc, 50,50,450,250,300,150);

35
LẬP TRÌNH HỆ THỐNG

 Vẽ hình elíp
BOOL Ellipse( HDC, int x1, int y1, int x2, int y2 );
BOOL Ellipse( HDC, LPCRECT lpRect );
Trong đó x1,y1 và x2,y2 là tọa độ hai điểm xác định hình chữ nhật
ngoại tiếp elíp cần vẽ, hoặc xác định qua cấu trúc kiểu RECT ở mẫu 2.
Hình elíp sẽ được tô theo mẫu tô hiện thời.
 Vẽ hình quạt
BOOL Pie( HDC, int x1, int y1, int x2, int y2,
int x3, int y3, int x4, int y4 );
BOOL Pie( HDC, LPCRECT lpRect,POINT ptStart,POINT ptEnd );
Trong đó x1,y1 và x2,y2 xác định tọa độ hình chữ nhật ngoại tiếp elíp
chứa hình quạt, x3,y3 xác định điểm nối tâm của đường góc chắn cung
thứ nhất và x4,y4 là góc chắn cung thứ 2 (các tham số giống lệnh vẽ
cung Arc). Ở đây vẽ hình quạt nên sẽ có thêm hai đường góc chắn cung
đồng thời được tô nền theo mẫu tô hiện thời.
 Vẽ dòng văn bản
BOOL TextOut( HDC, int x, int y, LPCTSTR text, int count);
BOOL DrawText ( HDC, LPCTSTR text, int count,
LPRECT rect, UINT format);
Vẽ xâu ký tự ‘text’ ra tại tọa độ x,y với số ký tự là count. Hoặc lệnh
‘DrawText’ vẽ ra theo giới hạn bởi một hình chữ nhật kiểu RECT có
{left, top, right, bottom} và định dạng format gồm DT_CENTER,
DT_LEFT, DT_RIGHT,... cho việc căn chỉnh văn bản.
Ví dụ 3.2.10: Lập trình xử lý giữ chuột trái và kéo để vẽ đường tự do trên cửa
sổ. Xử lý nhấn chuột phải để hiện ra họ và tên của bạn tại vị trí
chuột.
Chương trình sử dụng 2 cặp tọa độ (x1,y1) để chứa điểm xuất phát (xác lập
khi nhấn chuột trái) và (x2,y2) chứa tọa độ điểm đang kéo
chuột. Sau mỗi lần vẽ (đoạn thẳng từ (x1,y1) đến (x2,y2)) thì
tọa độ (x1,y1) được cập nhật bằng tọa độ (x2,y2) cho việc vẽ

36
LẬP TRÌNH HỆ THỐNG

đoạn mới tạo thành đường gấp khúc (đoạn vẽ rất ngắn) gần như
là đường cong tự do. Các biến chứa tọa độ này phải khai báo
dạng ‘static’ vì giá trị của nó ở lần xử lý trước được dùng cho
lần xử lý sau, nên cần phải lưu giữ chúng.
Trong thông điệp WM_MOUSEMOVE chúng ta kiểm tra tham số kiểu
WPARAM (t3) để biết có đang giữ chuột trái hay không để vẽ.
#include<windows.h>
#include<stdio.h>
#define wt "Ve tu do & hien ho ten" /*tieu de cua so*/
#define wn "W1" /*ten lop cua so*/

LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);

/**********Ham WinMain cua chuong trinh*************/


int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE, LPSTR, int ) {
MSG msg;
WNDCLASS wc;
HWND hWnd;

/*Dinh nghia lop cua so*/


ZeroMemory(&wc,sizeof(wc));
wc.style = CS_HREDRAW | CS_VREDRAW |
CS_DBLCLKS;
wc.lpfnWndProc = (WNDPROC)WndProc;
wc.hInstance = hInstance;
wc.hIcon = LoadIcon(NULL,IDI_INFORMATION); /*(1)*/
wc.hbrBackground= (HBRUSH)(COLOR_WINDOW+1);
wc.lpszClassName = wn;

/*Dang ky lop cua so*/


if (!RegisterClass(&wc)) return 0;

/*Tao cua so thuc su*/


hWnd = CreateWindow( wn, wt, WS_OVERLAPPEDWINDOW
| WS_HSCROLL | WS_VSCROLL,
CW_USEDEFAULT, CW_USEDEFAULT,
CW_USEDEFAULT, CW_USEDEFAULT,
NULL, NULL, hInstance, NULL);

/*Hien thi va cap nhat noi dung cua so*/


ShowWindow(hWnd,1);
UpdateWindow(hWnd);

/* Vong lap thong diep */

37
LẬP TRÌNH HỆ THỐNG

while (GetMessage(&msg, NULL, 0, 0)) {


TranslateMessage(&msg);
DispatchMessage(&msg);
}

return msg.wParam;
}

/********* Ham cua so cua chuong trinh ************/


LRESULT CALLBACK WndProc(HWND t1, UINT t2, WPARAM t3, LPARAM
t4) {
HDC d;
static int x1=0,y1=0,x2,y2;
switch (t2)
{
case WM_MOUSEMOVE:
if(t3==MK_LBUTTON){
x2 = LOWORD(t4); y2 = HIWORD(t4);
d = GetDC(t1);
MoveToEx(d,x1,y1,0); LineTo(d,x2,y2);
x1 = x2; y1 = y2;
DeleteDC(d);
}
break;
case WM_LBUTTONDOWN:
x1 = LOWORD(t4); y1 = HIWORD(t4);
break;
case WM_RBUTTONDOWN:
x1 = LOWORD(t4); y1 = HIWORD(t4);
d = GetDC(t1);
TextOut(d,x1,y1,"Nguyen Van An",13);
DeleteDC(d);
break;
case WM_DESTROY:
PostQuitMessage(0);
break;
default:
return DefWindowProc(t1, t2, t3, t4);
}
return 0;
}

Kết quả chương trình sau khi nhấn chuột phải và kéo chuột trái trên cửa sổ
như sau:

38
LẬP TRÌNH HỆ THỐNG

 Quy định các tham số đồ họa (vẽ, tô màu, văn bản,...)


Các thành phần hiện trên cửa sổ đều được thực hiện bằng phương pháp vẽ
và tô các hình ảnh, có hai đối tượng quan trọng khi vẽ đồ họa là nét bút
(pens) và mẫu tô (brushes).
Nét bút là đối tượng kiểu HPEN dùng để quy định nét vẽ cho các lệnh vẽ,
mỗi đối tượng nét bút bao gồm màu sắc, độ rộng nét, kiểu nét liền, chấm,
đứt quãng,...
Mẫu tô là đối tượng tạo kiểu BRUSH dùng để quy định mẫu tô cho các
lệnh tô nền hình vẽ, mỗi mẫu tô có kiểu tô, màu kiểu tô.
Ngoài ra chúng ta có thể quy định các tham số đồ họa văn bản như màu
chữ, kiểu chữ, màu nền chữ,... và chế độ ánh xạ đơn vị vẽ.
 Đối tượng HPEN
HPEN  = CreateCPen(int s,int w,COLORREF color);
Hàm tạo nét bút với ‘s’ là kiểu nét bút, ‘w’ là độ rộng, ‘color’ là màu
nét bút. Màu nét bút có thể dùng macro sau để thiết lập màu tùy ý:
COLORREF RGB( Red , Green , Blue )
Kiểu nét bút bao gồm các giá trị sau:
Giá trị Ý nghĩa Giá trị Ý nghĩa
PS_SOLID Nét liền PS_DASHDOT Gạch chấm
PS_DASH Nét gạch PS_DASHDOTDOT Gạch hai chấm
PS_DOT Nét chấm PS_NULL Không thấy
 Đối tượng HBRUSH
HBRUSH  = CreateSolidBrush(COLORREF color);
HBRUSH  = CreateHatchBrush(int style,COLORREF color);

39
LẬP TRÌNH HỆ THỐNG

Hàm tạo mẫu tô với các tham số ‘style’ quy định kiểu tô, màu được quy
định bởi ‘color’.
Kiểu tô bao gồm các giá trị sau:
Giá trị Ý nghĩa
HS_BDIAGONAL Nghiêng xuống 45o
HS_CROSS Chữ thập
HS_DIAGCROSS Chữ thập nghiêng
HS_FDIAGONAL Nghiêng lên 45o
HS_HORIZONTAL Kẻ ngang
HS_VERTICAL Kẻ dọc
Hàm ‘CreateSolidBrush’ tạo mẫu tô đặc do đó không quy định kiểu tô.
Chú ý: Các đối tượng quy định tham số đồ họa cần phải được đưa vào
thiết bị ảo phục vụ đồ họa (HDC) trước khi xử lý bằng lệnh sau:
SelectObject( HDC ,  );
Trong đó, tham số  là đối tượng quy các tham số đồ họa như HPEN,
HBRUSH,...
 Quy định chế độ vẽ trộn màu
int SetROP2( HDC hdc, int fnDrawMode );
Hàm này thường dùng để quy định vẽ trực tuyến đối với tương tác
người dùng, tức là khi một người dùng tương tác vẽ thì cơ chế vẽ này
cho phép vẽ/xóa liên tục để tạo hình vẽ mong muốn. Trong đó, tham số
‘fnDrawMode’ quy định chế độ đảo trộn màu khi vẽ như sau:
Chế độ trộn Ý nghĩa
R2_MASKNOTPEN Trộn màu pixel của nền và đảo màu bút vẽ.
R2_MASKPEN Trộn màu cả nền và bút vẽ.
R2_MASKPENNOT Trộn màu bút vẽ với đào màu nền.
R2_NOTMASKPEN Chế độ đảo của R2_MASKPEN.
R2_NOTXORPEN Chế độ đảo của R2_XORPEN.
R2_XORPEN Trộn màu của nền và bút vẽ theo chế độ xor bít.
 Chọn màu từ hộp thoại hệ thống
Windows cung cấp hộp thoại dùng chung để chọn màu từ hệ thống
thông qua việc gọi hàm ChooseColor như sau:

40
LẬP TRÌNH HỆ THỐNG

BOOL ChooseColor( LPCHOOSECOLOR pcolor );


Tham số ‘pcolor’ là địa chỉ biến nhớ kiểu CHOOSECOLOR nhằm quy
định các tham số cho việc sử dụng hộp thoại màu. Nó là một cấu trúc
gồm các thành phần sau:
typedef struct {
DWORD lStructSize; //độ lớn (=sizeof())
HWND hwndOwner; //cửa sổ chứa (=0 có thể)
HWND hInstance; //định danh chương trình
COLORREF rgbResult; //màu kết quả chọn
COLORREF *lpCustColors; //các màu tùy chọn thêm
DWORD Flags; //cờ (=CC_ANYCOLOR, CC_FULLOPEN,...)
LPARAM lCustData; //dữ liệu của hàm xử lý (=0)
LPCCHOOKPROC lpfnHook; //hàm xử lý nếu có (=0)
LPCTSTR lpTemplateName; //tên mẫu (=0)
} CHOOSECOLOR, *LPCHOOSECOLOR;
Kết quả trả về của màu được chọn là thành phần ‘rgbResult’ của biến
kiểu cấu trúc trên. Giá trị hàm trả về là TRUE/FALSE tương ứng người
dùng chọn nút lệnh đồng ý (Ok) hoặc hủy bỏ (Cancel) trên hộp thoại
màu.
 Quy định chế độ văn bản:
Mặc định khi hiện văn bản bằng lệnh TextOut() sẽ có màu chữ đen và
nền trắng, có thể đặt lại chế độ màu này bằng các lệnh như sau:
COLORREF SetTextColor( HDC, COLORREF crColor );
Đặt màu chữ với giá trị màu là crColor có thể xác định màu qua macro
RGB(red,green,blue). Tương tự hàm đặt màu nền văn bản là:
COLORREF SetBkColor( HDC, COLORREF crColor );
Hai hàm trên đều trả về giá trị màu trước đó của cửa sổ.
Khi hiện văn bản lên cửa sổ có thể thực hiện theo hai chế độ nền khác
nhau, đó là chế độ OPAQUE (không trong suốt) hoặc chế độ
TRANSPARENT (trong suốt). Ở chế độ OPAQUE văn bản hiện có màu
nền quy định bằng lệnh SetBkColor(), còn ở chế độ TRANSPARENT
văn bản hiện không có nền khi đó nền sẽ là màu nền cửa sổ.
Lệnh đặt chế độ hiện văn bản khai báo theo mẫu sau:
int SetBkMode( HDC, int nBkMode );

41
LẬP TRÌNH HỆ THỐNG

Tham số nBkMode quy định chế độ cần đặt là OPAQUE hoặc


TRANSPARENT. Hàm trả về giá trị chế độ trước đó.
Mặc định nền cửa sổ hiện văn bản ở chế độ OPAQUE. Ví dụ hình vẽ sau,

cho thấy chữ ABC hiện ra có màu nền với chế độ OPAQUE, số 123 hiện
không có màu nền với chế độ TRANSPARENT.
Ngoài ra chúng ta có thể quy định cấu trúc phông chữ hiển thị, các bạn
xem thêm chi tiết phần tài liệu tham khảo của [4].
Ví dụ 3.2.11: Lập trình xử lý kéo chuột trái vẽ hình elíp nội tiếp vùng chữ nhật
đang kéo chuột, xử lý vẽ hình tròn nếu có giữa thêm phím Ctrl.
Hình vẽ này có màu đỏ. Xử lý nhấn chuột phải để tô kín màu
cho vùng giới hạn bởi các hình vừa vẽ, nhấn phím F1 để chọn
màu tô từ hộp thoại màu.
Để vẽ hình trực tiếp theo tác động chuột phải sử dụng lệnh thiết lập chế độ vẽ
đảo màu (SetROP2) theo chế độ R2_NOTXORPEN. Vẽ đường
elíp hoặc đường tròn (không có nền) ta dùng lệnh Arc.
Trong chương trình sử dụng biến trạng thái ‘ttv’ để kiểm tra đang ở chế độ vẽ
hay không (được xác lập bắt đầu vẽ khi nhấn chuột trái), cặp
tọa độ x3,y3 để lưu tọa độ điểm nhấn chuột khi cần khôi phục
vẽ không đối xứng. Hai cặp tọa độ x1,y1 và x2,y2 để vẽ hình,
khi vẽ đối xứng thì điểm x3,y3 nằm giữa hai điểm x1,y1 và
x2,y2 do đó suy ra công thức tính x1,y1 cho trường hợp này, cụ
thể:
x1 = 2*x3 - x2; y1 = 2*y3 - y2;
Trong thông điệp WM_MOUSEMOVE ta kiểm tra trạng thái vẽ (ttv), nếu nó
bằng 1 (đang vẽ) thì xử lý xóa hình vẽ trước đó bằng cách vẽ lại
nó. Sau đó kiểm tra phím Ctrl có được giữ bằng lệnh
‘GetKeyState’ để vẽ đối xứng hay không.
Nội dung chương trình như sau:
#include<windows.h>
#include<stdio.h>

#define wt "Ve tu do & hien ho ten" /*tieu de cua so*/


#define wn "W1" /*ten lop cua so*/

42
LẬP TRÌNH HỆ THỐNG

LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);

/**********Ham WinMain cua chuong trinh*************/


int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE, LPSTR, int ) {
MSG msg;
WNDCLASS wc;
HWND hWnd;

/*Dinh nghia lop cua so*/


ZeroMemory(&wc,sizeof(wc));
wc.style = CS_HREDRAW | CS_VREDRAW |
CS_DBLCLKS;
wc.lpfnWndProc = (WNDPROC)WndProc;
wc.hInstance = hInstance;
wc.hIcon = LoadIcon(NULL,IDI_INFORMATION); /*(1)*/
wc.hbrBackground= (HBRUSH)(COLOR_WINDOW+1);
wc.lpszClassName = wn;

/*Dang ky lop cua so*/


if (!RegisterClass(&wc)) return 0;

/*Tao cua so thuc su*/


hWnd = CreateWindow(wn, wt, WS_OVERLAPPEDWINDOW
| WS_HSCROLL | WS_VSCROLL,
CW_USEDEFAULT, CW_USEDEFAULT,
CW_USEDEFAULT, CW_USEDEFAULT,
NULL, NULL, hInstance, NULL);

/*Hien thi va cap nhat noi dung cua so*/


ShowWindow(hWnd,1);
UpdateWindow(hWnd);

/* Vong lap thong diep */


while (GetMessage(&msg, NULL, 0, 0)) {
TranslateMessage(&msg);
DispatchMessage(&msg);
}

return msg.wParam;
}

/********* Ham cua so cua chuong trinh ************/


LRESULT CALLBACK WndProc( HWND t1, UINT t2,
WPARAM t3, LPARAM t4) {
HDC d;
HPEN p;
HBRUSH b;

43
LẬP TRÌNH HỆ THỐNG

static COLORREF mauto = 0;


static int x1,y1,x2,y2,x3,y3,ttv=0;
switch (t2)
{
case WM_LBUTTONUP:
d = GetDC(t1);
p = CreatePen(1,3,RGB(255,0,0));
SelectObject(d,p);
Arc(d,x1,y1,x2,y2,x1,y1,x1,y1);
DeleteDC(d);
DeleteObject(p);
ttv = 0;
break;
case WM_LBUTTONDOWN:
x1 = LOWORD(t4); y1 = HIWORD(t4);
x3 = x2 = x1; y3 = y2 = y1;
d = GetDC(t1);
p = CreatePen(1,3,RGB(255,0,0));
SelectObject(d,p);
SetROP2(d,R2_NOTXORPEN);
Arc(d,x1,y1,x2,y2,x1,y1,x1,y1);
DeleteDC(d);
DeleteObject(p);
ttv = 1;
break;
case WM_MOUSEMOVE:
if(ttv==1){
d = GetDC(t1);
p = CreatePen(1,3,RGB(255,0,0));
SelectObject(d,p);
SetROP2(d,R2_NOTXORPEN);
Arc(d,x1,y1,x2,y2,x1,y1,x1,y1);
x2 = LOWORD(t4); y2 = HIWORD(t4);
if(GetKeyState(VK_CONTROL)&0x8000){
x1 = 2*x3 - x2; y1 = 2*y3 - y2;
}else{
x1 = x3; y1 = y3;
}
Arc(d,x1,y1,x2,y2,x1,y1,x1,y1);
DeleteDC(d);
DeleteObject(p);
}
break;

44
LẬP TRÌNH HỆ THỐNG

case WM_KEYDOWN:
if(t3==VK_F1){
static COLORREF mau_tc[16];
CHOOSECOLOR cc;
ZeroMemory(&cc, sizeof(cc));
cc.lStructSize = sizeof(cc);
cc.hwndOwner = t1;
cc.lpCustColors = (LPDWORD)mau_tc;
cc.rgbResult = mauto;
cc.Flags = CC_FULLOPEN | CC_RGBINIT;

if (ChooseColor(&cc)==TRUE) {
mauto = cc.rgbResult;
}
}
break;
case WM_RBUTTONDOWN:
x1 = LOWORD(t4); y1 = HIWORD(t4);
d = GetDC(t1);
b = CreateSolidBrush(mauto);
SelectObject(d,b);
FloodFill(d,x1,y1,RGB(255,0,0));
DeleteDC(d);
DeleteObject(b);
break;
case WM_DESTROY:
PostQuitMessage(0);
break;
default:
return DefWindowProc(t1, t2, t3, t4);
}
return 0;
}
Kết quả chạy chương trình sau khi vẽ một số hình và tô màu:

45
LẬP TRÌNH HỆ THỐNG

Thông điệp nhấn phím F1 sẽ hiển thị hộp thoại chọn màu có dạng sau:

 Chế độ ánh xạ, khung nhìn và đơn vị vẽ trên cửa sổ


Các lệnh vẽ đều thực hiện trên các đơn vị tọa độ dạng lôgíc, mặc định
đơn vị lôgíc tương đương với đơn vị vật lý (pixels). Tuy nhiên chúng ta
có thể thay đổi tỷ lệ này cho phù hợp, ta gọi tỷ lệ này là chế độ ánh xạ
(mapping modes).
Ngoài ra trên cửa sổ theo mặc định sẽ có hệ trục tọa độ có gốc ở góc trái
trên, chiều X là trái sang phải, chiều Y là trên xuống dưới. Các lệnh vẽ
chỉ thực hiện trên tọa độ nằm trong vùng cửa sổ nhìn thấy gọi là khung
nhìn (viewport). Chúng ta có thể thay đổi kích thước khung nhìn bằng
cách định nghĩa chiều rộng và chiều cao của cửa sổ, sau khi có khung
nhìn mới thì các lệnh vẽ chỉ thực hiện được trong khung nhìn đó.
Các lệnh thực hiện thiết lập chế độ ánh xạ đơn vị vẽ, gồm:
int SetMapMode( HDC, int nMapMode );

46
LẬP TRÌNH HỆ THỐNG

Hàm này trả về chế độ ánh xạ trước đó nếu đặt thành công, ngược lại trả
về giá trị 0. Chúng ta thấy mặc định là chế độ MM_TEXT. Trong đó,
tham số nMapMode quy định chế độ cần đặt bao gồm các giá trị sau:
Giá trị Ý nghĩa
MM_HIENGLISH Một đơn vị lôgic bằng 0.001 inch, trục x
dương sang phải, trục y âm
xuống dưới
MM_HIMETRIC Một đơn vị lôgic bằng 0.01 mm, trục x dương
sang phải, trục y âm xuống dưới
MM_ISOTROPIC Đơn vị lôgic bằng với đơn vị người dùng
giống nhau cả hai trục X và Y
MM_ANISOTROPIC Theo đơn vị người dùng đặt nhưng khác nhau
giữa hai trục X và Y
MM_LOENGLISH Một đơn vị lôgic bằng 0.1 mm, trục x dương
sang phải, trục y âm xuống dưới
MM_LOMETRIC Một đơn vị lôgic bằng 0.01 inch, trục x dương
sang phải, trục y âm xuống dưới
MM_TEXT Một đơn vị lôgic bằng một đơn vị vật lý pixels
MM_TWIPS Bằng 20 điểm máy in (1/1440 inch)
Khi chọn chế độ ánh xạ đơn vị là MM_ISOTROPIC cho phép chúng ta
định nghĩa lại kích thước cửa sổ theo đơn vị lôgíc. Để định nghĩa độ dài
trục X và độ cao trục Y cho cửa sổ ta sử dụng hàm theo mẫu sau:
SIZE SetWindowExt( HDC, int cx, int cy );
SIZE SetWindowExt( HDC, SIZE size );
Giá trị cx và cy quy định độ rộng và độ cao của hai trục X, Y tương ứng.
Khi đó đơn vị vẽ lôgíc trên cửa sổ có độ chia theo hai trục ứng với giá trị
cx và cy này. Hàm trả về kích thước trước đó của cửa sổ dưới kiểu CSize
của cấu trúc SIZE như sau:
typedef struct tagSIZE {
LONG cx; //độ rộng
LONG cy; //chiều ngang
} SIZE;

47
LẬP TRÌNH HỆ THỐNG

Chú ý rằng khi thay đổi kích thước cửa sổ bằng lệnh trên sẽ không làm
thay đổi kích thước vật lý cửa sổ, chỉ thay đổi tỷ lệ giữa đơn vị lôgíc với
đơn vị vật lý là pixels.
Ví dụ cửa sổ có kích thước vật lý là 200x100, sau khi đặt kích thước lôgíc
là 400x160 thì tỷ lệ sẽ là 200/400=0.5 và 100/160=0.625 (mỗi đơn vị
lôgíc ứng với 0.5 pixel theo chiều ngang và 0.625 theo chiều dọc).
Lệnh định nghĩa khung nhìn trên cửa sổ như sau:
SIZE CDC::SetViewportExt( int cx, int cy );
SIZE CDC::SetViewportExt( SIZE size );
Giá trị cx và cy quy định độ rộng và độ cao của khung nhìn cần đặt, hoặc
bởi tham số size theo mẫu 2. Hàm trả về giá trị khung nhìn trước đó. Lệnh
này chỉ thực hiện được khi chế độ ánh xạ kiểu người dùng là
MM_ISOTROPIC.
Chúng ta có thể định nghĩa khung nhìn với kích thước bất kỳ và mọi lệnh
vẽ chỉ thực hiện được với tọa độ trong khung nhìn đó. Trường hợp muốn
lệnh vẽ chỉ thực hiện một phần trên cửa sổ thì thực hiện lệnh này để xác
lập, lệnh này phải thực hiện sau lệnh đặt chế độ ánh xạ đơn vị.
Lệnh đặt gốc tọa độ lôgíc cho khung nhìn: Mặc định gốc tọa độ lôgíc ở
góc trái trên, tuy nhiên chúng ta có thể thay đổi gốc này bởi lệnh sau:
POINT SetViewportOrg( HDC, int x, int y );
POINT SetViewportOrg( HDC, POINT point );
Tọa độ gốc mới cần đặt quy định bởi tham số x và y hoặc giá trị point theo
mẫu 2. Hàm trả về tọa độ gốc trước đó qua đối tượng POINT.
Chú ý: Các lệnh SetWindowExt() và SetViewportExt() chỉ có tác dụng khi
chúng ta đặt chế độ ánh xạ là MM_ISOTROPIC hoặc
MM_ANISOTROPIC, vì vậy sau khi đặt chế độ này chúng ta phải gọi hai
hàm trên mới thực hiện các lệnh vẽ được.
c) Thông điệp WM_PAINT và cửa sổ ảo
Thông điệp WM_PAINT được sinh ra và gửi đến chương trình khi cửa sổ
có sự thay đổi về trạng thái. Theo cơ chế đồ họa của Windows, một cửa sổ
thay đổi thì máy vẽ lại cửa sổ đó và tô lại nền cửa sổ, vậy chương trình phải
xử lý thông điệp này để khôi phục các kết quả đồ họa bị mất trên cửa sổ (do
máy tô lại nền). Các trạng thái thay đổi như phóng to, thu nhỏ, có giãn, ẩn
hiện,....

48
LẬP TRÌNH HỆ THỐNG

Thông điệp này được phát sinh lần đầu tiên khi cửa sổ được hiển thị, sau đó
được phát sinh liên tiếp mỗi khi có sự thay đổi trạng thái trên cửa sổ.
Trong phần xử lý thông điệp này ta dùng phải lệnh BeginPaint() để bắt đầu
hiện quá trình đồ họa và sau khi thực hiện xong dùng lệnh EndPaint() để
kết thúc, cú pháp 2 lệnh đó là:
HDC BeginPaint( LPPAINTSTRUCT lpPaint );
void EndPaint( LPPAINTSTRUCT lpPaint );
Lệnh thứ nhất trả về đối tượng đồ họa HDC, ta sử dụng nó để thực hiện quá
trình đồ họa trên cửa sổ. Hoặc có thể xác định HDC trong cấu trúc của biến
kiểu PAINTSTRUCT như sau:
typedef struct _PAINTSTRUCT {
HDC hdc;
BOOL fErase;
RECT rcPaint;
BOOL fRestore;
BOOL fIncUpdate;
BYTE rgbReserved[32];
} PAINTSTRUCT;
Trong đó:
 hdc: lưu số hiệu ngữ cảnh thiết bị ứng với cửa sổ,
 fErase: cho biết chúng ta có phải vẽ lại nền cửa sổ hay không,
 rcPaint: cho biết tọa độ kích thước của khung cửa sổ được hiện thị,
 fRestore, fIncUpdate, rgbReserved: là các tham số dành riêng cho hệ
thống.
Mẫu của chương trình xử lý sự kiện cho thông điệp WM_PAINT sẽ được
viết như sau:
/*Bước 1: Khởi động quá trình đồ họa */
PAINTSTRUCT ps;
HDC dc = BeginPaint( &ps );
/*Bước 2: Thực hiện đồ họa */
... /*viết các lệnh xử lý đồ họa ở đây */ ...
/*Bước 3: Kết thúc đồ họa */
EndPaint( &ps );
Cơ chế đồ họa lên cửa sổ thực hiện qua thông điệp WM_PAINT này có thể
thực hiện theo 2 phương pháp:

49
LẬP TRÌNH HỆ THỐNG

 Thứ nhất, mọi thông tin liên quan đến xử lý đồ họa (dữ liệu) trước đó
được lưu trong các biến nhớ, sau đó sử dụng để vẽ lại trong thông điệp
này nhằm khôi phục các kết quả đồ họa đã làm trước đó;
 Thứ hai, sử dụng cơ chế hình ảnh để lưu nội dung đồ họa trên cửa sổ
thành một bức ảnh (dạng bitmap). Sau đó khi cần khôi phục (trong
thông điệp này) chúng ta chỉ cần đưa ảnh đã lưu ra màn hình là được.
Phương pháp này không cho phép xử lý từng đối tượng vẽ riêng biệt
nên không phù hợp cho các bài toán xử lý vẽ, quản lý từng đối tượng
rời rạc.
Phương pháp sử dụng ảnh để lưu màn hình đồ họa được thực hiện thông
qua cơ chế cửa sổ ảo (virtual window), sẽ được trình bày ở sau.
Trong trường hợp chúng ta muốn tự động phát sinh và chạy thông điệp này
mà không có sự thay đổi trên cửa sổ thì sử dụng hàm sau:
void InvalidateRect ( LPCRECT lpRect, BOOL bErase);
Trong đó lpRect là cấu trúc kiểu RECT như trình bày ở trên chứa tọa độ
kích thước cửa sổ cần thực hiện cho thông điệp WM_PAINT, tuy nhiên có
thể đặt tham số này bằng NULL nếu thực hiện trên toàn bộ nền cửa sổ hiện
thời. Tham số thứ hai bErase cho biết có xóa và vẽ lại nền cửa sổ hay
không, thường đặt bằng TRUE.
Đối với phương pháp dùng cửa sổ ảo, chúng ta phải tạo một cửa sổ trên bộ
nhớ RAM để lưu nội dung đồ họa cho cửa sổ trên màn hình (ta gọi là cửa
sổ thật). Cửa sổ ảo này gồm 2 đối tượng:
(1) HDC: dùng để quy định các tham số đồ họa trên cửa sổ ảo;
(2) HBITMAP: dùng để lưu ảnh cửa sổ thật dưới dạng bitmap.
Các bước tạo cửa sổ ảo cho việc áp dụng phương pháp (2) như sau:
/*Bước 1) Khai báo các biến cho cửa sổ ảo */
HDC memDC;
HBITMAP memBM;
/*Bước 2) Tạo các đối tượng cửa sổ ảo */
memDC = CreateCompatibleDC( HDC-cửa-sổ-thật );
memBM = CreateCompatibleBitmap( HDC-cs-thật, w, h);
SelectObject( memDC, memBM );
/*Bước 3) Tô nền cửa sổ ảo (nếu cần) */
HBRUSH memBR;
memBR = CreateSolidBrush( màu-nền-cửa sổ ảo );

50
LẬP TRÌNH HỆ THỐNG

SelectObject( memDC, memBR );


PatBlt( memDC, 0,0, width, height, PATCOPY );

Trong đó, HDC cửa sổ thật nhằm để xác định tham số đồ họa của cửa sổ
ảo được lấy từ cửa sổ nào. Như vậy chúng ta được đối tượng cửa sổ ảo và
biến ‘memDC’ sẽ là đối tượng đồ họa để xử lý trên cửa sổ ảo này.
Phương pháp thứ hai yêu cầu trong quá trình xử lý đồ họa cần phải lưu giữ
nội dung đồ họa có được trên cửa sổ thật vào cửa sổ ảo, tại thời điểm bất
kỳ có sự thay đổi về nội dung và cần lưu lại. Có 2 cách lưu giữ:
 Cách 1: Vẽ lại đúng những gì (kể cả các tham số vẽ) đã vẽ trên cửa sổ
thật vào cửa sổ ảo;
 Cách 2: Sao chép toàn bộ hình ảnh từ cửa sổ thật vào cửa sổ ảo, bằng
lệnh BitBlt như sau:
BitBlt(dst, x1,y1, w,h, src, x2,y2, mode );
Trong đó, ‘dst’ và ‘src’ là hai đối tượng HDC đích và nguồn cần sao chép
ảnh (nếu chép từ cửa sổ thật lên cửa sổ ảo thì ‘dst’ là HDC cửa sổ ảo, ‘src’
là HDC cửa sổ thật). x1,y1 xác định vị trí đích chép đến trên ‘dst’, x2,y2 là
vị trí nguồn trên ‘src’ để bắt đầu chép, w,h là độ rộng, cao của vùng ảnh cần
chép và ‘mode’ là chế độ chép có thể gồm:
Giá trị Ý nghĩa
DSTINVERT Chế độ màu nội dung ảnh đích
MERGECOPY Trộn bằng phép AND giữa ảnh nguồn và đích
NOTSRCCOPY Chế độ đảo màu của nội dung ảnh nguồn
SRCCOPY Chép ảnh nguồn nguyên gốc

51
LẬP TRÌNH HỆ THỐNG

SRCINVERT Đảo màu nội dung ảnh nguồn,...


Tuy nhiên, có thể áp dụng phương pháp đó là trong mọi quá trình đồ họa,
chúng ta thực hiện vẽ trực tiếp vào cửa sổ ảo, sau đó được kết
quả sẽ sao chép vào cửa sổ thật trên màn hình để người dùng
quan sát và tương tác.
Ví dụ 3.2.12: Lập trình xử lý thông điệp WM_PAINT để vẽ đồ thị hàm số sin.
Xử lý nhấn chuột trái để xác định gốc tọa độ mới trên màn hình,
xử lý ấn phím mũi tên lên để phóng to đồ thị và phím mũi tên
xuống để thu nhỏ đồ thị (theo bước thay đổi tỷ lệ 0.01).
Hướng dẫn: Chúng ta sử dụng cơ chế vẽ bằng các đoạn thẳng gấp khúc để tạo
nên đường cong cho đồ thị, bắt đầu từ điểm có tọa độ x=a,
y=sin(s) dùng lệnh vẽ LineTo để vẽ đướng nối đến các điểm
liên tiếp có tọa độ x tăng dần theo dx và y được tính theo hàm
sin cho đến khi x>b. Trong đó [a,b] là giới hạn vẽ đồ thị trên
trục tọa độ. Để co giãn sử dụng thêm biến tyle và nhân các tọa
độ với biến này để thu phóng trên màn hình.
Tọa độ đề-các có trục OY ngược so với màn hình, gốc tọa độ màn hình là góc
trái trên cửa sổ nên chúng ta phải chuyển gốc đến vị trí OX,OY
để vẽ tùy ý trên cửa sổ đó. Vậy chúng ta chuyển tọa độ đề-các
(x,y) sang tọa độ màn hình là (mx=OX+x, my=OY-y).
Nội dung chương trình sẽ là:
#include<windows.h>
#include<math.h>

#define wt "Ve ham sin & co gian, dich goc" /*tieu de cua so*/
#define wn "W1" /*ten lop cua so*/

LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);

/**********Ham WinMain cua chuong trinh*************/


int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE, LPSTR, int ) {
MSG msg;
WNDCLASS wc;
HWND hWnd;

/*Dinh nghia lop cua so*/


ZeroMemory(&wc,sizeof(wc));
wc.style = CS_HREDRAW | CS_VREDRAW |
CS_DBLCLKS;
wc.lpfnWndProc = (WNDPROC)WndProc;

52
LẬP TRÌNH HỆ THỐNG

wc.hInstance = hInstance;
wc.hIcon = LoadIcon(NULL,IDI_INFORMATION); /*(1)*/
wc.hbrBackground= (HBRUSH)(COLOR_WINDOW+1);
wc.lpszClassName = wn;

/*Dang ky lop cua so*/


if (!RegisterClass(&wc)) return 0;

/*Tao cua so thuc su*/


hWnd = CreateWindow( wn, wt, WS_OVERLAPPEDWINDOW
| WS_HSCROLL | WS_VSCROLL,
CW_USEDEFAULT, CW_USEDEFAULT,
CW_USEDEFAULT, CW_USEDEFAULT,
NULL, NULL, hInstance, NULL);

/*Hien thi va cap nhat noi dung cua so*/


ShowWindow(hWnd,1);
UpdateWindow(hWnd);

/* Vong lap thong diep */


while (GetMessage(&msg, NULL, 0, 0)) {
TranslateMessage(&msg);
DispatchMessage(&msg);
}

return msg.wParam;
}
/********* Ham cua so cua chuong trinh ************/
LRESULT CALLBACK WndProc( HWND t1, UINT t2,
WPARAM t3, LPARAM t4) {
HDC d;
HPEN p;
COLORREF mauve = 0;
static double x,y, a=-5, b=5, dx=0.01, tyle = 5;
static int OX = 100, OY = 100, cx,cy, mx,my;
PAINTSTRUCT ps;
switch (t2)
{
case WM_PAINT:
d = BeginPaint(t1,&ps);
cx = ps.rcPaint.right - ps.rcPaint.left;
cy = ps.rcPaint.bottom - ps.rcPaint.top;
/*Ve truc toa do*/
p = CreatePen(0,1,RGB(255,0,0));
SelectObject(d,p);
MoveToEx(d,0,OY,0); LineTo(d,cx,OY);

53
LẬP TRÌNH HỆ THỐNG

MoveToEx(d,OX,0,0); LineTo(d,OX,cy);
/*Ve do thi*/
DeleteObject(p);
p = CreatePen(0,2,RGB(0,0,255));
SelectObject(d,p);
x=a; y=sin(x);
mx = (int)(OX+x*tyle);
my = (int)(OY-y*tyle);
MoveToEx(d,mx,my,0);
while(x<b){
x += dx; y = sin(x);
mx = (int)(OX+x*tyle);
my = (int)(OY-y*tyle);
LineTo(d,mx,my);
}
EndPaint(t1,&ps);
DeleteObject(p);
break;
case WM_LBUTTONDOWN:
OX = LOWORD(t4); OY = HIWORD(t4);
InvalidateRect(t1,0,1);
break;
case WM_KEYDOWN:
if(t3==VK_UP){
tyle *= 1.1;
InvalidateRect(t1,0,1);
}else if(t3==VK_DOWN){
tyle *= 0.9;
InvalidateRect(t1,0,1);
}
break;
case WM_DESTROY:
PostQuitMessage(0);
break;
default:
return DefWindowProc(t1, t2, t3, t4);
}
return 0;
}
Kết quả chạy chương trình lúc đầu,

54
LẬP TRÌNH HỆ THỐNG

và sau khi chọn gốc tọa độ mới (nhấn nút trái chuột) và phóng to đồ thị
(nhấn mũi tên lên),

Ví dụ 3.2.13: Lập trình tạo cửa sổ. Xử lý nhấn phím F1 để ẩn cửa sổ chương
trình, thực hiện chụp ảnh nền màn hình (desktop) hiện tại vào
cửa sổ ảo. Sau đó hiển thị cửa sổ chương trình và đưa ảnh đã
chụp lên cửa sổ này. Xử lý giữ và kéo chuột trái để di chuyển
ảnh đã chụp.
Hướng dẫn: Sử dụng 2 biến (HDC,HBITMAP) để quản lý cửa sổ ảo và tạo ra
chúng ngay sau khi tạo cửa sổ thật (xử lý thông điệp
WM_CREATE). Để chụp ảnh màn hình ta sử dụng lệnh
GetDC(0) cho việc xác định đối tượng đồ họa màn hình
desktop.
Xử lý việc di chuyển ảnh trên cửa sổ được thực hiện như lệnh chụp ảnh
(BitBlt) bằng cách đưa ảnh đã chụp vào các vị trí khác nhau
trên cửa sổ thật (qua biến mx,my). Hai biến này thay đổi khi giữ
chuột vào kéo.
Nội dung chương trình như sau,

55
LẬP TRÌNH HỆ THỐNG

#include<windows.h>
#include<math.h>
#define wt "Ve ham sin & co gian, dich goc" /*tieu de cua so*/
#define wn "W1" /*ten lop cua so*/
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
/**********Ham WinMain cua chuong trinh*************/
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE, LPSTR, int ) {
MSG msg;
WNDCLASS wc;
HWND hWnd;

/*Dinh nghia lop cua so*/


ZeroMemory(&wc,sizeof(wc));
wc.style = CS_HREDRAW | CS_VREDRAW |
CS_DBLCLKS;
wc.lpfnWndProc = (WNDPROC)WndProc;
wc.hInstance = hInstance;
wc.hIcon = LoadIcon(NULL,IDI_INFORMATION);
wc.hCursor =
LoadCursor(NULL,MAKEINTRESOURCE(32649)); /*(1)*/
wc.hbrBackground= (HBRUSH)(COLOR_WINDOW+1);
wc.lpszClassName = wn;

/*Dang ky lop cua so*/


if (!RegisterClass(&wc)) return 0;

/*Tao cua so thuc su*/


hWnd = CreateWindow( wn, wt, WS_OVERLAPPEDWINDOW
| WS_HSCROLL | WS_VSCROLL,
CW_USEDEFAULT, CW_USEDEFAULT,
CW_USEDEFAULT, CW_USEDEFAULT,
NULL, NULL, hInstance, NULL);

/*Hien thi va cap nhat noi dung cua so*/


ShowWindow(hWnd,1);
UpdateWindow(hWnd);

/* Vong lap thong diep */


while (GetMessage(&msg, NULL, 0, 0)) {
TranslateMessage(&msg);
DispatchMessage(&msg);
}

return msg.wParam;
}

56
LẬP TRÌNH HỆ THỐNG

/********* Ham cua so cua chuong trinh ************/


LRESULT CALLBACK WndProc(HWND t1, UINT t2, WPARAM t3, LPARAM
t4) {
HDC d;
static int mx=0,my=0,x1,y1,x,y,tt;
static HDC da;
static HBITMAP ba;
static int sx = GetSystemMetrics(SM_CXSCREEN);
static int sy = GetSystemMetrics(SM_CYSCREEN);
switch (t2)
{
case WM_CREATE:
d = GetDC(0);
da = CreateCompatibleDC(d);
ba = CreateCompatibleBitmap(d,sx,sy);
SelectObject(da,ba);
break;
case WM_KEYDOWN:
if(t3==VK_F1){
ShowWindow(t1,SW_HIDE);
Sleep(1000);
BitBlt(da,0,0,sx,sy,GetDC(0),0,0,SRCCOPY);
ShowWindow(t1,SW_SHOW);
BitBlt(GetDC(t1),mx,my,sx,sy,da,0,0,SRCCOPY);
}
break;
case WM_LBUTTONDOWN:
x1 = LOWORD(t4); y1 = HIWORD(t4);
tt = 1;
break;
case WM_MOUSEMOVE:
if(tt==1){
x = LOWORD(t4); y = HIWORD(t4);
mx += (x-x1); my += (y-y1);
d = GetDC(t1);
Rectangle(d,-1,-1,sx,sy);
BitBlt(GetDC(t1),mx,my,sx,sy,da,0,0,SRCCOPY);
x1=x; y1=y;
}
break;
case WM_LBUTTONUP:
tt=0;

57
LẬP TRÌNH HỆ THỐNG

break;
case WM_DESTROY:
PostQuitMessage(0);
break;
default:
return DefWindowProc(t1, t2, t3, t4);
}
return 0;
}

Trong chương trình có sử dụng lệnh thay đổi biểu tượng con trỏ chuột
(LoadCursor) với hình ảnh biểu tượng lấy từ hệ thống (xem
lệnh (1)).
Kết quả chạy chương trình sau khi nhấn phím F1 (chụp ảnh màn hình),

và sau khi giữ chuột kéo trên cửa sổ để di chuyển ảnh vừa chụp là,

58
LẬP TRÌNH HỆ THỐNG

3.2.5. Lập trình đa luồng


a) Các khái niệm
Phương pháp lập trình đa luồng được khai thác từ tính đa nhiệm trên
Windows, tức là trong một chương trình có thể lập trình thực
hiện nhiều phần công việc đồng thời bằng việc chia sẽ tài
nguyên của máy tính để xử lý các việc đó.
Trước hết, chúng ta xét khái niệm luồng là quá trình thực hiện một đơn vị
chương trình, độc lập tương đối với các quá trình thực hiện các
đơn vị khác trong chương trình đó. Mỗi luồng gắn với một số
tài nguyên nhất định của hệ thống, được cấp phát khi tạo luồng
và thu hồi khi kết thúc luồng.
Thông thường, trong lập trình mỗi luồng gắn với quá trình thực hiện một hàm
nào đó của chương trình, ta gọi hàm này là hàm xử lý luồng.
Mỗi chương trình khi chạy luôn có một luồng tương ứng với thực hiện hàm
chính của chương trình (WinMain), các luồng khác được tạo ra
từ luồng này.
Như vậy, về cơ bản lập trình đa luồng có thể hiểu là lập trình để điều khiển
máy thực hiện nhiều hàm cùng một lúc thay vì mỗi thời điểm
chỉ thực hiện được một hàm trong phương pháp đơn luồng.
Ví dụ minh họa: Giả sử chương trình có 3 hàm tên là A, B và C. Hàm A gồm 3
đoạn chương trình (1), (2) và (3), trong đó có gọi đến B và C,
hàm B gồm 2 đoạn (4) và (5) có gọi đến C, hàm C gồm đoạn
(6). Xét quá trình thực hiện theo 2 phương pháp đơn luồng và

59
LẬP TRÌNH HỆ THỐNG

đa luồng như sau (hình vẽ): thứ nhất, ở trường hợp đơn luồng
khi gọi thực hiện một hàm thì hàm đang thực hiện phải tạm
dừng chờ cho đến khi hàm được gọi chạy xong rồi mới tiếp tục
(tại một thời điểm chỉ chạy 1 hàm). Chẳng hạn trong sơ đồ dưới
chúng ta có thứ tự chạy các đoạn chương trình là:
(1)  (4)  (6)  (5)  (2)  (6)  (3)
Thứ hai, đối với đa luồng chúng ta có thể gọi nhiều hàm chạy đồng thời, tức
việc gọi một hàm không ảnh hưởng đến hàm đang thực chạy.
Khi đó, trong sơ đồ dưới các đoạn chương trình diễn ra trình tự
sau (các đoạn chương trình trong ngoặc { là đồng thời chạy,
phần bị gạch là giả sử đã chạy hết):

3
2 
 2   6
1  5  
4 6 5
 6

Rõ ràng, xuất phát với hàm A máy chạy đoạn (1), hết đoạn này có lệnh gọi
hàm B và theo cơ chế đa luồng nên vẫn tiếp tục chạy đoạn (2)
của A đồng thời thực hiện đoạn (4) của B, cứ như vậy cho đến
khi kết thúc.

B
A
(4)
(1) Call
C
(5)
Call
B C
(2)

Call (6)
C
(3)

b) Phương pháp lập trình đa luồng


Trên Windows, để lập trình đa luồng chúng ta thực hiện 2 bước chính sau:
Bước 1) Lập trình hàm xử lý luồng (thực hiện công việc);
Bước 2) Tạo luồng mới để chạy hàm trên.

60
LẬP TRÌNH HỆ THỐNG

Trong đó ở bước 1, hàm đa luồng là một hàm theo mẫu quy định của API có
dạng:
DWORD WINAPI tên_hàm( LPVOID ts ) {
/* nội dung của hàm */
}
Hàm này trả về giá trị số nguyên kiểu DWORD, vì vậy trong hàm phải có
lệnh “return”. Tham số của hàm là một con trỏ không định kiểu
(LPVOID) để có thể nhận địa chỉ của bất kỳ ô nhớ chứa dữ liệu
nào mà ta truyền vào. Với một tham số này, nhưng chúng ta
muốn truyền nhiều dữ liệu thì phải đóng gói các dữ liệu đó lại
bằng struct hoặc class rồi truyền địa chỉ của gói vào là được.
Khi đó bên trong hàm cần phải ép kiểu về con trỏ của dữ liệu
tương ứng để xử lý.
Lệnh tạo luồng ở bước 2 thực hiện theo cú pháp sau:
HANDLE WINAPI CreateThread( attr, ssize, tên_hàm, tham_số,
trạng_thái, số_hiệu );
Trong đó gồm các tham số:
 attr: quy định các thuộc tính thiết lập mức độ bảo vệ của luồng (thường đặt
=0);
 ssize: quy định độ lớn của stack dành cho chạy luồng (đặt =0 để máy tự
động thiết lập);
 tên_hàm: đã được lập trình ở bước 1;
 tham_số: là địa chỉ của dữ liệu cần truyền cho hàm, sẽ đưa vào tham số
của hàm ở trên;
 trạng_thái: cho biết luồng được tạo sẽ chạy ngay hoặc chưa (=0);
 số_hiệu: giá trị số nguyên phân biệt khi có nhiều luồng.
Ví dụ 3.2.14: Lập trình hàm đa luồng vẽ quả bóng chuyển động trên cửa sổ
theo cơ chế phản xạ (cửa sổ cần vẽ, tọa độ x,y xuất phát, màu,
kích thước, hướng chạy, tốc độ chạy được truyền qua tham số
hàm). Quả bóng này dừng sau 15 giây chuyển động. Chương
trình tạo một cửa sổ, xử lý nhấn nút trái chuột để tạo luồng chạy
hàm trên với tọa độ chuột là vị trí bóng xuất phát, các tham số
còn lại được lấy ngẫu nhiên.

61
LẬP TRÌNH HỆ THỐNG

Hướng dẫn: Chúng ta phải đóng gói dữ liệu các tham số vẽ quả bóng chuyển
động vào một cấu trúc ‘BONG’ (struct) có dạng sau (đồng thời
khai báo biến có tên là ‘bong’),
struct BONG{
HWND cs; //số hiệu cửa sổ để vẽ
int x,y,r,dx,dy,tocdo; //tọa độ, bán kính, hướng & phương chạy
COLORREF mau; //màu bóng
} bong;
Khi đó hàm xử lý đa luồng nhận địa chỉ của biến ‘bong’ và phải ép kiểu để
chuyển về dạng cấu trúc ở trên như sau,
BONG *p = (BONG*)tham_số_hàm_đa_luồng;
Để lấy số ngẫu nhiên ta vẫn dùng lệnh ‘rand’ của C/C ++, để kiểm tra thời gian
chạy của bóng ta dùng lệnh ‘time’ đo khoảng thời gian trong
quá trình lặp. Nếu vượt 15 giây thì dừng bóng (thoát khỏi lệnh
lặp điều khiển bóng).
Nội dung chương trình đầy đủ là,
#include<windows.h>
#include<time.h>
#define wt "Ve qua bong chuyen dong: da luong" /*tieu de cua so*/
#define wn "W1" /*ten lop cua so*/

struct BONG{
HWND cs;
int x,y,r,dx,dy,tocdo;
COLORREF mau;
} bong;

DWORD WINAPI HamDaLuong( LPVOID );


LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
/**********Ham WinMain cua chuong trinh*************/
int WINAPI WinMain(HINSTANCE t1, HINSTANCE, LPSTR, int ) {
MSG msg;
WNDCLASS wc;
HWND hWnd;
/*Dinh nghia lop cua so*/
ZeroMemory(&wc,sizeof(wc));
wc.style = CS_HREDRAW | CS_VREDRAW | CS_DBLCLKS;
wc.lpfnWndProc = (WNDPROC)WndProc;
wc.hInstance = t1;

62
LẬP TRÌNH HỆ THỐNG

wc.hIcon = LoadIcon(NULL,IDI_QUESTION);
wc.hbrBackground = (HBRUSH)(COLOR_WINDOW+1);
wc.lpszClassName = wn;
/*Dang ky lop cua so*/
if (!RegisterClass(&wc)) return 0;
/*Tao cua so thuc su*/
hWnd = CreateWindow(wn, wt, WS_SYSMENU,
100,100,300,200, NULL, NULL, t1, NULL);
/*Hien thi va cap nhat noi dung cua so*/
ShowWindow(hWnd,1);
UpdateWindow(hWnd);
/* Vong lap thong diep */
while (GetMessage(&msg, NULL, 0, 0)) {
TranslateMessage(&msg);
DispatchMessage(&msg);
}
return msg.wParam;
}
/********* Ham cua so cua chuong trinh ************/
LRESULT CALLBACK WndProc(HWND t1, UINT t2, WPARAM t3,
LPARAM t4) {
switch (t2)
{
case WM_LBUTTONDOWN:
bong.cs = t1;
bong.x = LOWORD(t4); bong.y = HIWORD(t4);
bong.dx = 3-rand()%6; bong.dy = 3-rand()%6;
bong.mau = RGB(rand(),rand(),rand());
bong.r = 10+rand()%40;
bong.tocdo = 10+rand()%50;
CreateThread(0,0,HamDaLuong,&bong,0,0);
break;
case WM_DESTROY:
PostQuitMessage(0);
break;
default:
return DefWindowProc(t1, t2, t3, t4);
}
return 0;
}

63
LẬP TRÌNH HỆ THỐNG

DWORD WINAPI HamDaLuong( LPVOID t1 ){


BONG *p = (BONG*)t1;
HDC d = GetDC(p->cs);
int x = p->x, y = p->y, r = p->r;
int dx = p->dx, dy = p->dy, tocdo = p->tocdo;
COLORREF mau = p->mau;
HPEN b = CreatePen(0,2,mau);
SelectObject(d,b);
SetROP2(d,R2_NOTXORPEN);
long tm1 = time(0);
while(1){
Ellipse(d,x-r,y-r,x+r,y+r);
Sleep(tocdo);
Ellipse(d,x-r,y-r,x+r,y+r);
if(time(0)-tm1>15000) break;
x += dx; y += dy;
RECT rt;
GetClientRect(p->cs, &rt);
if(x<r || x>rt.right-rt.left-r) dx=-dx;
if(y<r || y>rt.bottom-rt.top-r) dy=-dy;
}
DeleteObject(b);
DeleteDC(d);
return 0;
}
Trong thông điệp WM_LBUTTONDOWN chúng ta thiết lập các tham số cho
việc vẽ quá bóng (thông qua biến ‘bong’), sau đó gọi hàm đa
luồng bằng lệnh ‘CreateThread’ để chạy nó trên một luồng mới
độc lập với các luồng đang chạy nên kết quả trên cửa sổ sẽ có
nhiều quả bóng xuất hiện và chuyển động mặc dù trong chương
trình chỉ điều khiển một quả.
Kết quả màn hình sau khi bấm chuột trái ở các vị trí khác nhau trên cửa sổ sẽ
có,

64
LẬP TRÌNH HỆ THỐNG

c) Vấn đề đồng bộ hóa giữa các luồng


Khi các luồng cùng chạy và chúng cùng truy cập xử lý đến một tài nguyên
như dữ liệu chẳng hạn, khi đó sự đồng bộ là rất cần thiết. Ở đây,
sự đồng bộ được hiểu theo nghĩa khi một luồng T1 đang xử lý
một dữ liệu X và giá trị của nó phải nhất quán trong quá trình
xử lý thì mọi thay đổi trên dữ liệu X này bởi các luồng khác sẽ
làm mất tính nhất quán đối với luồng T1.

Giao dịch T1 X = 1000 Giao dịch T2

- Thời điểm 1: thấy X = 1000 - Thời điểm 1: ...


- Thời điểm 2: ... - Thời điểm 2: thấy X = 1000
- Thời điểm 3: rút 300 - Thời điểm 3: ...
X = X - 300
- Thời điểm 4: chạy giao dịch - Thời điểm 4: gửi 200
X = X + 200
- Thời điểm 5: thấy X=700 - Thời điểm 5: chạy giao dịch

- Thời điểm 6: thấy X=900?


X = 900

Chẳng hạn, ví dụ với một tài khoản X trên ngân hàng nào đó hiện số dư là
1000. Cùng lúc ta thực hiện 2 giao dịch, giao dịch T1 là rút 300
và giao dịch T2 là gửi 200. Nếu không đồng bộ quá trình xử lý
các giao dịch có thể dẫn đến việc giao dịch T2 thực hiện xong
và nhận thấy tài khoản chỉ còn 900, người thực hiện giao dịch

65
LẬP TRÌNH HỆ THỐNG

T2 sẽ phàn nàn là số dư ban đầu 1000, sau khi gửi thêm 200 mà
bây giờ chỉ còn 900 (hình vẽ trên)?
Minh họa của ví dụ trên bằng cơ chế trạng thái như sau:

Giao dịch T1 X = 1000 Giao dịch T2

(tt. mở)
- Bắt đầu: chuyển tt. đóng
(tt. đóng)
- Thời điểm 1: thấy X = 1000 - Thời điểm 1: chờ...
- Thời điểm 2: thực hiện rút 300 - Thời điểm 2: chờ...
X = X - 300
- Thời điểm 3: chạy giao dịch
(tt. mở)
- Thời điểm 4: thấy X=700 (tt. đóng)
- Thời điểm 5: kết thúc giao - Thời điểm 4: chờ...
dịch - Thời điểm 5: chuyển tt. đóng
- Thời điểm 6: thấy X = 700
X = 900
- Thời điểm 7: thực hiện gửi
200
X = X + 200
- Thời điểm 8: chạy giao dịch
- Thời điểm 9: kiểm tra X=900
- Thời điểm 10: kết thúc

Một phương pháp khác là sử dụng các kỹ thuật trên Windows (thực chất cũng
dựa trên nguyên tắc cơ chế khóa với hai trạng thái đóng/mở).
Sử dụng một trong 2 lệnh sau để kiểm tra và chờ trạng thái của
đối tượng  cần xử lý:
WaitForSingleObject( , timeout );
WaitForMultipleObject( count, , waitAll, timeout );
Trong đó:
 Ký hiệu  là đối tượng hoặc các đối tượng cần chờ cho đến khi có tín
hiệu mở gồm:
* Change notification: thông báo thay đổi;
* Console input: dòng nhập dữ liệu;
* Event: sự kiện;

66
LẬP TRÌNH HỆ THỐNG

* Mutex: truy xuất xử lý tệp lưu trữ;


* Semaphore: hiệu lệnh;...
Chúng ta phải tạo ra các đối tượng này bằng các lệnh tương ứng (xem chi tiết
ở phần phụ lục hoặc tài liệu tham khảo trong [4]).
 timeout: là thời gian chờ tối đa, có thể đặt INFINITE để không giới hạn;
 count: số đối tượng cần chờ nếu có nhiều, khi đó ký hiệu  phải là một
mảng các đối tượng;
 waitAll: cho biết có chờ tất cả các đối tượng hay không (TRUE/FALSE).
Hình vẽ sau minh họa phương pháp sử dụng Mutex để đồng bộ hóa quá trình
truy xuất xử lý tài nguyên giữa các luồng. Phần hình phía trên
là chưa đồng bộ thì các luồng xử lý tài nguyên sẽ có giai đoạn
chồng lên nhau (biểu thị màu vàng), nhưng sau khi sử dụng
mutex chúng ta thấy đoạn bị xử lý chồng sẽ không còn mà các
luồng phải chờ nhau.

3.2.6. Lập trình thư viện DLL và xử lý HOOK


a) Phương pháp lập trình thư viện động (DLL)
Khái niệm thư viện lập trình (library) là một tập các đại lượng, các hàm, các
biến,... cung cấp cho chương trình ứng dụng để thực hiện các
công việc xử lý. Thông thường thư viện có hai dạng, thư viện
tĩnh và thư viện động. Thư viện tĩnh (static library) được lập
trình, dịch và liên kết với chương trình sử dụng nó. Dẫn đến các
chương trình rất lớn khi lưu trữ, chiếm tài nguyên nhiều khi
chạy,...

67
LẬP TRÌNH HỆ THỐNG

Thư viện động (dynamic linked library - DLL) cho phép một chương trình sử
dụng được lưu trữ độc lập với nó, chỉ khi chạy chương trình cần
đến mới nạp thư viện vào máy. Nhằm mục đích giảm tải cho hệ
thống khi chạy các ứng dụng và đồng bộ trong quá trình khai
thác, chia sẽ tài nguyên.
Minh họa thư viện động:

Theo phương pháp thư viện động, nhiều chương trình có thể sử dụng chung
một thư viện trên cùng một phân vùng nhớ được chia sẽ, không
nhất thiết mỗi chương trình cần một phân vùng nhớ cho thư
viện riêng biệt.
Khi liên kết một chương trình với một thư viện, chúng ta có hai phương pháp
đó là implicite (dạng ẩn) hoặc explicite (dạng rõ). Trường hợp
dạng rõ cho phép nạp thư viện vào bộ nhớ khi cần và giải
phóng nó sau khi sử dụng xong một cách tùy ý, còn ở dạng ẩn
thì máy sẽ tự động nạp thư viện cùng với chương trình chạy
(hình vẽ minh họa sau).

Giả sử chương trình sử dụng 3 thư viện DLL-1, DLL-2 và DLL-3, ở trường
hợp ‘RunTime’ là liên kết theo dạng rõ do đó tại các thời điểm
cần chúng ta nạp thư viện và giải phóng sau khi dùng xong nên

68
LẬP TRÌNH HỆ THỐNG

bộ nhớ được tiết kiệm tối đa. Trong trường hợp này nếu có một
thư viện nào đó bị hỏng thì chức năng tương ứng sẽ không thực
hiện được, còn các chức năng khác của chương trình vẫn có thể
chạy bình thường.
Đối với trường hợp ‘LoadTime’, tức liên kết ở dạng ẩn, tất cả các thư viện
được nạp vào máy ngay khi chương trình chạy và thiết lập một
bảng địa chỉ các thành phần của thư viện để chương trình có thể
sử dụng nên chúng luôn sẵn sàng chờ lệnh truy cập xử lý. Tuy
nhiên, nếu có một thư viện bị lỗi thì toàn bộ chương trình sẽ
không thể chạy được, cho dù người dùng có thể không cần đến
chức năng của thư viện lỗi này.
Một yếu tố quan trọng của lập trình thư viện động đó là giao diện của thư
viện. Giao diện thư viện (interface) được hiểu như là phần khai
báo các thành phần có bên trong thư viện để các chương trình
ứng dụng có thể khai thác. Như vậy, nếu một thành phần được
xây dựng bên trong thư viện nhưng không khai báo ở giao diện
thì nó không thể được dùng trong chương trình ứng dụng (hình
vẽ sau).

b) Cách lập trình tạo và sử dụng thư viện động


Để lập trình tạo một thư viện động có hai bước chính như sau:
Bước 1) Xây dựng các hàm, biến... là nội dung của thư viện
Bước 2) Định nghĩa giao diện của thư viện

69
LẬP TRÌNH HỆ THỐNG

Trong đó, phần lập trình nội dung thư viện ở bước 1 được thực hiện như lập
trình cho các chương trình ứng dụng thông thường. Còn định
nghĩa giao diện cho thư viện sẽ có hai phương pháp:
 Thứ nhất, định nghĩa trực tiếp trong các nội dung đã lập trình của thư viện
bằng quy định sau,
__declspec(dllexport)
 Thứ hai, định nghĩa thông qua tệp *.DEF trên môi trường VisualC ++ 6.0
theo cú pháp sau,
LIBRARY “tên_thư_viện”
EXPORT
...liệt kê tên thành phần theo hàng...
Ngoài ra, mỗi thư viện, cũng giống như một chương trình, có một hàm chính
và máy sẽ tự động gọi hàm này mỗi khi nạp thư viện hoặc giải
phóng thư viện. Hàm chính của thư viện được gọi là lối vào
(entry-point) của thư viện, nó có dạng như sau:
BOOL WINAPI DllMain(
HINSTANCE hinstDLL,
DWORD fdwReason,
LPVOID lpvReserved
);
Trong đó, tên hàm quy định là “DllMain”, tham số kiểu HINSTANCE chứa
số hiệu định danh của thư viện khi nạp vào máy (là địa chỉ cơ
sở của chính thư viện đó), tham số kiểu DWORD chứa lý do
hàm được gọi bao gồm:
DLL_PROCESS_ATTACH Chương trình nạp thư viện vào máy
DLL_PROCESS_DETACH Chương trình giải phóng thư viện
DLL_THREAD_ATTACH Một luồng được tạo và gắn cùng với thư
viện
DLL_THREAD_DETACH Kết thúc luồng và thư viện gắn kèm được
giải phóng
Tham số thứ 3 dành riêng (không sử dụng đến).
Chúng ta có thể lập trình hàm này hoặc không tùy vào yêu cầu thiết kế. Thông
thường, hàm này dùng để khởi tạo các giá trị mặc định cho việc
xử lý trong thư viện.

70
LẬP TRÌNH HỆ THỐNG

Ví dụ 3.2.15: Lập trình thư viện động chứa hàm để khoảng cách giữa hai điểm
trên mặt phẳng, hàm tính diện tích của tam giác khi biết 6 tọa
độ x,y của 3 đỉnh.
Hướng dẫn: Trong ví dụ này chúng ta xây dựng thư viện chứa tọa độ 3 đỉnh
của tam giác là các biến được khai báo toàn cục (sử dụng chung
cho cả chương trình và thư viện). Hàm tính khoảng cách giữa
hai điểm chỉ sử dụng nội bộ của thư viện (không xuất ra ngoài),
hàm tính diện tích được xuất ra để sử dụng trong chương trình
ứng dụng.
#include <math.h>
#include <windows.h>
__declspec(dllexport) double _xx1=0, _yy1=0, _xx2=5, _yy2=9;
__declspec(dllexport) double _xx3=9, _yy3=3;
double khoang_cach( double x1, double y1, double x2, double y2)
{
return sqrt(pow(x2-x1,2)+pow(y2-y1,2));
}
__declspec(dllexport) double dien_tich()
{
double a,b,c,p;
a = khoang_cach(_xx1,_yy1,_xx2,_yy2);
b = khoang_cach(_xx2,_yy2,_xx3,_yy3);
c = khoang_cach(_xx3,_yy3,_xx1,_yy1);
p = (a+b+c)/2;
return sqrt(p*(p-a)*(p-b)*(p-c));
}
Đối với một chương trình ứng dụng có sử dụng thư viện liên kết động, cũng
có hai dạng liên kết (ẩn và tường minh). Ở dạng ẩn (implicite),
chúng ta phải khai báo các thành phần của thư viện muốn sử
dụng ở trong chương trình theo cách sau:
__declspec( dllimport ) khai_báo_thành_phần_muốn_dùng;
Và ở cách này bắt buộc phải có tệp .LIB để liên kết đến thư viện DLL, do đó
chúng ta phải có 2 tệp .LIB và .DLL xác định trong danh mục
thư viện liên kết của chương trình ứng dụng.
Còn ở dạng rõ (explicite) thì việc sử dụng các thành phần phải thông qua địa
chỉ của chúng. Cụ thể, dựa trên địa chỉ của thư viện được nạp
trên bộ nhớ chúng ta xác định địa chỉ các thành phần bên trong

71
LẬP TRÌNH HỆ THỐNG

thư viện đó. Các địa chỉ của các thành phần sẽ được ép kiểu về
dạng con trỏ tương ứng với kiểu của chúng trong thư viện để sử
dụng như bình thường (thông qua con trỏ).
Các bước thực hiện liên kết sử dụng thư viện ở dạng rõ gồm:
/* Bước 1: Nạp thư viện vào máy */
HMODULE  = LoadLibrary( “tên_thư_viện” );
/* Bước 2: Lấy địa chỉ các thành phần muốn dùng */
FARPROC  = GetProcAddress(  , “tên_thành_phần” );
/* Bước 3: Ép kiểu về dạng sử dụng được */
...
/* Bước 4: Sử dụng con trỏ để xử lý */
...
/* Bước 5: Giải phóng thư viện */
FreeLibrary(  );
Trong đó, kiểu HMODULE là định danh của thư viện được nạp vào máy
(chính là địa chỉ bộ nhớ chứa thư viện đó), FARPROC là con
trỏ dạng VOID (không định kiểu) chứa địa chỉ của bất kỳ thành
phần trong thư viện cần lấy ra.
Ở bước 3, việc ép kiểu thực hiện theo 2 dạng: biến nhớ và hàm. Đối với biến
nhớ đơn giản ta thực hiện như sau:
Kiểu_dữ_liệu *con_trỏ = (Kiểu_dữ_liệu *);
Còn đối với hàm ta phải khai báo dạng con trỏ hàm như sau:
typdedef Kiểu_hàm (*Kiểu_con_trỏ_hàm)(các_kiểu_tham_số);
Kiểu_con_trỏ_hàm con_trỏ_hàm = (Kiểu_con_trỏ_hàm);
Bước 4 sử dụng con trỏ để truy nhập, khai thác các thành phần thực hiện như
con trỏ thông thường. Con trỏ hàm bây giờ đóng vai trò như
một tên hàm và chúng ta viết lệnh gọi hàm như thông thường.
Ví dụ 3.2.16: Lập trình sử dụng thư viện ở ví dụ trên để hiện ra hộp thông báo
diện tích tam giác lúc khởi tạo (theo các giá trị gán ban đầu của
các biến trong thư viện) và diện tích tam giác sau khi thay đổi
các tọa độ.
Hướng dẫn: Trong chương trình chúng ta sử dụng phương pháp liên kết ở
dạng rõ theo 4 bước như hướng dẫn ở trên.
#include<windows.h>
#include<stdio.h>

72
LẬP TRÌNH HỆ THỐNG

int WINAPI WinMain(HINSTANCE,HINSTANCE,LPSTR,int)


{
HMODULE a = LoadLibrary("Vidu3_2_15.dll"); /*(1)*/
if (a==NULL){
MessageBox(0,"Khong tim thay DLL",0,0);
return 0;
}
double *px1 = (double*)GetProcAddress(a,"_xx1");
double *py1 = (double*)GetProcAddress(a,"_yy1");
double *px2 = (double*)GetProcAddress(a,"_xx2");
double *py2 = (double*)GetProcAddress(a,"_yy2");
double *px3 = (double*)GetProcAddress(a,"_xx3");
double *py3 = (double*)GetProcAddress(a,"_yy3");
typedef double (*CTH)();
CTH ph = (CTH)GetProcAddress(a,"_Z9dien_tichv"); /*(2)*/

if(!(px1 && px2 && px3 && py1 &&py2 && py3 && ph)){
MessageBox(0,"Khong lay duoc dia chi cac phan",0,0);
}else{
char s[100];
sprintf(s,"Dien tich ban dau: %.6f", ph()); /*(3)*/
MessageBox(0,s,"Using DLL",0);
*px1 = *py1 = 0;
*px2 = 0; *py2 = 4;
*px3 = 3; *py3 = 0;
sprintf(s,"Dien tich luc sau: %.6f", ph()); /*(4)*/
MessageBox(0,s,"Using DLL",0);
}

FreeLibrary(a);
return 1;
}
Lệnh (1) nạp thư viện “LoadLibrary” có thể viết đầy đủ đường dẫn đến thư
viện nếu cần. Ở trên chúng ta kiểm tra kết quả lệnh này (biến
“a”) nếu là NULL thì nạp thư viện không thành công. Tên hàm
được xuất ra ngoài (trên giao diện của thư viện) được đổi lại
thành “Z9dien_tichv” thay vì tên gốc trong thư viện là
“dien_tich” (lệnh (2)). Sử dụng 6 con trỏ kiểu “double” để trỏ
đến 6 biến trong thư viện và xử lý dữ liệu trên 6 biến đó, một
con trỏ hàm tên là “ph” được khai báo bởi kiểu CTH theo mẫu

73
LẬP TRÌNH HỆ THỐNG

hàm đã lập trình trong thư viện. Gọi hàm này ở các lệnh (3) và
(4) như gọi hàm bình thường.
Kết quả của chương trình khi chạy sẽ có 2 hộp thông báo sau:

c) Lập trình xử lý HOOK


Xử lý HOOK là một cơ chế đặc biệt trong lập trình trên Windows. Tương tự
phương pháp lập trình xử lý ngắt trên MS-DOS, lập trình xử lý
HOOK cho phép chương trình có thể chặn các sự kiện trên máy
tính hoặc các hàm API và thực hiện một số thao tác nhất định
nào đó rồi mới tiếp tục chạy sự kiện/hàm API như bình thường
hoặc bỏ qua. Chẳng hạn, với sự kiện nhấn phím sẽ phát sinh
thông điệp và gửi đến ứng dụng, khi đó chúng ta có thể chặn sự
kiện này để ghi lại các phím được nhấn trên máy tính kiểu như
các chương trình keylog.
Tương tự như chặn sự kiện, khi chặn các hàm API của bản thân hệ thống (gọi
là Hook API) chúng ta cũng phải chuyển hướng thực hiện API
trên Windows sang thực hiện hàm của chúng ta, sau đó mới
quay về chạy API hoặc bỏ qua (minh họa hình vẽ sau).

74
LẬP TRÌNH HỆ THỐNG

Windows

Users
Applications
Lõi
(4) (kernel) (1)
(1)
API

(3) (2)
(2) (3)

Hooks

Trên hình vẽ, ta có đối với HOOK sự kiện, người dùng tác động phát sinh sự
kiện được Windows quản lý (1). Chúng ta viết một mô-đun
chương trình “hook” để chạy (2) trước khi sự kiện đó chuyển
đến các ứng dụng xử lý (4). Tương tự với hook API, một
chương trình ứng dụng khi gọi hàm API (1) thay vì chạy trực
tiếp hàm đó chúng ta điều hướng để chạy mô-đun chương trình
“hook” (2) rồi quay về thực hiện tiếp hàm API được gọi (3).
Rõ ràng, tất cả mọi người lập trình đều có thể tạo các hook chặn sự kiện trên
máy hoặc các hàm API. Khi đó các mô-đun chương trình
“hook” đã được đăng ký trong hệ thống tạo thành một chuỗi các
hook (gọi là “hook chain” - danh sách các mô-đun xử lý hook).
Vậy, một sự kiện phát sinh thì lần lượt các mô-đun chương trình
“hook” trong danh sách sẽ được thực hiện trước khi sự kiện đó
chuyển đến các ứng dụng xử lý.
Về phía lập trình, chúng ta luôn luôn phải lập trình tạo mô-đun chứa phần
chương trình “hook”. Mô-đun chương trình này được lập trình
thành một thư viện động (DLL) với một hàm xử lý hook trong
đó. Trong một số tài liệu có trình bày khái niệm “inject dll” hay
gọi là tiêm DLL vào các chương trình ứng dụng. Tức là, chúng

75
LẬP TRÌNH HỆ THỐNG

ta tạo một DLL chứa các hàm xử lý, sau đó đính DLL này vào
bất kỳ chương trình nào để thực hiện cơ chế hook như trên.
Trong khuôn khổ tài liệu này, chúng ta sẽ làm quen với lập trình xử lý hook
các sự kiện đơn giản như thao tác chuột, thao tác trên bàm
phím. Hook sự kiện có 2 cấp độ: “local hook” là chỉ chặn các sự
kiện diễn ra bên trong chương trình ứng dụng của chúng ta;
“global hook” là chặn tất cả các sự kiện diễn trên hệ thống. Các
bước thực hiện lập trình được tóm tắt như sau:
Bước 1: Lập trình tạo thư viện DLL chứa hàm xử lý hook;
Bước 2: Lập chương trình sử dụng thư viện trên để đăng
ký hook vào hệ thống.
Trong DLL, ngoài việc chứa hàm xử lý hook chúng ta phải có một biến kiểu
HHOOK chứa định danh của việc gọi hook tiếp theo trong danh
sách hook. Biến này cần được thiết lập trong bước 2 với định
danh của hook tiếp theo sẽ lấy được khi đăng ký hook (hình vẽ
minh họa sau).

Thư viện DLL Chương trình đăng ký HOOK

Biến chứa Con trỏ chứa


HHOOK địa chỉ biến
tiếp theo trong DLL

Con trỏ chứa địa


chỉ hàm xử lý
HOOK
Hàm xử lý HOOK
-.-.-.-.-.-.
-.-.-.-.-.-. Đăng ký xử
-.-.-.-.-.-. lý HOOK
Gọi HOOK tiếp theo

Trong phần chương trình cần hai con trỏ: một con trỏ chứa địa
chỉ của biến HHOOK tiếp theo, một con trỏ khác chứa địa chỉ
hàm xử lý hook. Kết quả sau khi đăng ký hook sẽ gán vào biến
ở thư viện qua con trỏ thứ nhất, trong hàm xử lý hook để gọi
đến hàm hook tiếp theo ta sử dụng biến HHOOK này.

76
LẬP TRÌNH HỆ THỐNG

Cụ thể, trong bước 1 biến và hàm được lập trình theo mẫu sau:
HHOOK tên_biến;
LRESULT CALLBACK tên_hàm( int, WPARAM, LPARAM );
trong đó:
 Tham số đầu (int) cho biết sự kiện nhấn phím hoặc nhấn chuột ở dạng
HC_ACTION (do người dùng tác động) hay HC_NOREMOVE (do sử
dụng lệnh PeekMessage với tham số PM_NOREMOVE). Tham số này
nếu nhỏ hơn 0 thì cần phải gọi ngay hàm hook tiếp theo mà không xử lý
và trả về kết quả của đúng hàm tiếp theo đó;
 Tham số thứ hai (WPARAM) và thứ ba (LPARAM) chứa các thông tin
liên quan đến sự kiện, gồm:
Sự kiện phím Ý nghĩa
WPARAM Chứa mã phím được nhấn
LPARAM Chứa trạng thái một số phím, các tham số mở rộng...
Sự kiện chuột
WPARAM Chứa giá trị của thông điệp phát sinh ứng với sự kiện
LPARAM Chứa thông tin liên quan đến chuột như tọa độ, cửa sổ
tác động... trong cấu trúc
MOUSEHOOKSTRUCT dưới dạng địa
chỉ (con trỏ) gồm:
+ POINT pt: tọa độ chuột
+ HWND hwnd: cửa sổ tác động,...
Trong hàm xử lý hook này phải thực hiện lệnh gọi hàm hook tiếp theo trong
danh sách hook, cú pháp lệnh như sau:
CallNextHookEx(HHOOK nextHook, int, WPARAM, LPARAM);
tham số đầu (HHOOK) là số hiệu hook tiếp theo chứa trong biến HHOOK, 3
tham số sau chính là các tham số của hàm xử lý hook.
Các bước lập trình trong chương trình đăng ký hook cần thực hiện như
chương trình sử dụng thư viện ở dạng liên kết rõ. Nạp thư viện
vào máy và xác định địa chỉ của biến, hàm cần dùng và thực
hiện lệnh đăng ký hook sau:
SetWindowHookEx(int id,HOOKPROC hf,HINSTANCE hm,DWORD
ti);
trong đó:

77
LẬP TRÌNH HỆ THỐNG

 id (int): quy định kiểu sự kiện cần đăng ký hook gồm WH_KEYBOARD
(bàn phím), WH_MOUSE (chuột),...
 hf (HOOKPROC): quy định hàm xử lý hook để chạy khi có sự kiện phát
sinh, hàm này đã được lập trình ở thư viện DLL và lấy địa chỉ hàm cho vào
tham số này (phải ép kiểu);
 hm (HINSTANCE): quy định số hiệu định của thư viện DLL (xác định bởi
lệnh LoadLibrary);
 ti (DWORD): quy định luồng gắn với hàm xử lý hook (thường đặt =0).
Chú ý: Kết quả của lệnh này cần được gán vào biến HHOOK ở trong thư viện
thông qua con trỏ chứa địa chỉ.
Trong trường hợp muốn hủy bỏ đăng ký hook ta sử dụng lệnh sau:
UnhookWindowsHookEx( HHOOK hhk);
Ví dụ 3.2.17: Lập trình xây dựng thư viện DLL chứa hàm xử lý sự kiện phím
và lưu vào một tệp text các mã phím được ấn. Chương trình
chính thực hiện đăng ký hàm hook cho sự kiện gõ phím.
Nội dung chương trình thư viện DLL chứa hàm xử lý hook là,
#include<windows.h>
#include<stdio.h>
__declspec(dllexport) HHOOK _nextHook;

__declspec(dllexport) LRESULT CALLBACK KeyHook(int t1,


WPARAM t2, LPARAM t3)
{
if(t1>=0){
FILE *f = fopen("keylog.txt","at");
fprintf(f,"%d,",t2);
fclose(f);
}
return CallNextHookEx(_nextHook, t1,t2,t3);
}
Nội dung chương trình đăng ký hook sử dụng thư viện trên là,
#include<windows.h>
#include<stdio.h>
int WINAPI WinMain(HINSTANCE,HINSTANCE,LPSTR,int)
{
HMODULE a = LoadLibrary("Vidu3_2_17.dll");

78
LẬP TRÌNH HỆ THỐNG

if(a==NULL) MessageBox(0,"Khong co DLL",0,0);


else{
HHOOK *pnextHook =
(HHOOK*)GetProcAddress(a,"_nextHook");
typedef LRESULT (CALLBACK *CTH)(int,WPARAM,LPARAM);
/*(1)*/
CTH pKeyHook = (CTH)GetProcAddress(a,"_Z7KeyHookijl");
/*(2)*/
if(!(pnextHook && pKeyHook))
MessageBox(0,"Khong co KeyHook or _nextHook",0,0);
else{
if(*pnextHook =
SetWindowsHookEx(WH_KEYBOARD,pKeyHook,a,0)){
MessageBox(0,"Success",0,0);
for(int i=0;i<120;i++) Sleep(1000);
}else MessageBox(0,"Fail to set a hook",0,0);
}
UnhookWindowsHookEx(*pnextHook);
FreeLibrary(a);
}
return 0;
}
Hàm xử lý hook trong thư viện sẽ lưu mã phím được nhấn vào một tệp
“keylog.txt” dưới dạng text. Tệp thư viện đặt tên là
“Vidu3_2_17.dll”, thành phần hàm được định nghĩa trên giao
diện với tên là “_Z7KeyHookijl” do bộ dịch tự động định nghĩa
và đặt tên (xem tệp .DEF sau khi dịch).
Chương trình chính nạp thư viện vào máy và xác định địa chỉ các thành phần,
chú ý đến việc ép kiểu con trỏ hàm xử lý hook (xem lệnh (1) &
(2)). Chương trình này sẽ đăng ký và chờ trong 2 phút (xem
lệnh for & Sleep để chờ), sau đó hủy đăng ký kết thúc.
Kết quả chạy chương là tệp “keylog.txt” sau khi gõ một số phím là (các mã
phím được lưu lại trong tệp),

79
LẬP TRÌNH HỆ THỐNG

80

You might also like