[MFC][DirectInput]MFC下DirectX DirectInput的实现(有部分删改)

 

 

 

一般来说DirectX技术总是应用在游戏上的,而在DirectX天生就能与Win32很好的结合。看看市面上的图书,凡是用到DirectX技术的大多是使用Win32编程的,因为DirectDraw或者DirectXGraphics需要自己控制屏幕上的每一个象素,MFC等类库显然不适合太多自定义的东西。而另一方面,对于DirectInput里的内容,因为并不需要控制窗体,仅仅需要发送控制消息,则可以实现在MFC下的DirectInput编程。

  下面简要说明一下一个DirectInput在MFC下的实现,其中也包括DirectInput的一些基础知识。

  1.简介

  DirectInput和其他DirectX组成部分一样,是通过硬件抽象层(HAL)硬件仿真层(HEL)来实现,但是一般来说,所有的游戏控制器都有合适的驱动程序来支持,所以一般来说硬件仿真层的内容并不多,大多数的控制是通过DirectX调用驱动程序来完成的。

  DirectInput处理的输入设备一般包括鼠标、键盘、游戏控制杆、操作杆等。而且现在的DirectInput还能够支持力反馈设备。本文仅仅讲述如何实现一个普通的PS游戏控制手柄在MFC中的应用,因为所有的输入设备的实现一般都大同小异,具体的力反馈设备,可以在SDK中查询。

  2.组件

  DirectInput8.0版中包括很多COM接口,而最主要的接口有两个

  IDirectInput8:启动DirectInput所必须创建的主COM接口。创建了这个接口,才可以创建其他的接口,设置DirectInput属性,创建或者获得想要的输入设备。

  IDirectInputDevice8:由IDirectInput8接口创建而的,表示具体的输入设备。

  3.实现

  创建一个基于对话框的MFC应用程序。项目名称为DirectInputJS(JS表示joystick)其他使用默认设置。删除对话框中间的标签,按钮可以先不管,因为这不是本文的重点。

  现在在解决方案中添加一个一般 C++类,来封装DirectInput 中的Joystick。类名填选Joystick,没有基类,虚析构函数,完成。

  首先,在Joystick.h的头文件里补充一些声明:

  #pragma once

  #define DIRECTINPUT_VERSION 0x0800

  #include <dinput.h>


  注意声明版本在声明dinput.h头文件之前,因为如果先声明dinput.h,则这里的版本定义将因为dinput.h中已经声明而产生错误。这里需要说明的是SDK版本为DirectX9.0,而DirectInput版本为8.0。

  需要说明的是,这里的版本定义并不是必须的,只是编译器会产生一个没什么影响的警告。在dinput.h里有这么一段代码:

  #define DIRECTINPUT_HEADER_VERSION 0x0800

  #ifndef DIRECTINPUT_VERSION

  #define DIRECTINPUT_VERSION DIRECTINPUT_HEADER_VERSION

  #pragma message(__FILE__ ": DIRECTINPUT_VERSION undefined. Defaulting to version 0x0800")

  #endif

  所以如果你不声明版本的话,会给出你一个warning警告,不过也没什么关系,因为你确定使用的8.0的版本而不是其他版本,除非你需要使用其他版本,那么你才需要注意这点。

  下面开始把我们的新的类逐渐补充完整。在public下,声明如下的成员变量:

  //一般的成员变量

  HINSTANCE m_hInstance; // 实例句柄

  HWND m_hWnd; // 窗口句柄

  LPDIRECTINPUT8 m_lpDI; // DI8接口指针

  LPDIRECTINPUTDEVICE8 m_lpDIDevice; // DIDevice8接口指针

  DIJOYSTATE m_diJs; //存储Joystick状态信息

  GUID JoystickGUID; //GUID


  这些变量的意义都说明的很清楚,后面我们用到的时候,我会更加详细的说明每个变量的使用。下面我们也声明所需要用到的一些基本的成员函数,如下:


 

  //成员函数
  bool Initialise(void); //初始化函数

  //枚举设备

    //枚举到设备后都会调用此函数
  static BOOL CALLBACK DIEnumDevicesCallback(const DIDEVICEINSTANCE* lpddi, VOID* pvRef); 

 

    //枚举对象

    //枚举到对象后会调用此函数
  static BOOL CALLBACK EnumObjectsCallback( const DIDEVICEOBJECTINSTANCE* pdidoi, VOID* pContext );

  HRESULT PollDevice(void); // 轮循设备函数,在轮循过程中读取设备状态


  同样以后我们也会慢慢用到这些函数,到时候再详细的讲解他们的使用。这里强调的是这些都是一些基本的成员函数,你完全可以根据自己的应用添加其他的内容,只要你理解了DirectInput是如何工作的。

  先来看看构造和析构函数的定义:

