Войти
ПрограммированиеСтатьиОбщее

Управление устройствами ввода через Windows API и DirectX.

Автор: Михаил Гусаренко

Windows API
DirectInput

Windows API

Здраствуйте. Меня зовут Гусаренко Михаил (ник в форуме MO). В данный момент я вхожу в состав группы по разработке игровых проектов, и параллельно занимаюсь своим проектом универсального игрового движка. Моя программа создается на основе независимых библиотек, некоторые из которых я и буду периодически выкладывать.

В этой статье каждый может найти что-то полезное для себя, кто-то основы работы с устройствами, кто-то принципы организации данных. Можно написать кучу процедур которые будут пересылать друг другу данные, но такую систему будет сложно модифицировать и портировать под разные платформы.

Постановка задачи: Цель этой статьи заключена не сколько в том, чтобы показать как использовать разные интерфейсы для обработки данных поступающих в движок из внешних устройств, сколько в том,  что бы показать как это можно использовать совместно, т.е. создать универсальный класс для обработки данных с устройств, который осуществляет одну и ту же задачу разными способами. Это необходимо для создания наибольшей совместимости, и возможности быстрого переноса под платформы отличные от базисной.

Путь реализации: Создадим класс Управления, через который данные от устройств будут уходить дальше например DeviceCtrl. Этот класс будет содержать в себе виртуальные устройства такие как MouseDevice и KeyboardDevice а так же полученные из этих устройств данные. Общаться эти устройства будут с управляющем классом по средствам сообщений/меседжей и статусов устройства, которые содержаться в передаваемой структуре KeyboardInfo и MouseInfo для устройств соответственно. MouseDevice и KeyboardDevice это виртуальные классы с интерфейсной процедурой например ReadData, которая будет заполнять структуру данными считанными из непосредственно устройства. Отмечу, что буду описывать процесс создания библиотеки немного не последовательно, так что какие куска кода идут за какими вам придется догадываться самим.

Для чего всё это надо: Такая структура позволит создавать нам абсолютно независимые классы устройств с одинаковым интерфейсом. Теперь не важно что находиться в классе устройства, Наш менеджер всё равно это устройство признает и сможет прочитать из него данные.

И так, что мы имеем:

class KeyboardDevice
{
public:	virtual void ReadData(KeyboardInfo &Info)=NULL;
};

class MouseDevice
{
public:
	virtual void ReadData(MouseInfo &Info)=NULL;
};

class DeviceCtrl 
{
public:
	KeyboardInfo KeyboardInfo;
	MouseInfo    MouseInfo;

	KeyboardDevice *KeyboardDevice;
	MouseDevice    *MouseDevice;
};

При инициализации движка, относительно настроек KeyboardDevice и MouseDevice, получат значения тех или иных устройств.

Теперь перейдем к созданию информационных потоков MouseInfo и KeyboardInfo. Как уже говорилось основой этого патока будет сообщение, которое определяет что произошло, и имеет значение с чем произошло, или как произошло. Это похоже на систему Windows, только мы будем оперировать не отдельными меседжами, и блоками оных. Так же структура информации будет содержать и конкретный статус устройства.

Вот какие события могут у нас произойти:

enum InputDeviceEvent
{
	id_KeyUp,       //кнопку клавиатуры отпустили, в значении будет скан код клавиши
	id_KeyDown,     //кнопку клавиатуры нажали,    в значении будет скан код клавиши
	id_ButtonUp,    //кнопку мышки отпустили,   в значении будет номер клавиши мышки
	id_ButtonDown,  //кнопку мышки нажали,      в значении будет номер клавиши мышки
	id_MoveX,       //мышь переместилась по x,  в значении будет перемещение в 
	                //                          пикселях курсора мыши по x
	id_MoveY,       //мышь переместилась по y,  в значении будет перемещение в 
	                //                          пикселях курсора мыши по y
	id_MoveZ        //мышь переместилась по z,  в значении будет поворот скролла
};

//далее структура меседжа:

struct InputDeviceMessage
{
	InputDeviceEvent Event;  //сообщение
	int Value;               //значение
};

//Теперь объявим непосредственно структуры с потоками:

struct KeyboardInfo
{       //статус устройства
	char KeyState[256];      //положение клавиш

	//поток сообщений
	InputDeviceMessage *Msg; //произошедшие события
	int MsgNum;              //кол-во произошедших событий
	int MaxNum;              //Макс число событий
};

struct MouseInfo
{	//статус устройства
	char ButtonState[8];     //Положение кнопок
	int  Axis[3];            //Смещение по осям

