Để sử dụng hook trong .NET, bạn cần phải làm việc với unmanaged-code hay cụ thể là các Windows API (Win32 API) gồm: SetWindowsHookEx, CallNextHookEx và UnhookWindowsEx (Xem bài Giới thiệu kĩ thuật Hook và các khái niệm cơ bản). Trong bài viết này, tôi sẽ hướng dẫn cách tạo một ứng dụng hook bàn phím toàn hệ thống.

Ánh xạ kiểu dữ liệu giữa Win32 và .NET

Một vài điểm cần lưu ý khi bạn làm việc với unmanaged-code là việc ánh xạ kiểu dữ liệu giữa Win32 và .NET. Trong Win32 bạn thấy có nhiều kiểu dữ liệu như LRESULT, HWND, HINSTANCE, HHOOK,… thực chất chúng là chỉ là kiểu số nguyên. Chúng được định nghĩa lại cho phù hợp với ý nghĩa và mục đích sử dụng. Trong .NET các kiểu này được ánh xạ tương ứng với kiểu IntPtr. Bạn cũng có thể dùng kiểu int và có thể ép kiểu trực tiếp giữa int và IntPtr.

Tương tự như vậy, con trỏ hàm trong Win32 được ánh xạ tương ứng với delegate. Vậy, với mỗi Hook procedure, bạn cũng phải tạo ra một delegate để làm callback function cho các hàm SetWindowsHookEx và CallNextHookEx.

Một điểm thuận lợi khi dùng các Win32 API trong .NET là bạn có thể tùy chọn kiểu trong khai báo các tham số của hàm. Như bạn có thể thấy trong ví dụ, việc thay thế giữa kiểu int và IntPtr là hợp lệ khi khai báo các hàm như SetWindowsHookEx, UnhookWindowsEx, CallNextHookEx,…

Các Win32 API Function

Sử dụng attribute [DllImport], ta sẽ thêm ba Win32 API cần thiết cho việc sử dụng hook.



Mã:
[DllImport("user32.dll",SetLastError=true)]private static extern IntPtr SetWindowsHookEx(int idHook,    LowLevelKeyboardProc lpfn, IntPtr hMod, uint dwThreadId); [DllImport("user32.dll", SetLastError = true)]private static extern bool UnhookWindowsHookEx(IntPtr hhk); [DllImport("user32.dll", SetLastError = true)]private static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode,    IntPtr wParam, IntPtr lParam);
Tham số SetLastError được đặt bằng true để chúng ta có thể lấy được lỗi trong trường hợp cài đặt hook thất bại.

Các Win32 Structure và Constants

Vì tôi sẽ tạo một ứng dụng hook low level keyboard nên cần phải có hằng số xác định kiểu hook là:


private const int WH_KEYBOARD_LL = 13;
Tiếp đến trong ví dụ này tôi chỉ bắt hai thông điệp KeyUp và KeyDown của bàn phím. Hai thông điệp này có giá trị tương ứng là:


private const int WM_KEYDOWN = 0×0100;

private const int WM_KEYUP = 0×101;
Ngoài ra Win32 còn định nghĩa một structure dùng để lưu thông tin của các sự kiện bàn phím ở mức thấp. Structure này được đặt tên là KBDLLHOOKSTRUCT, tuy nhiên theo chuẩn đặt tên của C#, tôi sẽ đặt một tên rõ ràng hơn cho structure này.



Mã:
[StructLayout(LayoutKind.Sequential)]public struct KeyboardHookStruct{    public int VirtualKeyCode;    public int ScanCode;    public int Flags;    public int Time;    public int ExtraInfo;}
Tham khảo (MSDN): KBDLLHOOKSTRUCT Structure

LowLevelKeyboardProc Callback Function (Hook procedure)

Khi có sự kiện nhấn phím, hàm callback này sẽ được gọi bởi hệ thống. Cú pháp định nghĩa của hàm này có dạng:


LRESULT CALLBACK LowLevelKeyboardProc(
__in int nCode,
__in WPARAM wParam,
__in LPARAM lParam
);
Trong đó:

- nCode: Xác định hook procedure có thực hiện xử lý không. Nếu giá trị nCode nhỏ hơn 0, hook procedure sẽ bỏ qua xử lý thông điệp. Nếu giá trị bằng 0 (HC_ACTION) có nghĩa là hai tham số wParam và lParam sẽ chứa thông tin về thông điệp bàn phím.

- Value Meaning
HC_ACTION0 The wParam and lParam parameters contain information about a keyboard message.
- wParam: kiểu thông điệp bàn phím, bao gồm: WM_KEYDOWN, WM_KEYUP, WM_SYSKEYDOWN, WM_SYSKEYUP.

- lParam: con trỏ tới KBDLLHOOKSTRUCT structure.

Trong ví dụ này tôi sẽ định nghĩa hook procedure này như sau:



Mã:
private IntPtr KeyboardHookProc(int nCode, IntPtr wParam, IntPtr lParam){    if (nCode >= 0)    {    // do something    }     return CallNextHookEx(_hookHandle, nCode, wParam, lParam);}
Và tạo một delegate tương ứng để làm nhiệm vụ “con trỏ hàm”:


Mã:
public delegate IntPtr KeyboardHookDelegate(int nCode, IntPtr wParam, IntPtr lParam);
Các điểm cần lưu ý

Lấy handle của tập tin chứa hook procedure

Khi cài đặt global hook, bạn phải có được handle của tập tin PE chứa hook procedure. Trong .NET, tập tin này là một module (một assembly có thể gồm nhiều module). Bạn có thể lấy handle của tập tin bằng cách dùng Win32 API GetModuleHandle, tuy nhiên một cách khác mà .NET hỗ trợ là dùng phương thức static Marshal.GetHINSTANCE().

Dòng lệnh sau cho ta thấy cách để lấy handle của module chính (chứa hook procedure) trong assembly:


Mã:
IntPtr hInstance = Marshal.GetHINSTANCE(Assembly.GetExecutingAssembly().GetModules()[0]);

Nhận thông báo lỗi của Win32

Để lấy được lỗi khi cài đặt hook thất bại, bạn phải tham số SetLastError bằng true trong attribute [DllImport] của hàm API cần bắt lỗi. Khi đó để lấy được lấy mã lỗi, thay vì dùng Win32 API GetLastError ta có thể dùng phương thức static GetLastWin32Error() của lớp System.Runtime.InteropServices.Marshal.

Khi đã có được mã lỗi, thay vì tìm kiếm bảng dò mã lỗi thì bạn có thể dùng lớp Win32Exception trong namespace System.ComponentModel. Constructor của Win32Exception nhận một số chỉ mã lỗi và sẽ chuyển sang thông điệp lỗi tương ứng.

Ví dụ hàm SetWindowsHookEx trả về handle của hook là 0 nếu cài đặt thất bại, bạn có thể viết như sau để xem nguyên nhân của việc thất bại:


Mã:
if (_hookHandle == IntPtr.Zero)throw new Win32Exception(Marshal.GetLastWin32Error());
Cài đặt Hook

Phần cài đặt rất đơn giản, tôi tạo một phương thức public Install() để gọi phương thức cài đặt hook thực sự là SetupHook(). Sau khi gọi SetupHook(), phương thức Install() sẽ kiểm tra hook handle và sẽ ném ra một Win32Exception nếu cài đặt hook thất bại. Như bạn thấy phương thức SetupHook() chỉ đơn giản là gọi hàm Win32 API SetWindowsHookEx:



Mã:
private KeyboardHookDelegate _hookProc;private IntPtr _hookHandle = IntPtr.Zero; // ... public void Install(){    _hookProc = KeyboardHookProc;    _hookHandle = SetupHook(_hookProc);     if (_hookHandle == IntPtr.Zero)        throw new Win32Exception(Marshal.GetLastWin32Error());} private IntPtr SetupHook(KeyboardHookDelegate hookProc){    IntPtr hInstance = Marshal.GetHINSTANCE(Assembly.GetExecutingAssembly().GetModules()[0]);     return SetWindowsHookEx(WH_KEYBOARD_LL, hookProc, hInstance, 0);}
Gỡ bỏ Hook

Bạn có thể thấy tôi có vẻ tạo một phương thức dư thừa khi chỉ đơn giản gọi lại một hàm khác. Thực sự phương thức Uninstall() sau chỉ khác nhau về tham số so với hàm UnhookWindowsHookEx:


Mã:
public void Uninstall(){    UnhookWindowsHookEx(_hookHandle);}
Phương thức này cũng như phương thức Install() trên được tôi đặt trong một lớp riêng là Y2KeyboardHook. Sẽ an toàn hơn khi bạn tạo hạn chế việc gọi trực tiếp đến các Win32 API do việc truyền tham số sai có thể gây ra nguy hiểm trong một số trường hợp. Việc encapsulation các hàm API cũng giúp cho việc sử dụng lớp bạn tạo ra dễ dàng và thân thiện hơn, như vậy khi sử dụng, bạn không cần phải nhớ tất cả các tham số không cần thiết.

Để bảo đảm việc gỡ bỏ hook được thực hiện, tôi viết thêm một destructor và gọi phương thức Uninstall() trong đó. Destructor sẽ tự động được gọi khi đối tượng bị hủy, bạn không cần phải gọi trực tiếp destructor:


Mã:
// destructor~Y2KeyboardHook(){    Uninstall();}
Thêm các event

Sẽ tiện lợi hơn nếu bạn thêm các event cho lớp để sử dụng như một control. Net đã cung cấp sẵn delegate KeyEventHandler. Nếu bạn cần tìm hiểu về cách tạo event, có thể tham khảo hướng dẫn tại: Tạo, sử dụng và quản lý Event trong C#.


Mã:
public event KeyEventHandler KeyDown;public event KeyEventHandler KeyUp;
Trong hook procedure, ta sẽ xử lý để kích hoạt event nếu thông điệp tương ứng với event đó xảy ra:


Mã:
private IntPtr KeyboardHookProc(int nCode, IntPtr wParam, IntPtr lParam){    if (nCode >= 0)    {        KeyboardHookStruct kbStruct = (KeyboardHookStruct)Marshal.PtrToStructure(lParam, typeof(KeyboardHookStruct));         if (wParam == (IntPtr)WM_KEYDOWN)        {            if (KeyDown != null)                KeyDown(null, new KeyEventArgs((Keys)kbStruct.VirtualKeyCode));        }        else if (wParam == (IntPtr)WM_KEYUP)        {            if (KeyUp != null)                KeyUp(null, new KeyEventArgs((Keys)kbStruct.VirtualKeyCode));        }    }     return CallNextHookEx(_hookHandle, nCode, wParam, lParam);}
Ví dụ hoàn chỉnh

Để thực hiện ví dụ này, bạn hãy tạo một dự án Windows Forms Application. Thêm lớp Y2KeyboardHook sau vào dự án:



Mã:
using System;using System.Runtime.InteropServices;using System.Diagnostics;using System.Windows.Forms;using System.Reflection;using System.ComponentModel; namespace HookApp{     class Y2KeyboardHook    {         #region Win32 API Functions and Constants         [DllImport("user32.dll", SetLastError = true)]        private static extern IntPtr SetWindowsHookEx(int idHook,            KeyboardHookDelegate lpfn, IntPtr hMod, int dwThreadId);         [DllImport("user32.dll", SetLastError = true)]        private static extern bool UnhookWindowsHookEx(IntPtr hhk);         [DllImport("user32.dll", SetLastError = true)]        private static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode,            IntPtr wParam, IntPtr lParam);         [DllImport("kernel32.dll")]        private static extern IntPtr GetModuleHandle(string lpModuleName);         private const int WH_KEYBOARD_LL = 13;         private const int WM_KEYDOWN = 0x0100;        private const int WM_KEYUP = 0x101;         #endregion         private KeyboardHookDelegate _hookProc;        private IntPtr _hookHandle = IntPtr.Zero;         public delegate IntPtr KeyboardHookDelegate(int nCode, IntPtr wParam, IntPtr lParam);         [StructLayout(LayoutKind.Sequential)]        public struct KeyboardHookStruct        {            public int VirtualKeyCode;            public int ScanCode;            public int Flags;            public int Time;            public int ExtraInfo;        }         #region Keyboard Events         public event KeyEventHandler KeyDown;        public event KeyEventHandler KeyUp;         #endregion         // destructor        ~Y2KeyboardHook()        {            Uninstall();        }         public void Install()        {            _hookProc = KeyboardHookProc;            _hookHandle = SetupHook(_hookProc);             if (_hookHandle == IntPtr.Zero)                throw new Win32Exception(Marshal.GetLastWin32Error());        }        private IntPtr SetupHook(KeyboardHookDelegate hookProc)        {            IntPtr hInstance = Marshal.GetHINSTANCE(Assembly.GetExecutingAssembly().GetModules()[0]);             return SetWindowsHookEx(WH_KEYBOARD_LL, hookProc, hInstance, 0);        }         private IntPtr KeyboardHookProc(int nCode, IntPtr wParam, IntPtr lParam)        {            if (nCode >= 0)            {                KeyboardHookStruct kbStruct = (KeyboardHookStruct)Marshal.PtrToStructure(lParam, typeof(KeyboardHookStruct));                 if (wParam == (IntPtr)WM_KEYDOWN)                {                    if (KeyDown != null)                        KeyDown(null, new KeyEventArgs((Keys)kbStruct.VirtualKeyCode));                }                else if (wParam == (IntPtr)WM_KEYUP)                {                    if (KeyUp != null)                        KeyUp(null, new KeyEventArgs((Keys)kbStruct.VirtualKeyCode));                }            }             return CallNextHookEx(_hookHandle, nCode, wParam, lParam);        }         public void Uninstall()        {            UnhookWindowsHookEx(_hookHandle);        }     }}
Trong tập tin Form1.cs, bạn hãy sửa lại như sau:


Mã:
using System;using System.Windows.Forms;using HookApp;using System.Drawing; namespace WindowsFormApplication1{    public partial class Form1 : Form    {        Y2KeyboardHook _keyboardHook;         public Form1()        {            InitializeComponent();             this.TopMost = true;             ListBox listBox1 = new ListBox();            listBox1.Location = new Point(10, 10);            listBox1.Size = new Size(200, 200);             this.Controls.Add(listBox1);             _keyboardHook = new Y2KeyboardHook();            _keyboardHook.Install();             _keyboardHook.KeyDown += (sender, e) =>                {                    listBox1.Items.Add("KeyDown: " + e.KeyCode);                     listBox1.SelectedIndex = listBox1.Items.Count - 1;                };             _keyboardHook.KeyUp += (sender, e) =>                {                    listBox1.Items.Add("KeyUp: " + e.KeyCode);                     listBox1.SelectedIndex = listBox1.Items.Count - 1;                };        }     }}
Đoạn mã mới thêm vào Form1 sẽ thêm một ListBox vào Form, đồng thời tạo ra một đối tượng Y2KeyboardHook và xử lý hai event KeyDown và KeyDown bằng lambda expression. Bạn có thể chuyển sang ứng dụng khác và nhấn phím bất kì, các sự kiện tương ứng sẽ được thêm vào ListBox.

Kết luận

Bạn có thể thấy rằng việc sử dụng hook trong C# rất dễ dàng, tuy nhiên để cải tiến chương trình hook bạn cần phải kiến thức đầy đủ về các hàm Win32 API cần thiết. Dựa vào ví dụ này, bạn có thể viết được một chương trình hook mouse đơn giản. Và hơn nữa là một chương trình có mức tổng quát cao cho phép cài đặt nhiều loại hook khác nhau.

Related articles
Win32 – Giới thiệu kĩ thuật Hook và các khái niệm cơ bản