Joystick::Joystick(void)

{
m_lpDIDevice = NULL;
m_lpDI = NULL;
m_hWnd = NULL;
m_hInstance = GetModuleHandle(NULL) ; //获取实例句柄

//The GetModuleHandle function retrieves a module handle for the specified module if the file has been mapped into the address space of the calling process.

//If this parameter is NULL, GetModuleHandle returns a handle to the file used to create the calling process.
}


Joystick::~Joystick(void)
{
if(m_lpDIDevice)
   m_lpDIDevice->Unacquire();
//释放DI和DIDevice对象,注意释放顺序
if(m_lpDIDevice)
{
   m_lpDIDevice->Release();
   m_lpDIDevice = NULL; 
}
if(m_lpDI)
{
  m_lpDI->Release();
  m_lpDI = NULL; 
}
}


  需要说明的是析构函数。输入设备的使用是需要获取的,当进行一系列的设置以后,应该使用IDirectInputDevice8::Acquire()来获取设备或者重获取设备(Acquire)。当不再需要设备了,则应该先归还(Unacquire)设备,然后再释放设备接口,最后再释放IDirectInput8接口。取消设备的获取,使用IDirectInputDevice8::Unacquire ()成员函数,释放设备接口资源使用IDirectInputDevice8::Release()成员函数,释放IDirectInput8接口,使用IDirectInput8::Release()成员函数。另外需要注意的是,在归还设备前进行释放,将是破坏性的,所以一定要先归还,再释放。

 下面定义bool Joystick::Initialise(void )函数。这个函数的目的是进行类的初始化,为了简单起见,我们把所有的必要的设置都放在初始化里面。这样在外部定义一个类以后,我们只使用一个初始化函数就完成所有的设置。

  首先定义一个HRESULT hr做为一些设置的返回值。

  HRESULT hr;


  然后建立DirectInput接口

//建立DI8接口

if(NULL == m_lpDI)

{

hr = DirectInput8Create(m_hInstance,

DIRECTINPUT_VERSION, 

IID_IDirectInput8,

(void**)&m_lpDI, //接口取值

NULL); 

if FAILED(hr) 

{

OutputDebugString("Create 失败 - in CDIJoystick::Initialise/n");

return false;

}

}


  唯一需要说明的函数是DirectInput8Create(…)。先来看一下函数在dinput.h中的定义:

HRESULT WINAPI DirectInput8Create(

HINSTANCE hinst, //程序的主实例

DWORD dwVersion, //DirectInput版本

REFIID riidltf, //IID的引用

LPVOID *ppvOut, //接口取值,这个指针是保存接口的,所以双指针类型。

LPUNKNOWN punkOuter //系统保留,一般为NULL

);


  这里首先需要讲解的是IID。在微软的COM编程里,每一个COM对象以及接口都必须有一个128位的标识符,用户可以通过这个标识符来申请对象或者接口。对于对象,这个标识符称为GUID(Globally Unique Identifiers,全局唯一标识符)。对于接口,这个标识符称为IID(Interface ID,接口标识符)。

  对于上面的DirectInput8Create 函数,我们只需要传递IID_IDirectInput8常量给函数就可以。这个常量是在dinput.h中定义的。其中,程序是否支持UNICODE,IID_IDirectInput8的值是不同的,不过我们不需要考虑这些。

  另外的一个参数LPVOID *ppvOut是保存接口的指针。我们把成员变量m_lpDI赋值进去,这样就可以把接口保存下来。

  punkOuter是系统保留的参数,一般设置为NULL就可以了。

  hr保存了函数返回的信息,可以使用FAILED()或者SUCCEEDED()宏来判断是否成功。

  OutputDebugString(…)可以用来在调试栏里输出信息,便于以后的调试。

  接下来我们需要枚举(Enumeration)设备。枚举设备是游戏手柄特有的部分。它使用了一个回调函数,对于每一个检测到的设备,调用回调函数来处理相关的信息。说简单点,比如现在你的系统上有二三个游戏手柄设备,那么枚举函数可以依次检测这些设备,或者检测到一个就停止。检测到以后,Windows调用回调函数来处理这个设备的信息,比如你可以设置一个数据结构来保存所有的设备信息,或者其他你所希望的处理。一般来说,你应该在回调函数中处理设备的GUID,这样你才可以通过这个GUID来创建游戏手柄设备。枚举函数的原型为:

  HRESULT EnumDevices(

  DWORD dwDevType , //扫描的设备类型

  LPDIENUMCALLBACK lpCallback , //指向回调函数的指针

  LPVOID pvRef , //32位指针,用来存储你所需要的信息,这个指针将传递给回调函数

  DWORD dwFlags //枚举扫描的控制标志

  ) ;

 对于扫描的设备类型,在DirectInput8.0以上的版本主要有以下几种选择,你可以讲这些选择或(OR)起来进行你所需要的组合:

  #define DI8DEVCLASS_ALL 0 扫描所有设备

  #define DI8DEVCLASS_DEVICE 1 扫描未知设备

  #define DI8DEVCLASS_POINTER 2 扫描鼠标,轨迹球,触摸点

  #define DI8DEVCLASS_KEYBOARD 3 扫描键盘

  #define DI8DEVCLASS_GAMECTRL 4 扫描游戏控制器


  其他的一些信息请参考SDK,这里我们传递给枚举设备函数DI8DEVCLASS_GAMECTRL,用来识别我们的游戏手柄。

  参数lpCallback为回调函数的函数名,这样枚举函数就知道如何去通过Windows来调用函数,做出相应的处理了。pvRef为32位的指针,来返回信息值。注意它的类型为LPVOID,所以你可以自定义任意的一个数据结构,然后传递这个结构的指针给枚举函数。在回调函数中,这个指针也将作为一个参数传递过去,然后你可以在回调函数中对这个指针指向的数据结构进行修改或其他的操作,来完成你所要完成的任务。

  dwFlags控制枚举函数如何扫描,是扫描所有设备,还是扫描安装和连接好的设备,还是扫描力反馈设备。dwFlags的取值如下:

  #define DIEDFL_ALLDEVICES 0x00000000 //扫描所有设备

  #define DIEDFL_ATTACHEDONLY 0x00000001 //扫描安装和连接好的设备

  #define DIEDFL_FORCEFEEDBACK 0x00000100 //扫描力反馈设备


  这里我们选择扫描安装和连接好的设备DIEDFL_ATTACHEDONLY 。最后的代码如下:

  hr = m_lpDI->EnumDevices( DI8DEVCLASS_GAMECTRL, 

  DIEnumDevicesCallback, //回调函数

  &JoystickGUID, //赋值GUID

  DIEDFL_ATTACHEDONLY ); //扫描安装好的和连接好的设备


  if FAILED(hr) 

  {

  OutputDebugString("枚举设备失败 - in CDIJoystick::Initialise/n");

  return false; 

  }


  DIEnumDevicesCallback为回调函数的函数名,在回调函数中,我们将传递JoystickGUID 的地址过去,然后把检测到的游戏手柄的GUID保存下来,为我们以后申请COM对象、创建设备做准备。

  下面我们把回调的成员函数完成:

  BOOL CALLBACK Joystick::DIEnumDevicesCallback(const DIDEVICEINSTANCE* lpddi, VOID* pvRef)

  {

  *(GUID*) pvRef = lpddi->guidInstance;

//lpddi为系统枚举后回调函数获取的设备信息结构体的指针,pvRef 为枚举函数传递进来的数据指针,再次将设备的GUID实例传递给全局变量JoystickGUID

  return DIENUM_STOP; //扫描后停止

  }


  别看这个函数仅仅几行,要注意的东西可不少。首先,在声明中我们将它声明为静态成员函数。使用了static关键字。静态成员函数与普通的成员函数的最大的区别就在于,这个函数不属于任何一个实际的对象,而是属于整个类的。如果你对C++很熟悉的话,这个并不难理解。想想枚举回调的原理,系统或者说你定义的类枚举到每一个设备,自动调用回调函数来处理,这里回调函数并不属于任何一个最后你所定义的对象,而属于系统。如果在一般的Win32程序中,回调函数一般是声明成一个全局的函数,然后任意调用的,那里将不需要任何static声明。

  接下来是BOOL,在dinput.h中,BOOL有如下的定义:

  typedef BOOL (FAR PASCAL*LPDIENUMDEVICESCALLBACKW)(LPCDIDEVICEINSTANCEW,LPVOID); //Unicode下


  所以你得用BOOL来作为函数的返回值。


  下面要说明的是一个数据结构DIDEVICEINSTANCE,它的Unicode下5.0以上版本的定义如下:

  typedef struct DIDEVICEINSTANCEW {

  DWORD dwSize; //结构大小

  GUID guidInstance; //设备的GUID实例,这个是我们所需要的

  GUID guidProduct; //设备产品的GUID

  DWORD dwDevType; //扫描的设备类型,和枚举函数中定义的一样

  WCHAR tszInstanceName[MAX_PATH]; //实例名称字符串

  WCHAR tszProductName[MAX_PATH]; //产品名称字符串

  GUID guidFFDriver; //力反馈驱动GUID

  WORD wUsagePage; //高级参数

  WORD wUsage; //高级参数

  } DIDEVICEINSTANCEW, *LPDIDEVICEINSTANCEW;


  这里最主要的就是guidInstance了,它是你创建设备所必须的。

  接下来是VOID* pvRef,它是一个32位指针,你可以通过它把所进行的操作保存下来。上面的例子中,在枚举函数里传递了JoystickGUID的地址到回调函数中,然后在回调函数中把扫描到的设备的GUID保存下来。 *(GUID*) pvRef = lpddi->guidInstance;

  这个语句首先把pvRef转换为(GUID*)类型,然后使用*来赋值。

  对于回调函数如何执行下面的操作,有两种方式:

  #define DIENUM_STOP 0 //扫描后停止

  #define DIENUM_CONTINUE 1 //扫描后继续


  以上是枚举函数和回调函数的一些基本内容,当然你可以加入更多自己的操作在回调函数里面。

  上面的例子中,我们枚举一个设备以后,在回调函数中保存了设备的GUID,然后立刻停止继续枚举扫描,来简单的实现我们的功能,当然如果你有很多的设备的话,你可以依次扫描每一个设备,保存他们每一个的GUID。

  枚举和回调完成以后,就可以创建设备了。