	//поток сообщений
	InputDeviceMessage *Msg; //произошедшие события
	int MsgNum;              //кол-во произошедших событий
	int MaxNum;              //Макс число событий
};

Теперь, когда мы конкретно определились с типами данных допишем DeviceCtrl:

Тут я задаю в конструктор параметр MsgBufSize который характеризует максимальную длину потока, эта величена может задаваться где и как угодно. Жадничать с его значением не советую т.к. если буфер будет слишком мал, то некоторые данные от устройства не дойдут до получателя - просто не влезут. Сколько я не старался поток больше чем в 5 меседжей с обоих устройств при fps 85 я не выбил, так что значения 16 хватит вполне...

class DeviceCtrl
{
public:
	KeyboardInfo KeyboardInfo;
	MouseInfo    MouseInfo;

	KeyboardDevice *KeyboardDevice;
	MouseDevice    *MouseDevice;

	DeviceCtrl(int MsgBufSize)
	{
		//Изначально к менеджеру устройства не подключены поэтому
		KeyboardDevice =NULL;
		MouseDevice    =NULL;

		//Теперь выделим память для сообщений
		KeyboardInfo.MaxNum =MsgBufSize;
		KeyboardInfo.Msg    =(InputDeviceMessage*)malloc(MsgBufSize*sizeof(InputDeviceMessage));

		MouseInfo.MaxNum    =MsgBufSize;
		MouseInfo.Msg       =(InputDeviceMessage*)malloc(MsgBufSize*sizeof(InputDeviceMessage));
	};

	~DeviceCtrl()
	{
		//вернем память выделенную для меседжей
		free(KeyboardInfo.Msg);
		free(MouseInfo.Msg);
	};

	void UpdateInfo()
	{
		//Если устройство подключено тогда считаем из него данные
		if(KeyboardDevice) KeyboardDevice ->ReadData(KeyboardInfo);
		if(MouseDevice)    MouseDevice    ->ReadData(MouseInfo);
	};
};

С менеджером мы разобрались. Теперь приступим к созданию непосредственно устройств. Для начала через Windows API, основываясь на меседжах пришедших окну. Как известно wParam содержит виртуальный код клавиши, а нам нужен скан код. Для перевода создадим массив KeyCode и заполним его при инициализации. KeyState мы храним для выдачи его в статус устройства при опросе. WndHandle это хендл окошка сообщения которого мы перехватываем, если этот параметр равен NULL, то мы перехватим все сообщения которые пришли от Windows всем созданным окошкам. Важно понимать, что считывать данные таким образом мы должны до всем известной конструкции в WinMain()

while(PeekMessage(&Msg, 0, 0, 0, PM_REMOVE)) 
{ 
	TranslateMessage(&Msg);
	DispatchMessage(&Msg);
};

Иначе буфер сообщений Windows будет уже пустой. Можно конечно поменять флаг в WinMain() в процедуре PeekMessage() с PM_REMOVE на PM_NOREMOVE, тогда сообщения будут оставаться, и копиться там обрабатываясь по несколько раз. Это нам не надо.

class MSMessageKeyboard:public KeyboardDevice
{
private:
	HWND WndHandle;
	char KeyState[256];
	char KeyCode[256];

public:
	MSMessageKeyboard()
	{
		ZeroMemory(KeyState, 256);     //Поставим все клавиши в положение отпущено
		for(int i=0; i<256; i++)       //Заполним массив конвертации из VirtualKeyCode в ScanCode
			KeyCode[i]=MapVirtualKey(i, 0);
	};

	//инициализация
	void Init(HWND h_Wnd)
	{
		WndHandle=h_Wnd;
	};

	//деинициализация. DSK.
	void Done(){};

	//Процедура обработки
	void ReadData(KeyboardInfo &Info)
	{
		MSG Msg;  //Меседж от системы
		int m=0;  //Счетчик наших меседжей
		int k;

		//Пока есть сообщения на тему кнопок т.е. между WM_KEYFIRST и WM_KEYLAST, и буффер не кончился
		while(PeekMessage(&Msg, WndHandle, WM_KEYFIRST, WM_KEYLAST, PM_REMOVE) && m<Info.MaxNum) 
		{ 
			TranslateMessage(&Msg);  //Приведем сообщение в нормальный вид

			switch (Msg.message)     //Непосредственно обработчик
			{
			case WM_KEYDOWN:
				k=KeyCode[Msg.wParam];        //переведём из Virtual в Scan
				if(KeyState[k])break;         //если кнопка уже нажата то выйдем
				KeyState[k]=1;                //установим кнопку в нажатое положение в массиве состояний
				Info.Msg[m].Event=id_KeyDown; //Произошло событие кнопка вниз
				Info.Msg[m].Value=k;          //Какая кнопка нажата
				m++;                          //Увеличим кол-во отсылаймых меседжей
				break;

			case WM_KEYUP:                        //Аналогично WM_KEYDOWN
				k=KeyCode[Msg.wParam];
				if(!KeyState[k])break;
				KeyState[k]=0;
				Info.Msg[m].Event=id_KeyUp;
				Info.Msg[m].Value=k;
				m++;
				break;
			};
			//Тут можно поставить DispatchMessage(&Msg), тогда сообщение дойдет до процедуры
			//обратной связи окошка WndCallBackFunc, если таковая имеется.
		};
		
		memcpy(Info.KeyState, KeyState, 256); //Заполним буфер состояния клавиш
		Info.MsgNum=m; //укажем сколько сообщений отправляем менеджеру
	};
};

