Đọc màn hình với VC 6

Chỉ cần một số bước khá đơn giản, bạn có thể tự tạo một chương trình đọc cho bạn nghe những dòng chữ trên màn hình, chẳng hạn như tin tức của trang tin điện tử, sách điện tử ở dạng .chm,… Điều này rất ý nghĩa, đặc biệt đối với những người khiếm thị. Bài viết này giới thiệu cách thực hiện một chương trình như thế – tạm gọi là ScreenReader – dùng MS Visual C++ 6.0 kết hợp với Office XP. Chương trình này hoạt động gần giống với MS Narrator được tích hợp trong Windows XP (nếu dùng Windows XP, bạn nhấn tổ hợp phím [Windows + u] để kích hoạt Narrator).

Quá trình tạo ScreenReader gồm 3 bước chính:

Dùng cơ chế câu móc của Windows (Windows Hook) để theo dõi vị trí của trỏ chuột trên màn hình.

Lấy thông tin của phần tử giao diện tại vị trí trỏ chuột. Trong bài viết này, tôi dùng phương pháp MSAA (Microsoft Active Accessiblity).
Dùng chức năng text-to-speech của Office XP (thông qua cơ chế Automation) để đọc các thông tin lấy được ở bước 2. Như vậy điều kiện cần để chương trình hoạt động được là máy phải cài Office XP (hoặc phiên bản mới hơn).

1. Tạo HOOK

Vì phải theo dõi vị trí chuột ở mọi điểm trên màn hình, nên ta phải dùng cách câu móc toàn bộ hệ thống, do đó hàm lọc (hay còn gọi là “hook”) phải được đặt trong một DLL, tạm gọi là ScreenReader.dll. Để tạo ScreenReader.dll, bạn chạy VC 6.0, chọn menu File.New, sau đó nhấn tab project rồi chọn Win32 Dynamic-Link Library, sau đó chọn An empty DLL project, đặt tên project là ScreenReader. VC 6.0 sẽ tạo project trống (chỉ có file file ScreenReader.dsp) có tên ScreenReader. Tiếp theo bạn tạo mới một file ScreenReader.cpp, “add” file này vào project ScreenReader (vào menu Project.Add To Project.File…). Bạn nhập nội dung file ScreenReader.cpp như sau:

a. Việc đầu tiên là “gắn thêm” một số file .h cần thiết, bạn thêm các dòng sau vào đầu file ScreenReader.cpp:

#include “windows.h”

#include “comdef.h” // Khai báo kiểu _bstr_t

#include “oleacc.h” // Khai báo các hàm và hằng số trong thư viện MSAA

b. Khai báo các biến toàn cục và hàm:

__declspec(dllexport) int CALLBACK InstallFilter(int nCode );

__declspec(dllexport) LRESULT CALLBACK MouseFunc(int nCode, WPARAM wParam, LPARAM lParam );

__declspec(dllexport) VOID CALLBACK TimerProc(HWND hwnd, UINT uMsg, UINT_PTR idEvent, DWORD dwTime);

HANDLE g_hInstance; // Lưu trữ handle của DLL

HHOOK g_hhookMouse; // Lưu handle của mouse hook (hàm lọc)

#pragma data_seg(“.SHARDATA”)

static bool g_bFrozen = false; // Cờ cho biết chuột có chuyển động hay không.

static POINT g_ptCurPos = {-1, -1}; // Vị trí trỏ chuột

static int g_nTimer = -1; // ID của timer

static IDispatch *g_pSpeech = NULL; // Con trỏ tới giao diện IDispatch

#pragma data_seg()

void Speak(_bstr_t _bstrSp);

HRESULT CallFnc(int autoType, VARIANT *pvResult, IDispatch *pDisp, LPOLESTR ptName, int cArgs…);
bool GetSpDispIntf();

DWORD GetInfoFromPoint(POINT pt, _bstr_t &_bstrInfo);