//创建DI8设备

if(!m_lpDIDevice)

{

hr = m_lpDI->CreateDevice(JoystickGUID,&m_lpDIDevice,NULL);

//JoystickGUID为刚才枚举设备后得到的设备GUID,在回调函数中进行了保存

if FAILED(hr) 

{

OutputDebugString("创建设备失败 - in CDIJoystick::Initialise/n");

return false; 

}

}


  创建设备是不是很简单,只要使用LPDIRECTINPUT8接口的CreateDevice成员函数就可以完成。参数也很简单,第一个为设备的GUID,就是枚举和回调中所保存的那个GUID,第二个为设备接口的地址,来保存设备的接口,第三个参数为系统保留,一般使用NULL。

  接下来要进行一些设备的必要的设置,包括协作等级、数据格式以及游戏手柄特有的输入特性方面的设置

//设置协作等级—— 前台模式 | 独占模式

hr = m_lpDIDevice ->SetCooperativeLevel(m_hWnd,DISCL_FOREGROUND|DISCL_EXCLUSIVE);

if FAILED(hr) 

{

OutputDebugString("设置协作等级失败 - in CDIJoystick::Initialise/n");

return false; 

}

 首先介绍协作等级。设置协作等级函数原型为:

HRESULT SetCooperativeLevel(

HWND hwnd , //窗口句柄

DWORD dwFlags //协作标志

);


  对于协作标志,主要有以下一些选择:

  DISCL_BACKGROUND 后台模式:应用程序在前台和后台都能够使用DirectInput设备

  DISCL_FOREGROUND 前台模式:应用程序要求前台访问。如果应用程序转到后台,那么应用程序将失去对DirectInput设备的控制

  DISCL_EXCLUSIVE 独占模式:应用程序获得设备,则其他的程序将不能对其申请独占访问。但可以申请非独占访问。

  DISCL_NONEXCLUSIVE 非独占模式:应用程序请求非独占访问设备。

  前台协作等级表明只有应用程序在前台,或者换句话说,只有获得了输入的焦点,那么程序才能读取数据。如果程序到了后台,那么设备自动的丢失,或者不可用。

  后台协作等级表明无论在前台还是后台,程序都可以在任何时候读取数据,获得输入。

  独占模式防止其他的程序独占设备。事实上,如果你的程序使用了独占模式占用设备,并不表示其他的程序不能从设备读取数据。当一个程序独占了键盘的输入,DirectInput将禁止包括Windows键在内的所有的键盘消息,除了CTRL+ALT+DEL 和 ALT+TAB 这两种键盘消息。

  非独占模式表明其他的应用程序可以独占或非独占的获得设备。Windows键消息仍被禁止,以防止用户不小心跳出程序。

  上面的例子中我们选择了前台独占模式,所以将DISCL_FOREGROUND|DISCL_EXCLUSIVE作为dwFlags参数的值。

  接下来我们需要设置设备的数据格式。