С клавиатурой всё понятно.

Теперь перейдем к мышке: Стоит отметить, что Windows API сильно ограничен в работе с мышкой, т.к. поддерживается всего 3 кнопки, и работа со скроллом практически отсутствует. Принцип тот же, что и с клавиатурой: Перехватим сообщение и сконвертируем его в удобный для себя стандарт, так что объясню лишь некоторые моменты.

class MSMessageMouse:public MouseDevice
{
private:
	char ButtonState[8]; //сотояние кнопок мышки
	HWND WndHandle;
	int ox, oy;          //Предыдущая позиция курсора
public:
	stMSMessageMouse()
	{
		ZeroMemory(ButtonState, 8);
	};

	void Init(HWND h_Wnd)
	{
		WndHandle=h_Wnd;
		//Возьмем начальную точку курсора
		tagPOINT p;
		GetCursorPos(&p);
		ox=p.x;
		oy=p.y;
	};

	void Done(){};

	void ReadData(MouseInfo &Info)
	{
		MSG Msg;
		int m=0;

		int lX, lY;
		while(PeekMessage(&Msg, WndHandle, WM_MOUSEFIRST, WM_MOUSELAST, PM_REMOVE) && m<Info.MaxNum)
		{ 
			TranslateMessage(&Msg);
			switch (Msg.message)
			{
			case WM_MOUSEMOVE:
				//Мы будем оперировать не с глобальными координатами курсора 
				//как делает винда, а с пиращениями координат в каждый тик.
				lX=LOWORD(Msg.lParam)-ox;
				lY=HIWORD(Msg.lParam)-oy;

				//заполним смещение по осям				
				Info.Axis[0]=lX;
				Info.Axis[1]=lY;
				Info.Axis[2]=0;

				//Если мышка изменила координату по x, то добавим соответствующее сообщение
				if(lX)
				{
					Info.Msg[m].Event=id_MoveX;
					Info.Msg[m].Value=lX;
					m++;
				};

				//Если мышка изменила координату по y, то добавим соответствующее сообщение
				if(lY)
				{
					Info.Msg[m].Event=id_MoveY;
					Info.Msg[m].Value=lY;
					m++;
				};

				//Запомним координаты курсора для вычисления пиращения в следующем кадре
				ox=LOWORD(Msg.lParam);
				oy=HIWORD(Msg.lParam);
				break;

//Для любителей Delphi6, там есть этот меседж и работает он только под NT, в cpp6 его нету. 
//  И как брать скролл я не знаю.
//			case WM_MOUSEWHEEL:
//				Info.Msg[m].Event=id_MoveZ;
//				Info.Msg[m].Value=HIWORD(Msg.wParam);
//				m++;
//				break;
//
			// Обработка кнопок. Поступаем точно так-же как и с клавиатурой, 
			// только тут для каждой кнопки свой меседж.
			case WM_LBUTTONDOWN:   
				ButtonState[0]=1;
				Info.Msg[m].Event=id_ButtonDown;
				Info.Msg[m].Value=0;
				m++;
				break;

			case WM_LBUTTONUP:
				ButtonState[0]=0;
				Info.Msg[m].Event=id_ButtonUp;
				Info.Msg[m].Value=0;
				m++;
				break;
			//Аналогично обрабатываются WM_RBUTTONUP, WM_RBUTTONDOWN, WM_MBUTTONUP и WM_MBUTTONDOWN..
			//для WM_RBUTTON Value=1, а для WM_MBUTTON Value=2.
		};
		
		memcpy(Info.ButtonState, ButtonState, 8); //Передаем состояние кнопок.
		Info.MsgNum=m;
	};
};
Страницы: 1 2 Следующая »

#DirectInput, #DirectX

4 апреля 2003

Комментарии [11]