Vì ta dùng cơ chế câu móc toàn bộ hệ thống, nên DLL chứa hàm câu móc sẽ được ánh xạ vào không gian địa chỉ của tất cả các tiến trình (process) đang chạy trên hệ thống. Vì vậy, để có thể truy xuất các biến toàn cục dùng chung giữa các tiến trình, các biến này phải được đặt trong vùng dữ liệu dùng chung. Ở đây, vùng dữ liệu dùng chung chứa biến g_bFrozen, g_ptCurPos, g_nTimer, g_pSpeech. Để khai báo vùng dữ liệu dùng chung, bạn dùng chỉ dẫn #pragma, đồng thời mô tả các thuộc tính truy xuất của vùng dữ liệu đó vào mục SECTION trong file .def của project. Bạn tạo file ScreenReader.def có nội dung như sau rồi “add” vào project.

; ScreenReader.def : Declares the module parameters for the DLL.

LIBRARY “ScreenReader”

DESCRIPTION ‘ScreenReader Windows Dynamic Link Library’

EXPORTS

; Explicit exports can go here

SECTIONS

.SHARDATA Read Write Shared

c. Định nghĩa hàm chính (entry point) DllMain() cho DLL:

BOOL APIENTRY DllMain(HANDLE hModule, DWORD wReasonForCall, LPVOID lpReserved)

{

g_hInstance = hModule; // Lưu handle của DLL vào biến toàn cục

return TRUE;

}

d. Cài đặt hook. Để cài đặt hook, bạn cần thêm vào file ScreenReader.cpp 2 hàm sau:

__declspec(dllexport) int CALLBACK InstallFilter(int nCode)

{

if(nCode)

{

g_hhookMouse = SetWindowsHookEx(WH_MOUSE, (HOOKPROC)MouseFunc, (HINSTANCE)g_hInstance, 0); // Cài đặt hook

//Tạo đồng hồ

g_nTimer = ::SetTimer(NULL, 0, 1000, (TIMERPROC)TimerProc);

CoInitialize(NULL); // Khởi tạo thư viện COM

GetSpDispIntf(); // Tìm giao diện IDispatch của đối tượng Speech

}

else

{

UnhookWindowsHookEx(g_hhookMouse); // Bỏ hook

::KillTimer(NULL, g_nTimer); // Xoá bỏ đồng hồ

if(g_pSpeech != NULL)

g_pSpeech->Release();

CoUninitialize(); // Kết thúc thư viện COM

}

return 1;

}

__declspec(dllexport) LRESULT CALLBACK MouseFunc(int nCode, WPARAM wParam, LPARAM lParam)

{

g_bFrozen = false;

LPMOUSEHOOKSTRUCT MouseHookParam;

MouseHookParam = (MOUSEHOOKSTRUCT *)lParam;

g_ptCurPos = MouseHookParam->pt; // Lưu vị trí trỏ chuột

return( CallNextHookEx(g_hhookMouse, nCode, wParam, lParam));

}

Chỉ dẫn __declspec(dllexport) được dùng để chỉ định một hàm sẽ được DLL xuất ra bên ngoài cho các chương trình khác sử dụng sau này. Hàm MouseFunc sẽ được hệ thống gọi mỗi khi có một thông điệp liên quan đến chuột xuất hiện (ví dụ WM_MOUSEMOVE,…). Nhiệm vụ của hàm này là lưu lại vị trí hiện hành của trỏ chuột vào biến toàn cục g_ptCurPos.

2. Lấy thông tin về phần tử giao diện

Để lấy thông tin về phần tử giao diện màn hình, ta dùng Microsoft Active Accessiblity. MSAA là một chuẩn dựa trên COM do Microsoft đề xuất nhằm tạo ra một cách thức để các phần mềm và HĐH có thể giao tiếp được với nhau. Các DLL hiện thực cơ chế này được tích hợp vào các HĐH Windows phiên bản từ 98 trở về sau (để sử dụng được MSAA trên Windows 95, cần phải cài đặt MASSSDK được tải về từ website của Microsoft). MSAA cung cấp các tính năng lập trình dưới dạng các giao diện COM (COM interface) và API. Theo cơ chế này, các phần tử giao diện được đặc trưng bởi các đối tượng COM (COM object), tạm gọi là accessible object – đối tượng có thể truy cập. Các đối tượng này lưu giữ thông tin (còn gọi là thuộc tính) về phần tử giao diện mà nó đại diện, bao gồm tên, toạ độ màn hình, và một số thông tin khác. Tất cả các accessibility object đều hiện thực interface IAccessible; và thông qua việc gọi các phương thức và truy cập vào các thuộc tính của interface này, chúng ta có thể lấy được thông tin của phần tử giao diện.