//设置数据格式

hr = m_lpDIDevice->SetDataFormat( &c_dfDIJoystick);

if FAILED(hr) 

{

OutputDebugString("设置数据格式失败 - in CDIJoystick::Initialise/n");

return false; 

}


  设置数据格式需要调用IDIRECTINPUTDEVICE8::SetDataFormat()来完成。设置数据格式的函数原型为HRESULT SetDataFormat(LPCDIDATAFORMAT lpdf);

  lpdf是指向数据格式结构的指针。关于数据格式结构DIDATAFORMAT,这里不详细介绍了,可以参考SDK。DirectInput为我们设置了几种常规的数据格式,我们可以简单的利用它们来完成数据格式的设置。

  c_dfDIMouse 通用鼠标

  c_dfDIMouse2 通用鼠标2,在7.0以上版本使用

  c_dfDIKeyboard 通用键盘

  c_dfDIJoystick 通用游戏杆

  c_dfDIJoystick2 通用游戏杆2,一般指力反馈设备


  这里我们需要使用普通的游戏手柄,所以把c_dfDIJoystick的地址作为参数传递过去。

  对于游戏手柄,我们还要设置它的输入特性。这里首先要对游戏手柄类型做一个简单的说明。前面我们并没有具体的区分游戏手柄和游戏杆,其实他们是有区别的。游戏手柄指我们常见的那种PS手柄,它的方向键也是一些电平开关。而游戏杆是摇杆式的手柄,属于一个模拟设备,在移动方向杆的时候输出的是一系列连续的值。如果你使用的是游戏手柄的话,因为它的方向仅仅是一些电平开关,那么读取它们的数据,你很容易就识别按键。如果使用的是游戏杆的话,那么读取出的连续的值到底表示什么意思,你必须提前设定。比如你可以设定X轴范围为-1024~+1024,Y轴范围为-128~+128,一切取决于你自己的意愿。

 设置游戏杆任何特性,包括游戏杆范围、相对或绝对数据格式、死区、最小灵敏度等,都使用SetPorperty()函数完成。函数原型为:

  HRESULT SetProperty(

  REFGUID rguidProp, //所要改变属性的GUID

  LPCDIPROPHEADER pdiph //指向属性头结构的指针,包含一些需要修改的信息细节

  );


  对于诸多的特性,一般来说主要设置游戏手柄的范围就可以了。对于游戏杆,可能还需要设置死区。如果想了解更多的特性,请参考SDK。下面我们首先需要了解几个数据结构。

typedef struct DIPROPHEADER { //属性头部结构

DWORD dwSize; //结构大小

DWORD dwHeaderSize; //属性头部结构大小

DWORD dwObj; //对象

DWORD dwHow; //存取方式

} DIPROPHEADER, *LPDIPROPHEADER;

typedef struct DIPROPRANGE { //属性轴范围的结构

DIPROPHEADER diph;

LONG lMin; //轴最小值

LONG lMax; //轴最大值

} DIPROPRANGE, *LPDIPROPRANGE;

