Professional Documents
Culture Documents
LTHT Windows
LTHT Windows
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
App2 App4
App1
App3
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
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ý
5
LẬP TRÌNH HỆ THỐNG
6
LẬP TRÌNH HỆ THỐNG
7
LẬP TRÌNH HỆ THỐNG
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
9
LẬP TRÌNH HỆ THỐNG
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
14
LẬP TRÌNH HỆ THỐNG
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
19
LẬP TRÌNH HỆ THỐNG
dx>0
dy>0
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
thông điệp)
22
LẬP TRÌNH HỆ THỐNG
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);
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à:
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
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 */
28
LẬP TRÌNH HỆ THỐNG
29
LẬP TRÌNH HỆ THỐNG
30
LẬP TRÌNH HỆ THỐNG
}
return msg.wParam;
}
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:
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
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ẽ.
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*/
37
LẬP TRÌNH HỆ THỐNG
return msg.wParam;
}
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
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
41
LẬP TRÌNH HỆ THỐNG
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>
42
LẬP TRÌNH HỆ THỐNG
return msg.wParam;
}
43
LẬP TRÌNH HỆ THỐNG
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:
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
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
#define wt "Ve ham sin & co gian, dich goc" /*tieu de cua so*/
#define wn "W1" /*ten lop cua so*/
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;
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;
return msg.wParam;
}
56
LẬP TRÌNH HỆ THỐNG
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
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)
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;
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
64
LẬP TRÌNH HỆ THỐNG
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:
(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
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).
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
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:
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).
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;
78
LẬP TRÌNH HỆ THỐNG
79
LẬP TRÌNH HỆ THỐNG
80