MFC下DirectX DirectInput的实现
一般来说DirectX技术总是应用在游戏上的,而在DirectX天生就能与Win32很好的结合。看看市面上的图书,凡是用到DirectX技术的大多是使用Win32编程的,因为DirectDraw或者DirectXGraphics需要自己控制屏幕上的每一个象素,MFC等类库显然不适合太多自定义的东西。而另一方面,对于DirectInput里的内容,因为并不需要控制窗体,仅仅需要发送控制消息,则可以实现在MFC下的DirectInput编程。 下面简要说明一下一个DirectInput在MFC下的实现,其中也包括DirectInput的一些基础知识。 #pragma once 注意声明版本在声明dinput.h头文件之前,因为如果先声明dinput.h,则这里的版本定义将因为dinput.h中已经声明而产生错误。这里需要说明的是SDK版本为DirectX9.0,而DirectInput版本为8.0。 需要说明的是,这里的版本定义并不是必须的,只是编译器会产生一个没什么影响的警告。在dinput.h里有这么一段代码: #define DIRECTINPUT_HEADER_VERSION 0x0800 所以如果你不声明版本的话,会给出你一个warning警告,不过也没什么关系,因为你确定使用的8.0的版本而不是其他版本,除非你需要使用其他版本,那么你才需要注意这点。 //一般的成员变量 这些变量的意义都说明的很清楚,后面我们用到的时候,我会更加详细的说明每个变量的使用。下面我们也声明所需要用到的一些基本的成员函数,如下: //成员函数 同样以后我们也会慢慢用到这些函数,到时候再详细的讲解他们的使用。这里强调的是这些都是一些基本的成员函数,你完全可以根据自己的应用添加其他的内容,只要你理解了DirectInput是如何工作的。 先来看看构造和析构函数的定义: Joystick::Joystick(void)
下面定义bool Joystick::Initialise(void )函数。这个函数的目的是进行类的初始化,为了简单起见,我们把所有的必要的设置都放在初始化里面。这样在外部定义一个类以后,我们只使用一个初始化函数就完成所有的设置。 HRESULT hr; 然后建立DirectInput接口 //建立DI8接口 唯一需要说明的函数是DirectInput8Create(…)。先来看一下函数在dinput.h中的定义: HRESULT WINAPI DirectInput8Create( 这里首先需要讲解的是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( 对于扫描的设备类型,在DirectInput8.0以上的版本主要有以下几种选择,你可以讲这些选择或(OR)起来进行你所需要的组合: #define DI8DEVCLASS_ALL 0 扫描所有设备 其他的一些信息请参考SDK,这里我们传递给枚举设备函数DI8DEVCLASS_GAMECTRL,用来识别我们的游戏手柄。 参数lpCallback为回调函数的函数名,这样枚举函数就知道如何去通过Windows来调用函数,做出相应的处理了。pvRef为32位的指针,来返回信息值。注意它的类型为LPVOID,所以你可以自定义任意的一个数据结构,然后传递这个结构的指针给枚举函数。在回调函数中,这个指针也将作为一个参数传递过去,然后你可以在回调函数中对这个指针指向的数据结构进行修改或其他的操作,来完成你所要完成的任务。 dwFlags控制枚举函数如何扫描,是扫描所有设备,还是扫描安装和连接好的设备,还是扫描力反馈设备。dwFlags的取值如下: #define DIEDFL_ALLDEVICES 0x00000000 //扫描所有设备 这里我们选择扫描安装和连接好的设备DIEDFL_ATTACHEDONLY 。最后的代码如下: hr = m_lpDI->EnumDevices( DI8DEVCLASS_GAMECTRL, DIEnumDevicesCallback为回调函数的函数名,在回调函数中,我们将传递JoystickGUID 的地址过去,然后把检测到的游戏手柄的GUID保存下来,为我们以后申请COM对象、创建设备做准备。 下面我们把回调的成员函数完成: BOOL CALLBACK Joystick::DIEnumDevicesCallback(const DIDEVICEINSTANCE* lpddi, VOID* pvRef) 别看这个函数仅仅几行,要注意的东西可不少。首先,在声明中我们将它声明为静态成员函数。使用了static关键字。静态成员函数与普通的成员函数的最大的区别就在于,这个函数不属于任何一个实际的对象,而是属于整个类的。如果你对C++很熟悉的话,这个并不难理解。想想枚举回调的原理,系统或者说你定义的类枚举到每一个设备,自动调用回调函数来处理,这里回调函数并不属于任何一个最后你所定义的对象,而属于系统。如果在一般的Win32程序中,回调函数一般是声明成一个全局的函数,然后任意调用的,那里将不需要任何static声明。 接下来是BOOL,在dinput.h中,BOOL有如下的定义: typedef BOOL (FAR PASCAL*LPDIENUMDEVICESCALLBACKW)(LPCDIDEVICEINSTANCEW,LPVOID); //Unicode下
typedef struct DIDEVICEINSTANCEW { 这里最主要的就是guidInstance了,它是你创建设备所必须的。 接下来是VOID* pvRef,它是一个32位指针,你可以通过它把所进行的操作保存下来。上面的例子中,在枚举函数里传递了JoystickGUID的地址到回调函数中,然后在回调函数中把扫描到的设备的GUID保存下来。 *(GUID*) pvRef = lpddi->guidInstance; 这个语句首先把pvRef转换为(GUID*)类型,然后使用*来赋值。 对于回调函数如何执行下面的操作,有两种方式: #define DIENUM_STOP 0 //扫描后停止 以上是枚举函数和回调函数的一些基本内容,当然你可以加入更多自己的操作在回调函数里面。 上面的例子中,我们枚举一个设备以后,在回调函数中保存了设备的GUID,然后立刻停止继续枚举扫描,来简单的实现我们的功能,当然如果你有很多的设备的话,你可以依次扫描每一个设备,保存他们每一个的GUID。 枚举和回调完成以后,就可以创建设备了。 //创建DI8设备 创建设备是不是很简单,只要使用LPDIRECTINPUT8接口的CreateDevice成员函数就可以完成。参数也很简单,第一个为设备的GUID,就是枚举和回调中所保存的那个GUID,第二个为设备接口的地址,来保存设备的接口,第三个参数为系统保留,一般使用NULL。 接下来要进行一些设备的必要的设置,包括协作等级、数据格式以及游戏手柄特有的输入特性方面的设置。 //设置协作等级―― 前台模式 | 独占模式 首先介绍协作等级。设置协作等级函数原型为: HRESULT SetCooperativeLevel( 对于协作标志,主要有以下一些选择: DISCL_BACKGROUND 后台模式:应用程序在前台和后台都能够使用DirectInput设备 DISCL_FOREGROUND 前台模式:应用程序要求前台访问。如果应用程序转到后台,那么应用程序将失去对DirectInput设备的控制 DISCL_EXCLUSIVE 独占模式:应用程序获得设备,则其他的程序将不能对其申请独占访问。但可以申请非独占访问。 DISCL_NONEXCLUSIVE 非独占模式:应用程序请求非独占访问设备。 前台协作等级表明只有应用程序在前台,或者换句话说,只有获得了输入的焦点,那么程序才能读取数据。如果程序到了后台,那么设备自动的丢失,或者不可用。 后台协作等级表明无论在前台还是后台,程序都可以在任何时候读取数据,获得输入。 独占模式防止其他的程序独占设备。事实上,如果你的程序使用了独占模式占用设备,并不表示其他的程序不能从设备读取数据。当一个程序独占了键盘的输入,DirectInput将禁止包括Windows键在内的所有的键盘消息,除了CTRL+ALT+DEL 和 ALT+TAB 这两种键盘消息。 非独占模式表明其他的应用程序可以独占或非独占的获得设备。Windows键消息仍被禁止,以防止用户不小心跳出程序。 上面的例子中我们选择了前台独占模式,所以将DISCL_FOREGROUND|DISCL_EXCLUSIVE作为dwFlags参数的值。 接下来我们需要设置设备的数据格式。 //设置数据格式 设置数据格式需要调用IDIRECTINPUTDEVICE8::SetDataFormat()来完成。设置数据格式的函数原型为HRESULT SetDataFormat(LPCDIDATAFORMAT lpdf); lpdf是指向数据格式结构的指针。关于数据格式结构DIDATAFORMAT,这里不详细介绍了,可以参考SDK。DirectInput为我们设置了几种常规的数据格式,我们可以简单的利用它们来完成数据格式的设置。 c_dfDIMouse 通用鼠标 这里我们需要使用普通的游戏手柄,所以把c_dfDIJoystick的地址作为参数传递过去。 对于游戏手柄,我们还要设置它的输入特性。这里首先要对游戏手柄类型做一个简单的说明。前面我们并没有具体的区分游戏手柄和游戏杆,其实他们是有区别的。游戏手柄指我们常见的那种PS手柄,它的方向键也是一些电平开关。而游戏杆是摇杆式的手柄,属于一个模拟设备,在移动方向杆的时候输出的是一系列连续的值。如果你使用的是游戏手柄的话,因为它的方向仅仅是一些电平开关,那么读取它们的数据,你很容易就识别按键。如果使用的是游戏杆的话,那么读取出的连续的值到底表示什么意思,你必须提前设定。比如你可以设定X轴范围为-1024~+1024,Y轴范围为-128~+128,一切取决于你自己的意愿。 设置游戏杆任何特性,包括游戏杆范围、相对或绝对数据格式、死区、最小灵敏度等,都使用SetPorperty()函数完成。函数原型为: HRESULT SetProperty( 对于诸多的特性,一般来说主要设置游戏手柄的范围就可以了。对于游戏杆,可能还需要设置死区。如果想了解更多的特性,请参考SDK。下面我们首先需要了解几个数据结构。 typedef struct DIPROPHEADER { //属性头部结构 属性头部的结构用来说明一些基本信息。比如属性结构的大小,头部大小,要设置的对象以及存取方式是绝对数据还是相对的偏移。 属性轴范围结构用来表示属性轴的范围,第一个参数为属性头部,后两个参数分别表示轴最小值和最大值。 属性死区设置结构用来设置死区,当然如果你使用的仅仅是电平式的游戏手柄,那么这个是不必要的。第一个参数是属性头部,第二个参数是死区范围值。对于死区,可能你还不太了解。游戏杆的方向摇杆是一个模拟设备,在摇杆的中间地带,应该是没有输出的,这样你才可能控制你的飞机或人物停下来不动。对于中间地带的中心往外一个小的范围,游戏杆都应该是没有输出的,所以你必须定义这个中间的小的范围。死区的值用0~10000之间的绝对值表示,所以如果你要定义中间10%的范围为死区,那么dwData的值应该为1000。 枚举对象函数和枚举函数类似,枚举函数枚举连接到系统上的设备,枚举对象函数枚举设备中每一个对象,比如设备中的轴、按钮、滑杆等等。这里我们简单的使用枚举对象函数来设置轴的特性。 hr = m_lpDIDevice->EnumObjects(EnumObjectsCallback, (VOID*)this, DIDFT_ALL ); 和枚举函数类似,在声明中枚举对象函数需要声明为static类型,返回值仍然为BOOL型。参数第一个为枚举对象的回调函数名,第二个为传递给回调函数的指针,可以在这个指针指向的结构中保存所修改的信息。第三个为枚举方式,主要有一下的一些标志选项。 #define DIDFT_ALL 0x00000000 //所有的对象 我们把整个的对象传递到回调函数里,枚举方式选择DIDFT_ALL。然后在枚举对象函数里设置对象的属性。 BOOL CALLBACK Joystick::EnumObjectsCallback( const DIDEVICEOBJECTINSTANCE* pdidoi, VOID* pContext ) 回调函数首先得到类对象的指针,判断扫描到的对象类型。本例中只设置了轴的属性,其实也可以在枚举对象的回调函数中扫描方式使用DIDFT_AXIS标志,例子目的在于说明如何依次设置手柄的各个对象属性。后面的代码设置每个轴的范围在-1024~+1024,如果是游戏杆的话,那么还设置了10%的死区。回调函数的返回值有一下两种: #define DIENUM_STOP 0 //停止扫描 注意如果你使用的游戏手柄,一定要注释掉设置死区的部分,因为如果仍然对手柄设置死区,那么不会成功,回调函数就会返回DIENUM_STOP,后面也不会再枚举对象,所以有的轴并没有设置成功。甚至,在初始化的函数里,因为hr的错误代码导致初始化函数Initialise()返回了false,以后的代码将因此而失效。 所有的设备设置都妥当了以后,就应该获取游戏手柄了。获取游戏手柄使用Acquire()函数。在游戏手柄的使用中,还需要用到一个重要的组成部分――轮循(Poll)。 HRESULT Joystick::PollDevice(void) 如果轮循失败,则再次获取设备;如果获取失败,则检测失败类型,重复获取设备。如果错误连续出现30次(实际为31次),则退出程序。DIERR_INPUTLOST表示设备输入丢失,并且下一次调用的时候未获得,需要重新获取。其他类型的错误信息请参考SDK。 再往后你只需要读取游戏手柄状态并把他们存储到你的DIJOYSTATE类型成员变量m_diJs中去了。应用的函数为GetDeviceState(),原型为: HRESULT GetDeviceState( 至此,你的游戏手柄类已经基本完成了,如果你需要什么其他的功能或者需要进行其他的设置,你可以自己去添加和修改。现在你的类已经可以为你的程序工作了,你接下来应该做的就是在你的MFC程序中声明一个类的对象并调用他,然后检测状态并完成相关的输出。 在DirectInputJSDlg.cpp文件中加入自定义类的头文件#include "Joystick.h"。 然后在所有函数的开始声明一个全局变量Joystick joystick。 然后在函数BOOL CDIJoystickDlg::OnInitDialog()的最后添加如下代码: joystick.m_hWnd = m_hWnd; //首先获得窗口句柄 首先需要把窗口句柄赋值到m_hWnd中,因为在设置协作等级的时候需要用到这个句柄。然后进行初始化。最后设置一个50毫秒的定时器,它会产生WM_TIMER消息。 在CDirectInputJSDlg类的属性框中点击重写标签添加CDirectInputJSDlg::OnCancel()函数。加入如下代码: KillTimer(1); //销毁定时器 在CDirectInputJSDlg类的属性框中点击消息标签,在WM_TIMER消息中选择添加OnTimer()函数,将下面代码写入函数: char ch[20]; 这里如果轮循失败,则销毁定时器,做出提示。然后判断joystick.m_diJs的各个成员。 注意joystick.m_diJs.rgbButtons[i] & 0x80 ,判断手柄按钮的状态需要让它跟0x80相与。 另外,如果要使用StringCchPrintf()函数,需要添加头文件,#include <strsafe.h>。 现在离完成仅仅剩一步之遥了,你需要的是对环境的设置。 右键点击工程,选择属性,在链接器,输入选项框中的附加依赖项中添加如下的库: dxguid.lib dxerr9.lib dinput8.lib comctl32.lib 然后在菜单栏工具中选择选项,项目,VC++目录选项框中添加SDK的包含文件目录和库文件目录。 最后你可以编译你的程序看看是否成功。在Debug模式下,可以在调试栏中看到输出信息。 嗯,基本上就是这样。如果你需要对手柄的输入做更多的输出,那么你可以费费心思设计更好的程序。 |
- 上一篇:做一个可编辑的表格控件
- 下一篇:windows编程通用的Win32类型和常见的结构