typedef struct DIPROPDWORD { //属性死区设置结构

DIPROPHEADER diph;

DWORD dwData; //死区范围值

} DIPROPDWORD, *LPDIPROPDWORD;


  属性头部的结构用来说明一些基本信息。比如属性结构的大小,头部大小,要设置的对象以及存取方式是绝对数据还是相对的偏移。

  属性轴范围结构用来表示属性轴的范围,第一个参数为属性头部,后两个参数分别表示轴最小值和最大值。

  属性死区设置结构用来设置死区,当然如果你使用的仅仅是电平式的游戏手柄,那么这个是不必要的。第一个参数是属性头部,第二个参数是死区范围值。对于死区,可能你还不太了解。游戏杆的方向摇杆是一个模拟设备,在摇杆的中间地带,应该是没有输出的,这样你才可能控制你的飞机或人物停下来不动。对于中间地带的中心往外一个小的范围,游戏杆都应该是没有输出的,所以你必须定义这个中间的小的范围。死区的值用0~10000之间的绝对值表示,所以如果你要定义中间10%的范围为死区,那么dwData的值应该为1000。

  枚举对象函数和枚举函数类似枚举函数枚举连接到系统上的设备,枚举对象函数枚举设备中每一个对象,比如设备中的轴、按钮、滑杆等等。这里我们简单的使用枚举对象函数来设置轴的特性。

hr = m_lpDIDevice->EnumObjects(EnumObjectsCallback, (VOID*)this, DIDFT_ALL );

//(VOID*)this传递进当前DirectInput类对象指针

 

 

//枚举设备中的对象,对设备中的对象进行特性设定

if FAILED(hr)

{

OutputDebugString("枚举对象失败 - in CDIJoystick::Initialise/n");

return false; 

}


  和枚举函数类似,在声明中枚举对象函数需要声明为static类型,返回值仍然为BOOL型。参数第一个为枚举对象的回调函数名,第二个为传递给回调函数的指针,可以在这个指针指向的结构中保存所修改的信息。第三个为枚举方式,主要有一下的一些标志选项。

  #define DIDFT_ALL 0x00000000 //所有的对象

  #define DIDFT_AXIS 0x00000003 //所有的轴

  #define DIDFT_BUTTON 0x0000000C //所有的按钮

  #define DIDFT_ANYINSTANCE 0x00FFFF00 //所有的实例

 我们把整个的对象传递到回调函数里,枚举方式选择DIDFT_ALL。然后在枚举对象函数里设置对象的属性。

BOOL CALLBACK Joystick::EnumObjectsCallback( const DIDEVICEOBJECTINSTANCE* pdidoi, VOID* pContext )

{

HRESULT hr;

CJoystick * js = (CJoystick*)pContext; //首先取得JS对象指针

//pContext 传递进来的是当前DirectInput类对象指针

//设置游戏杆输入特性

if( pdidoi->dwType & DIDFT_AXIS ) //如果枚举的对象为轴

//枚举所有对象,然后对枚举对象进行判断,为轴时进行输入特性设置

{

DIPROPRANGE diprg; //设置轴范围结构

diprg.diph.dwSize = sizeof(DIPROPRANGE); 

diprg.diph.dwHeaderSize = sizeof(DIPROPHEADER); 

diprg.diph.dwHow = DIPH_BYID; 

diprg.diph.dwObj = pdidoi->dwType; // 枚举的轴

diprg.lMin = -1024; //最小值

diprg.lMax = +1024; //最大值

// 设置轴范围 

hr = js->m_lpDIDevice->SetProperty( DIPROP_RANGE, &diprg.diph);

 

  //HRESULT SetProperty(

    //REFGUID rguidProp, //所要改变属性的GUID

  //LPCDIPROPHEADER pdiph //指向属性头结构的指针,包含一些需要修改的信息细节

  //);



if( FAILED(hr)) 

{

OutputDebugString("设置轴范围失败 - in CDIJoystick::EnumObjectsCallback/n");

return DIENUM_STOP;

}

//设置死区属性,如果你使用的是电平式的游戏手柄,需要注释掉一下部分

DIPROPDWORD dipdw; //死区结构

dipdw.diph.dwSize = sizeof( dipdw ); 

dipdw.diph.dwHeaderSize = sizeof( dipdw.diph ); 

diprg.diph.dwObj = pdidoi->dwType; // 枚举的轴

dipdw.diph.dwHow = DIPH_DEVICE;

dipdw.dwData = 1000; //10%的死区

hr = js->m_lpDIDevice->SetProperty(DIPROP_DEADZONE, &dipdw.diph);

if( FAILED(hr)) 

{

OutputDebugString("设置死区失败 - in CDIJoystick::EnumObjectsCallback/n");

return DIENUM_STOP;

}

}

return DIENUM_CONTINUE;

}


  回调函数首先得到类对象的指针,判断扫描到的对象类型。本例中只设置了轴的属性,其实也可以在枚举对象的回调函数中扫描方式使用DIDFT_AXIS标志,例子目的在于说明如何依次设置手柄的各个对象属性。后面的代码设置每个轴的范围在-1024~+1024,如果是游戏杆的话,那么还设置了10%的死区。回调函数的返回值有一下两种:

  #define DIENUM_STOP 0 //停止扫描

  #define DIENUM_CONTINUE 1 //继续扫描


  注意如果你使用的游戏手柄,一定要注释掉设置死区的部分,因为如果仍然对手柄设置死区,那么不会成功,回调函数就会返回DIENUM_STOP,后面也不会再枚举对象,所以有的轴并没有设置成功。甚至,在初始化的函数里,因为hr的错误代码导致初始化函数Initialise()返回了false,以后的代码将因此而失效。

 所有的设备设置都妥当了以后,就应该获取游戏手柄了。获取游戏手柄使用Acquire()函数。在游戏手柄的使用中,还需要用到一个重要的组成部分——轮循(Poll)。

  轮循是因为手柄的驱动程序时序的产生中断,需要我们时序的检测手柄的状态。轮循由函数Poll()来实现。下面实现了类中的轮循:

HRESULT Joystick::PollDevice(void)

{

HRESULT hr;

if( NULL == m_lpDIDevice ) //未获得设备

return S_OK;

hr = m_lpDIDevice->Poll(); // 轮循设备读取当前状态

if( FAILED(hr) ) 

{

// 输入流中断,不能通过轮循获得任何状态值。

// 所以不需要任何重置,只要再次获得设备就行。


hr = m_lpDIDevice->Acquire();

while( hr == DIERR_INPUTLOST )

{

static int iCount = 0;

if (iCount>30) exit(-1); //累积30次获取设备失败,退出程序。

iCount++;

OutputDebugString("丢失设备,轮循失败 - in CJoystick::PollDevice/n");

hr = m_lpDIDevice->Acquire();

if( SUCCEEDED(hr) ) iCount = 0; 

} // hr也许为其他的错误.

return S_OK; 

}

// 获得输入状态,存储到成员变量 m_diJs 中

if( FAILED( hr = m_lpDIDevice->GetDeviceState( sizeof(DIJOYSTATE), &m_diJs ) ) )

//可将数据结构更改为DIJOYSTATE2

//The first parameter is the size of the structure in which the data is returned, and the second parameter is the address of this structure, which is of type DIJOYSTATE2. This structure holds data for up to six axes, 128 buttons, and four point-of-view hats. The sample program proceeds to view the state of all axes and buttons, and one slider. This information is then displayed in the main dialog window.Joystick buttons work just like keys or mouse buttons. If the high bit of the returned byte is 1, the button is pressed.

return hr; // 在轮循过程中设备将为 已获得 状态

return S_OK;

}


  如果轮循失败,则再次获取设备;如果获取失败,则检测失败类型,重复获取设备。如果错误连续出现30次(实际为31次),则退出程序。DIERR_INPUTLOST表示设备输入丢失,并且下一次调用的时候未获得,需要重新获取。其他类型的错误信息请参考SDK。

  再往后你只需要读取游戏手柄状态并把他们存储到你的DIJOYSTATE类型成员变量m_diJs中去了。应用的函数为GetDeviceState(),原型为:

HRESULT GetDeviceState(

DWORD cbData; //存储到的结构的大小

LPVOID lpvData //存储的成员变量的指针

);


  至此,你的游戏手柄类已经基本完成了,如果你需要什么其他的功能或者需要进行其他的设置,你可以自己去添加和修改。现在你的类已经可以为你的程序工作了,你接下来应该做的就是在你的MFC程序中声明一个类的对象并调用他,然后检测状态并完成相关的输出。

  在DirectInputJSDlg.cpp文件中加入自定义类的头文件#include "Joystick.h"。

  然后在所有函数的开始声明一个全局变量Joystick joystick。

 然后在函数BOOL CDIJoystickDlg::OnInitDialog()的最后添加如下代码:

joystick.m_hWnd = m_hWnd; //首先获得窗口句柄

if(!joystick.Initialise()) //初始化

{

OutputDebugString("初始化游戏杆失败 - in CDIJoystickDlg::OnInitDialog/n");

OnCancel();

return FALSE;

}

