利用鍵盤鉤子開發按鍵發音程式

Max Woods發表於2014-08-16

一、前言

一日,看見我媽正在用電腦練習打字,頻頻低頭看鍵盤,我想:要是鍵盤能發音的話,不就可以方便她養成"盲打"的好習慣嗎?光想不做可不行,開始行動(您可千萬別急著去拿工具箱啊^_^)...
按鍵能發音,其關鍵就是讓程式能夠知道當前鍵盤上是哪個鍵被按下,並播放相應的聲音,自己的程式當然不在話下,那麼其它程式當前按下哪個鍵如何得知呢?利用鍵盤鉤子便可以很好地解決。

二、掛鉤(HOOK)的基本原理

WINDOWS呼叫掛接的回撥函式時首先會呼叫位於函式鏈首的函式,我們只要將自己的回撥函式置於鏈首,該回撥函式就會首先被呼叫。那麼如何將我們自己的回撥函式置於函式鏈的鏈首呢?函式SetWindowsHookEx()實現的就是該功能。我們首先來看一下SetWindowsHookEx函式的原型:

HHOOK SetWindowsHookEx(
  int idHook,       
  HOOKPROC lpfn,     
  HINSTANCE hMod,    
  DWORD dwThreadId  
); 

第一個引數:指定鉤子的型別,有WH_MOUSE、WH_KEYBOARD等十多種(具體參見MSDN)

第二個引數:標識鉤子函式的入口地址

第三個引數:鉤子函式所在模組的控制程式碼;

第四個引數:鉤子相關函式的ID用以指定想讓鉤子去鉤哪個執行緒,為0時則攔截整個系統的訊息。

另外需要注意的是為了捕獲所有事件,掛鉤函式應該放在動態連結庫DLL中。

三、具體實現

理論的話就不多說了,執行VC++6.0,新建一個MFC AppWizard(dll)工程,命名為Hook,使用預設的建立DLL型別的選項,也就是使用共享MFC DLL,點選完成後開始編寫程式碼:

(1)在Hook.h中定義全域性函式

BOOL installhook(); //鉤子安裝函式
LRESULT CALLBACK KeyboardProc(int nCode, WPARAM wParam, LPARAM lParam);//掛鉤函式

(2)在Hook.cpp檔案的#endif下新增定義全域性變數Hook的程式碼:

static HHOOK hkb=NULL;
HINSTANCE hins; //鉤子函式所在模組的控制程式碼

(3)新增核心程式碼

BOOL installhook()
{
    hkb=SetWindowsHookEx(WH_KEYBOARD,(HOOKPROC)KeyboardProc,hins,0);
    return TRUE;
}

第一個引數指定鉤子的型別,因為我們只用到鍵盤操作所以設定為WH_KEYBOARD;第二個引數將鉤子函式的入口地址指定為KeyboardProc,當鉤子鉤到任何訊息後便呼叫這個函式,即當不管系統的哪個視窗有鍵盤輸入馬上會引起KeyboardProc的動作;第三個引數是鉤子函式所在模組的控制程式碼;最後一個引數是鉤子相關函式的ID用以指定想讓鉤子去鉤哪個執行緒,為0時則攔截整個系統的訊息;

現在,就開始定義當鍵盤上的鍵按下時程式要做什麼了~

KeyboardProc動作:

LRESULT CALLBACK KeyboardProc(int nCode,WPARAM wParam,LPARAM lParam)
{ 
    if(((DWORD)lParam&0x40000000) && (HC_ACTION==nCode))
    {
       switch(wParam) //鍵盤按鍵標識
        {
        case ''1'':sndPlaySound("1.wav",SND_ASYNC);break; //當數字鍵1被按下
         case ''2'':sndPlaySound("2.wav",SND_ASYNC);break;
        case ''3'':sndPlaySound("3.wav",SND_ASYNC);break;
        case ''4'':sndPlaySound("4.wav",SND_ASYNC);break;
        ....
        case ''A'':sndPlaySound("a.wav",SND_ASYNC);break; //當字母鍵A被按下
         case ''B'':sndPlaySound("b.wav",SND_ASYNC);break;
        case ''C'':sndPlaySound("c.wav",SND_ASYNC);break;
        case ''D'':sndPlaySound("d.wav",SND_ASYNC);break;
        ....
        }
     }
     LRESULT RetVal = CallNextHookEx( hkb, nCode, wParam, lParam ); 
     return RetVal;
}

上面的程式碼中我們用播放聲音做為按鍵被按下後的動作,API函式sndPlaySound的第一個引數定義的聲音檔案的絕對路徑(比如要播放C盤下的a.wav,就定義成"C:\\a.wav");第二引數定義播放模式,SND_ASYNC模式可以及時地釋放正在播放的聲音檔案,立刻停止當前聲音的播放轉去播放新的聲音,這樣在我們連續擊鍵時就不會有阻塞感了.為了執行sndPlaySound函式,必須在Hook.cpp的檔案頭加上:

#include "mmsystem.h"

並且點選VC++選單上的“工程”-“設定”進入Link屬性頁,在L物件/庫模組下輸入:winmm.lib後確定即可.

(4)新增輸出標識

在Hook.def的末尾新增

installhook
KeyboardProc

短短的四步,鍵盤鉤子的製作算是完成了,編譯生成後的DLL檔案就可以自由的用別的程式來呼叫了.

在程式中如何呼叫DLL呢?那就簡單了.再用VC++6.0新建一個MFC AppWizard(exe)工程,命名為KeySound,點選"確定"後選擇程式型別為對話方塊,直接點選確定即可.

在KeySoundDlg.cpp檔案中的OnInitDialog()初始化函式的CDialog::OnInitDialog();下面新增:

//阻止程式反覆駐留記憶體,也為了防止有兩個程式同時讀取DLL而發生錯誤.

CreateMutex(NULL, FALSE, "KeySound");
if(GetLastError()==ERROR_ALREADY_EXISTS)
   OnOK();

//讀取DLL
static HINSTANCE hinstDLL; 
typedef BOOL (CALLBACK *inshook)(); 
inshook instkbhook;
if(hinstDLL=LoadLibrary((LPCTSTR)"Hook.dll"))
{
    instkbhook=(inshook)GetProcAddress(hinstDLL,"installhook");  
    instkbhook();
}
else
{
    MessageBox("當前目錄找不到Hook.dll檔案,程式初始化失敗");
    OnOK();
}

將編譯生成後的KeySound.exe和Hook.dll放在同一目錄下,定義好聲音檔案,執行KeySound.exe後開啟記事本或寫字板,體驗一下系統為您即時快速地朗讀您按下的每一個鍵的快感吧^-^

有一點必須說明,標準鍵盤有101個鍵,您想讓多少鍵發聲音,就必須在上面的KeyboardProc動作裡定義多少個鍵,常用的10個數字鍵和26個英文字母不會給您帶來太大的困難,只要相應的''A''對應A鍵,''1''對應1鍵就可以,但如果您希望能讓更多的鍵都有各種特色音樂的話,很可能會遇到一些鍵盤編碼上的麻煩,比如ESC鍵就不能簡單的用''ESC''來搞定了,得用VK_ESCAPE,又比如Alt鍵得用VK_MENU來定義,沒有個鍵盤編碼表的話會令人相當頭疼,這裡我介紹一種讓程式來告訴您鍵盤按鍵名稱的方法:

為一個工程新增PreTranslateMessage對映,新增如下程式碼:

char KeyName[50];
ZeroMemory(KeyName,50);
if(pMsg -> message == WM_KEYDOWN)
{ 
   GetKeyNameText(pMsg->lParam,KeyName,50);
   MessageBox(KeyName);
} 

那麼當程式視窗顯示在面前時按下某個鍵,就會彈出一個訊息顯示該鍵的名稱,然後用''''包起來就可以了,比如逗號句號,就是'',''和''.'',簡單吧:)

到此就全部完成了按鍵發音程式的編寫,通過改變聲音檔案的名稱而不用改動程式本身就可以達到更換按鍵聲音的目的了,只是有個遺憾,聲音檔案在硬碟中的位置不能變更,從C盤換移動D盤程式就不能播放了,怎麼樣才能靈活的讀取聲音檔案呢?可以用API函式GetModuleFileName來得到程式所在的目錄,具體實現方法如下:

(1)在Hook.h的public:下面新增:

BOOL InitInstance(); //初始化函式

(2)在Hook.cpp的#endif下新增定義全域性變數的程式碼:

char szBuf[256];
char *p;
CString msg;

(3)在Hook.cpp中適當位置新增:

BOOL CHookApp::InitInstance ()
{ 
   hins=AfxGetInstanceHandle();
   GetModuleFileName(AfxGetInstanceHandle( ),szBuf,sizeof(szBuf));
   p = szBuf;
   while(strchr(p,''\\'')) 
   { 
        p = strchr(p,''\\''); 
        p++; 
   }
   *p = ''\0''; 
   msg=szBuf;
   return TRUE; 
}

(4)新建一個資料夾並命名為Sound;

(5)改變聲音檔案物理位置定義方式

case ''1'':sndPlaySound(msg+"sound\\1.wav",SND_ASYNC);break; 

msg是得到程式當前所在目錄,加上後面的程式碼就是指播放當前目錄下的Sound目錄裡的1.wav檔案,這樣就將聲音檔案的絕對路徑改成了靈活的相對路徑.您只要把KeySound.exe,Hook.dll和Sound資料夾放在同一個資料夾下,以後只要搬動整個資料夾就能實現聲音檔案的任意移動了。

除錯時需要注意:將Hook.dll、Sound目錄放在KeySound.exe的執行目錄下。假如編譯連結的時候出現unresolved external symbol __imp__sndPlaySoundA@8 這樣的資訊,請在Project Settings中加入Winmm.lib 。

相關文章