Trước hết, dùng MSAA API AccessibleObjectFromPoint để lấy giao diện IAccessible của accessible object tại vị trí trỏ chuột. Sau đó gọi các phương thức lấy thuộc tính của giao diện IAccessible (ví dụ như get_accName(), accLocation(), get_accRole(), v.v…) để lấy thông tin trên màn hình tại vị trí trỏ chuột.

Để dùng được MSAA, bạn cần “gắn thêm” file oleacc.h như trong phần 1.a) và thêm tham chiếu đến file oleacc.lib vào project (vào menu Project.Setting rồi chọn tab Link, chọn Category Input).

DWORD GetInfoFromPoint(POINT pt, _bstr_t &_bstrInfo)

{

IAccessible *paccObj = NULL;

VARIANT varChild;

VariantInit(&varChild);

if(AccessibleObjectFromPoint(pt, &paccObj, &varChild) == S_OK)

{

VARIANT var;

VariantInit(&var);

UINT nRoleLeng;

WCHAR *lpszRoleStr = NULL;

BSTR bstrName = NULL;

// Lấy role của object

if(paccObj->get_accRole(varChild, &var) == S_OK)

{

nRoleLeng = GetRoleTextW(var.lVal, (WCHAR*)NULL, 0);

lpszRoleStr = new WCHAR[nRoleLeng + 1];

GetRoleTextW(var.lVal, lpszRoleStr, nRoleLeng + 1);

}

// Lấy thuộc tính name hoặc value của object

if(paccObj->get_accName(varChild, &bstrName) != S_OK)

paccObj->get_accValue(varChild, &bstrName);

_bstr_t _bstrName(bstrName, FALSE);

_bstr_t _bstrTemp(L”: “, TRUE);

_bstrInfo = lpszRoleStr + _bstrTemp;

_bstrInfo = (wchar_t*)_bstrInfo + _bstrName;

VariantClear(&var);

paccObj->Release();

delete [] lpszRoleStr;

}

VariantClear(&varChild);

return 1;

}

3. Đọc chuỗi mô tả phần tử giao diện

Chức năng text-to-speech của Office XP cho phép đọc một chuỗi tiếng Anh bất kỳ. Chúng ta sẽ dùng cơ chế Automation để thêm tính năng này vào ScreenReader. Các bước tiến hành như sau:

a. Xây dựng hàm gọi một phương thức bất kỳ được hỗ trợ bởi một COM object thông qua giao diện IDispatch.

HRESULT CallFnc(int nContext, VARIANT *pvResult, IDispatch *pDisp, LPOLESTR ptName, int cArgs…)

