在Windows環境下通過編程來操縱鼠標、鍵盤是一件再簡單不過的事了,不過大家有沒有想過要嘗試一下另一樣我們比較常見的輸入工具——游戲操縱桿呢?在某些情況下,尤其是象編制一些小型的游戲軟件的時候,加入對游戲操縱桿的支持可以給使用者提供更為友好的人機界面,極大的提高游戲軟件的可玩性。
C++Builder中沒有專門控制操縱桿函數(其實在常見的編程語言中基本上都沒有),因此要增加對游戲操縱桿的支持,就要和Windows的MCI API函數打交道,這裡我們首先介紹一些在讀取操縱桿的屬性、狀態,位置和按鈕信息時要用到的API函數、常量及數據結構。
相關常量:
#define MM_JOY1MOVE 0x3A0 /* 用以傳遞操縱桿當前狀態的一些消息 */
#define MM_JOY2MOVE 0x3A1
#define MM_JOY1ZMOVE 0x3A2
#define MM_JOY2ZMOVE 0x3A3
#define MM_JOY1BUTTONDOWN 0x3B5
#define MM_JOY2BUTTONDOWN 0x3B6
#define MM_JOY1BUTTONUP 0x3B7
#define MM_JOY2BUTTONUP 0x3B8
#define JOY_BUTTON1 0x0001 /* 用以表明當前操縱桿的狀態 */
#define JOY_BUTTON2 0x0002
#define JOY_BUTTON3 0x0004
#define JOY_BUTTON4 0x0008
#define JOY_BUTTON1CHG 0x0100
#define JOY_BUTTON2CHG 0x0200
#define JOY_BUTTON3CHG 0x0400
#define JOY_BUTTON4CHG 0x0800
/* 游戲操縱桿錯誤返回值 */
#define JOYERR_BASE 160
#define JOYERR_NOERROR (0) /* 正常 */
#define JOYERR_ParmS (JOYERR_BASE+5) /* 參數錯誤 */
#define JOYERR_NOCANDO (JOYERR_BASE+6) /* 無法正常工作 */
#define JOYERR_UNPLUGGED (JOYERR_BASE+7) /* 操縱桿未連接 */
/* 操縱桿標識號 */
#define JOYSTICKID1 0
#define JOYSTICKID2 1
相關函數:
WINMMAPI UINT WINAPI joyGetNumDevs(void);
獲取設備標識號。
MMRESULT WINAPI joyGetDevCaps(UINT uJoyID, LPJOYCAPS pjc, UINT cbjc);
獲取操縱桿屬性信息,以結構體JoyCaps接收。
WINMMAPI MMRESULT WINAPI joyGetPos(UINT uJoyID, LPJOYINFO pji);
獲取操縱桿位置和按鈕狀態,以結構體接收。
WINMMAPI MMRESULT WINAPI joyGetThreshold(UINT uJoyID, LPUINT puThreshold);
讀取操縱桿移動阈值。
WINMMAPI MMRESULT WINAPI joyReleaseCapture(UINT uJoyID);
結束對操縱桿信息的接收。
WINMMAPI MMRESULT WINAPI joySetCapture(HWND hwnd, UINT uJoyID, UINT uPeriod,
BOOL fChanged);
設置接收某一操縱桿的信息的窗口以及將何種頻度接收。
WINMMAPI MMRESULT WINAPI joySetThreshold(UINT uJoyID, UINT uThreshold);
設置操縱桿移動阈值。
相關結構體: typedef struct joyCaps{
WORD wMid; /* 制造商標識 */
WORD wPid; /* 生產編號 */
char szPname[MAXPNAMELEN]; /* 產品名稱 */
UINT wXmin; /* X軸最小值 */
UINT wXmax; /* X軸最大值 */
UINT wYmin; /* Y軸最小值 */
UINT wYmax; /* Y軸最大值 */
UINT wZmin; /* Z軸最小值 */
UINT wZmax; /* Z軸最大值 */
UINT wNumButtons; /* 按鈕數 */
UINT wPeriodMin; /* 最小調用間隔時間(單位 毫秒)*/
UINT wPeriodMax; /* 最大調用間隔時間(單位 毫秒)*/
}JOYCAPS, *PJOYCAPS, NEAR *NPJOYCAPS, FAR *LPJOYCAPS;
typedef struct joyInfo{
UINT wXpos; /* x 軸位置 */
UINT wYpos; /* y 軸位置 */
UINT wZpos; /* z 軸位置 */
UINT wButtons; /* 按鈕狀態 */
} JOYINFO, *PJOYINFO, NEAR *NPJOYINFO, FAR *LPJOYINFO;
以上這些定義存儲在mmsystem.h文件中,所以程序要包含這個頭文件。
程序需要首先檢查游戲操縱桿的存在,這包括了檢查驅動程序支持和確認操縱桿已與系統相連的兩項工作。joyGetNumDevs調用檢查系統是否配置了游戲端口和驅動程序。如果返回值為零,表明不支持操縱桿功能。如果joyGetNumDevs返回值不為零,則說明系統支持游戲操縱桿功能。但joyGetNumDevs並不能確定操縱桿是否已被連接上了,通過調用可以完成這些工作,並檢查是否有錯誤發生。
如果有游戲端口,joyGetNumDevs返回值通常為16.
一旦確認了操縱桿已連上,就可以接受器發來的消息。joySetCapture通知Windows操縱桿消息應發送到哪裡機發送的頻率如何。
joySetCapture中的第一個參數通知Windows誰將得到消息,第二個參數確定程序將從那個操縱桿接收消息。第三個參數時表示希望以怎樣的頻度接受JM_MOVE消息(單位為毫秒),無論操縱桿是否移動,都將以這個頻度接受JM_MOVE消息。joySetCapture的四個參數允許程序當操縱桿移動一定的距離後才接受消息。該距離由joySetThreshold設置。
joySetCapture被調用後,窗口將接受操縱桿事件。MM_JOYXMOVE(X=操縱桿號)事件已joySetCapture定義的時間間隔發生。只有當操縱桿的按鈕被按下時,MM_JOYXBUTTONUP和MM_JOYXBUTTONDOWN事件才發生。操縱桿時間出發句柄,改變相應的標簽狀態信息。移動消息也同時通知程序在新的位置重畫操縱桿標志。調用joyReleaseCapture通知Windows已結束操縱桿的調用。
在實際編制程序時,應首先在Form1的頭文件Form1.h中加入對mmsystem.h的引用,再加入一些相關的消息映射即對MM_JOYXMOVE、MM_JOYXBUTTONUP和MM_JOYXBUTTONDOWN事件的響應函數說明。
#include <mmsystem.h>
//--------------------
class Tform1:public TForm
{
__published:
...
...
private:
...
TPoint Position;//用於存儲操縱桿的坐標位置。
...
public:
MESSAGE_HANDLER(MM_JOY1BUTTONDOWN,TMessage,JMButonUpdate)
MESSAGE_HANDLER(MM_JOY1BUTTONUP,TMessage,JMButonUpdate)
MESSAGE_HANDLER(MM_JOY1MOVE,TMessage,JMMove)
END_MESSAGE_MAP(TForm)
};
在Form1的OnCreate事件中加入以下代碼用以檢測操縱桿。
void __fastcall TForm1::FormCreate(TObject *Sender)
{
DriverCount = joyGetNumDevs();
Connected = false;
MMRESULT JoyResult;
JOYINFO JoyInfo;
//檢查系統是否配置了游戲端口和驅動程序。
if(DriverCount != 0)
{
//仍需調用joyGetPos進行檢測,如果返回JOYERR_NOERROR則表示操縱桿連接正常。
//測試第一個操縱桿。
JoyResult = joyGetPos(JOYSTICKID1,&JoyInfo);
if(JoyResult == JOYERR_NOERROR )
{
Connected = true;
JoystickID = JOYSTICKID1;
}
//如果發生INVALIDPARAM錯誤,則退出。
else if(JoyResult == MMSYSERR_INVALPARAM)
Application->MessageBox("An error occured while calling joyGetPos",
"Error", MB_OK);
// 如果第一個操縱桿為連接,則檢查第二個操縱桿。
else if((JoyResult=joyGetPos(JOYSTICKID2,&JoyInfo)) == JOYERR_NOERROR)
{
Connected = true;
JoystickID = JOYSTICKID2;
}
}
}
在確定操縱桿已正確連接之後就可以讀取操縱桿的設備信息。
void TForm1::ShowDeviceInfo(void)
{
joyGetDevCaps(JoystickID,&JoyCaps, sizeof(JOYCAPS));
Label1->Caption = "Number of joysticks supported by driver = " +
IntToStr(DriverCount);
Label2->Caption = "Current Joystick ID = " +
IntToStr(intJoystickID);
Label3->Caption = "Manufacturer ID = " +
IntToStr(JoyCaps.wMid);
Label4->Caption = "Product ID = " +
IntToStr(JoyCaps.wPid);
Label5->Caption = "Number of buttons = "+
IntToStr(JoyCaps.wNumButtons);
.
.
.
// 設置當前窗口接收操縱桿信息。
if(Connected)
joySetCapture(Handle,JoystickID,2*JoyCaps.wPeriodMin,FALSE);
//計算操縱桿活動范圍和屏幕范圍的比率,在後面繪制操縱桿標志時會用到。
XDivider = (JoyCaps.wXmax - JoyCaps.wXmin)/ Width;
YDivider = (JoyCaps.wYmax - JoyCaps.wYmin)/ Height;
}
讀取操縱桿位置信息和按鈕狀態:
void TForm1::ShowStatusInfo(void)
{
if(Connected)
{
JOYINFO JoyInfo;
TPoint Position;
joyGetPos(JoystickID,&JoyInfo);
Position.x = JoyInfo.wXpos;
Position.y = JoyInfo.wYpos;
//顯示操縱桿的X、Y軸位置。
Label6->Caption = "X Position = " + IntToStr(int(JoyInfo.wXpos));
Label7->Caption = "Y Position = " + IntToStr(int(JoyInfo.wYpos));
//判斷某按鈕是否被按下,這裡只是指按鈕初始的狀態。
if(JoyInfo.wButtons & JOY_BUTTON1)
Label8->Caption = "Button 1 = Pressed";
else
Label8->Caption = "Button 1 = Not Pressed";
}
}
下面可以編寫用以響應當初在頭文件中定義的事件JMMove、JMButtonUpdate的代碼: JMButtonUpdate的代碼:
void __fastcall TForm1::JMMove(TMessage &msg)
{
/*當操縱桿位置發生變化時會自動調用本函數。
在本函數中經常是根據操縱桿當前的位置來繪制操縱桿在屏幕上顯示的標志,並擦 去原來的標志。這裡只是簡單的改變Image的坐標位置來表示操縱桿為的移動。 */
Position.x = msg.LParamLo;
Position.y = msg.LParamHi;
//計算新的坐標。
ScreenX = (Position.x-JoyCaps.wXmin)/XDivider - ImageList1->Width/2;
ScreenY = (Position.y-JoyCaps.wYmin)/YDivider - ImageList1->Height/2;
//顯示新位置的X、Y值。
Label6->Caption = "X Position = " + IntToStr(int(Position.x));
Label7->Caption = "Y Position = " + IntToStr(int(Position.y));
//移動Image的位置。
Image1->Top=ScreenY;
Image1->Left=ScreenX;
}
void __fastcall TForm1::JMButtonUpdate(TMessage &msg)
{
//當程序接收到JM_BUTTONDOWN和JM_BUTTONUP消息時,即某一按鈕的狀態發生改變時,都會調用本函數。
if(msg.WParam & JOY_BUTTON1) //判斷按鈕1是否被按下
Label8->Caption = "Button 1 = Pressed";
else
Label8->Caption = "Button 1 = Not Pressed";
}
最後在程序退出的時候要記得關閉對操縱桿的調用,即在FormDestroy事件中加入joyReleaseCapture(JoystickID)。
void __fastcall TForm1::FormDestroy(TObject *Sender)
{
if(Connected)
joyReleaseCapture(JoystickID);
}
以上就是使用操縱桿的常用方法,通過這些方法基本上就可以完成對操縱桿的一般操作,而且在了解了操縱桿的消息傳遞機制之後,我們還可以編寫出一些用操縱桿模擬鼠標或是用鼠標/鍵盤來模擬操縱桿的程序以及通過編程讓一般的手柄也象那種專用於格斗游戲的操縱桿一樣具有可以記憶組合鍵的功能。不過要想更快速、更全面的操作操縱桿就需要用到Windows下高版本DirectX中的DirectInput技術了,鑒於篇幅以及使用比較繁瑣的關系,這裡就不予介紹了。
最後說明一下,本程序在 CBuilder4/PWin98 SE 環境下通過,在 WindowsNT 下用到的API函數將會與本程序中介紹的函數有所不同,詳細區別請參閱Windows API函數手冊。另外在程序測試中,我僅使用了最一般的接在聲卡上的那種普通4鍵模擬手柄。針對其他的操縱桿及新型USB手柄/操縱桿,還希望有條件的朋友自己去測試一下。