SetTimer(1,50,NULL); //设置一个50毫秒定时器


  首先需要把窗口句柄赋值到m_hWnd中,因为在设置协作等级的时候需要用到这个句柄。然后进行初始化。最后设置一个50毫秒的定时器,它会产生WM_TIMER消息。

  在CDirectInputJSDlg类的属性框中点击重写标签添加CDirectInputJSDlg::OnCancel()函数。加入如下代码:

KillTimer(1); //销毁定时器

OutputDebugString("程序退出 - OnCancel()/n");


  在CDirectInputJSDlg类的属性框中点击消息标签,在WM_TIMER消息中选择添加OnTimer()函数,将下面代码写入函数:

char ch[20];

if(FAILED( joystick.PollDevice() ) ) //轮循

{

KillTimer( 1 ); 

MessageBox(TEXT("读取设备状态错误") /

TEXT("程序即将退出"), TEXT("DirectInput 示例"), 

MB_ICONERROR | MB_OK );

}

if (joystick.m_diJs.lX < 0 )

OutputDebugString(" 方向左键按下 /n");

if (joystick.m_diJs.lX > 0 )

OutputDebugString(" 方向右键按下 /n");

if (joystick.m_diJs.lY < 0 )

OutputDebugString(" 方向上键按下 /n");

if (joystick.m_diJs.lY > 0 )

OutputDebugString(" 方向下键按下 /n");

for(int i = 0; i < 32 ; i++)

if (joystick.m_diJs.rgbButtons[i] & 0x80)

{

StringCchPrintf(ch,20,"按钮 %d 键按下/n",i);

OutputDebugString(ch);

}

 

======================================================

typedef struct DIJOYSTATE {
    LONG lX;
    LONG lY;
    LONG lZ;
    LONG lRx;
    LONG lRy;
    LONG lRz;
    LONG rglSlider[2];
    DWORD rgdwPOV[4];
    BYTE rgbButtons[32];
} DIJOYSTATE, *LPDIJOYSTATE;

Members

lX
X-axis, usually the left-right movement of a stick.
lY
Y-axis, usually the forward-backward movement of a stick.
lZ
Z-axis, often the throttle control. If the joystick does not have this axis, the value is 0.
lRx
X-axis rotation. If the joystick does not have this axis, the value is 0.
lRy
Y-axis rotation. If the joystick does not have this axis, the value is 0.
lRz
Z-axis rotation (often called the rudder). If the joystick does not have this axis, the value is 0.
rglSlider
Two additional axes, formerly called the u-axis and v-axis, whose semantics depend on the joystick. Use the IDirectInputDevice8::GetObjectInfo method to obtain semantic information about these values.
rgdwPOV
Direction controllers, such as point-of-view hats. The position is indicated in hundredths of a degree clockwise from north (away from the user). The center position is normally reported as –1; but see Remarks. For indicators that have only five positions, the value for a controller is –1, 0, 9,000, 18,000, or 27,000.
rgbButtons
Array of buttons. The high-order bit of the byte is set if the corresponding button is down, and clear if the button is up or does not exist.

=====================================================
  这里如果轮循失败,则销毁定时器,做出提示。然后判断joystick.m_diJs的各个成员。

  注意joystick.m_diJs.rgbButtons[i] & 0x80 ,判断手柄按钮的状态需要让它跟0x80相与。

  另外,如果要使用StringCchPrintf()函数,需要添加头文件,#include <strsafe.h>。 (使用StringCchPrintf()时,编译出错,未解决)

  现在离完成仅仅剩一步之遥了,你需要的是对环境的设置。

  右键点击工程,选择属性,在链接器,输入选项框中的附加依赖项中添加如下的库:

  dxguid.lib dxerr9.lib dinput8.lib comctl32.lib


  然后在菜单栏工具中选择选项,项目,VC++目录选项框中添加SDK的包含文件目录和库文件目录。

  最后你可以编译你的程序看看是否成功。在Debug模式下,可以在调试栏中看到输出信息。

  嗯,基本上就是这样。如果你需要对手柄的输入做更多的输出,那么你可以费费心思设计更好的程序。

阅读更多
想对作者说点什么? 我来说一句

没有更多推荐了,返回首页

加入CSDN,享受更精准的内容推荐,与500万程序员共同成长!
关闭
关闭