{

// Danh sách các tham số

va_list marker;

va_start(marker, cArgs);

HRESULT hr = S_FALSE;

if(pDisp)

{

// Tìm DISPID tương ứng với tên của phương thức hoặc thuộc tính

DISPID dispID;

if(pDisp->GetIDsOfNames(IID_NULL, &ptName, 1, LOCALE_USER_DEFAULT, &dispID) == S_OK)

{

// Tạo các tham số dispatch

DISPPARAMS dp = {NULL, NULL, 0, 0};

DISPID dispidNamed = DISPID_PROPERTYPUT;

// Cấp phát vùng nhớ cho các tham số

VARIANT *pArgs = new VARIANT[cArgs+1];

// Tách các tham số

for(int i=0; i

b. Nhận giao diện IDispatch của đối tượng Speech

bool GetSpDispIntf()

{

CLSID clsid;

if(CLSIDFromProgID(L”Excel.Application”, &clsid) == S_OK)

{

// Khởi tạo Excel và lấy IDispatch

IDispatch *pExcelApp = NULL;

if(CoCreateInstance(clsid, NULL, CLSCTX_LOCAL_SERVER, IID_IDispatch, (void **)&pExcelApp) == S_OK)

{

// Lấy giao diện IDispatch của object Speech

VARIANT result;

VariantInit(&result);

CallFnc(DISPATCH_PROPERTYGET, &result, pExcelApp, L”Speech”, 0); // Dùng macro L để tạo chuỗi unicode

g_pSpeech = result.pdispVal;

pExcelApp->Release();

return true;

}

else

::MessageBox(NULL, “Excel not registered properly”, “Error”, 0x10010);

}

else

::MessageBox(NULL, “CLSIDFromProgID() failed”, “Error”, 0x10010);

return false;

}

c. Gọi phương thức Speak của đối tượng Speech để đọc một chuỗi tiếng Anh.

void Speak(_bstr_t _bstrSp)

{

if(g_pSpeech)

{

VARIANT result;

VariantInit(&result);

VARIANT x;

x.vt = VT_BSTR;

x.bstrVal = _bstrSp;

// Gọi phương thức Speech.Speak() để đọc chuỗi

CallFnc(DISPATCH_METHOD, &result, g_pSpeech, L”Speak”, 1, x);

SysFreeString(x.bstrVal);

}

return;

}

d. Để tránh việc đọc diễn ra liên tục khi trỏ chuột di chuyển trên màn hình, ta dùng một đồng hồ (Timer) để đo thời gian dừng của trỏ chuột, chỉ khi nào trỏ chuột dừng chuyển động trong một khoảng thời gian nào đó (trong bài viết này giả sử là 1 giây), chúng ta mới thực hiện việc lấy thông tin trên màn hình và đọc.

__declspec(dllexport) VOID CALLBACK TimerProc(HWND hwnd, UINT uMsg, UINT_PTR idEvent, DWORD dwTime)

{

if(g_bFrozen)

{

g_bFrozen = false;

_bstr_t _bstrInfo;

// Lấy thông tin tại vị trí trỏ chuột

GetInfoFromPoint(g_ptCurPos, _bstrInfo);

// Bỏ timer vì thời gian đọc chuỗi có thể lớn hơn thời gian giữa các

//message WM_TIMER.

::KillTimer(NULL, g_nTimer);

Speak(_bstrInfo);

g_nTimer = ::SetTimer(NULL, 0, 1000, (TIMERPROC)TimerProc);

}

g_bFrozen = true;

}

Tới đây, bạn đã hoàn tất những công việc để tạo ScreenReader.dll. Biên dịch toàn bộ project ScreenReader.dsp để tạo ra hai file ScreenReader.lib và ScreenReader.dll. Công việc tiếp theo là tạo một ứng dụng sử dụng DLL vừa được tạo.

Dùng ScreenReader.dll

Dùng VC 6.0 tạo một MFC dialog project, thêm hai button Run và Stop vào dialog, xoá tất cả các button không cần thiết khác. Dialog có dạng như sau:

Thêm các hàm xử lý sự kiện cho các button:

int CALLBACK InstallFilter(int nCode ); // prototype

void CBlinderAidDlg::OnRun()

{

InstallFilter(TRUE); // Hàm này được định nghĩa trong ScreenReader.dll

}

void CBlinderAidDlg::OnStop()

{

InstallFilter(FALSE);

}

Việc cuối cùng còn lại là dịch chương trình và chạy thử. Bạn có thể mở một trang web (tiếng Anh), rê chuột đến đoạn cần đọc và… nghe máy tính đọc.

Kết luận

MSAA có một số hạn chế trong việc lấy thông tin trên màn hình nên chương trình đọc không chính xác trên một số giao diện ứng dụng như Acrobat, Visual Studio 6.0 v.v… Tuy nhiên, chương trình chạy tốt trên nhiều ứng dụng khác như IE, Visual Studio 7.0, Office (mọi version), Windows Explorer, v.v… Bạn cũng có thể áp dụng qui trình vừa mô tả trên với VNSpeech – một thư viện đọc tiếng Việt (http://www.freewebs.com/vnspeech/) – để tạo ra chương trình đọc báo điện tử bằng tiếng Việt hoặc màn hình có giao diện tiếng Việt (TGVT A , số 7/2004, Tr.113). Ngoài ra, để chương trình chạy tốt hơn, bạn nên tạo một tiểu trình (thread) riêng để thực hiện việc đọc chuỗi, và thêm chức năng ngừng đọc nếu không muốn nghe hết những đoạn văn bản quá dài. Trong bài viết này, vì có xử lý thông điệp WM_MOUSEMOVE nên tôi dùng cơ chế Windows Hook để theo dõi vị trí trỏ chuột. Nếu không có nhu cầu xử lý thông điệp WM_MOUSEMOVE, bạn có thể theo dõi vị trí trỏ chuột bằng cách dùng API GetCurPos() thay vì dùng Windows Hook. Mã nguồn chương trình mẫu có thể tải về tại đây.