netlib.narod.ru< Назад | Оглавление | Далее >

Использование устройств DirectInput

Microsoft проделала длинный путь, чтобы упростить работу с устройствами ввода. Она настолько проста, что фактически вы можете использовать один и тот же COM-интерфейс (IDirectInputDevice8) для взаимодействия практически с любым устройством ввода, которое вы себе можете представить (и, возможно, с теми, которые вы и представить не можете!). Шаги для создания и использования различных устройств ввода похожи, так что позвольте мне забежать вперед и показать, как это делается. В таблице 3.2 показаны общие этапы в правильном порядке вместе с интерфейсами DirectInput и функциями, которые данный этап реализуют.


Таблица 3.2. Этапы создания и использования устройства



Этап Интерфейс/Функция

1. Получение GUID устройства IDirectInput8::EnumDevices
2. Создание COM-объекта устройства IDirectInput8::CreateDevice
3. Установка формата данных IDirectInputDevice8::SetDataFormat
4. Установка уровня кооперации IDirectInputDevice8::SetCooperativeLevel
5. Установка различных специальных свойств IDirectInputDevice8::SetProperty
6. Захват устройства IDirectInputDevice8::Acquire
7. Опрос устройства IDirectInputDevice8::Poll
8. Чтение данных IDirectInputDevice8::GetDeviceState


Перед тем, как двигаться дальше, убедитесь, что вы объявили объект устройства DirectInput (IDirectInputDevice8):

IDirectInputDevice8 *pDIDevice;

Получение GUID устройства

У каждого установленного устройства есть GUID (Глобальный Уникальный Идентификатор) — присвоенное ему число. Чтобы использовать устройство надо сначала узнать его GUID. Проще всего это сделать для системной клавиатуры и мыши. DirectInput определяет их как GUID_SysKeyboard и GUID_SysMouse соответственно. Для всех других устройств вы должны сначала выполнить перечисление, чтобы найти то, которое вам требуется.

ПРИМЕЧАНИЕ
Перечисление (enumeration) — это процесс перебора элементов списка. В нашем случае элементы — это устройства ввода, такие как джойстики. Предположим, к вашей системе подключено пять джойстиков. В процессе перечисления DirectInput будет передавать вам информацию, относящуюся к каждому джойстику, по одному за раз, пока не будут перебраны все джойстики, или вы не решите остановиться.

Перечисление устройств — это работа функции IDirectInput8::EnumDevices:

HRESULT IDirectInput8::EnumDevices(
     DWORD            dwDevType,  // тип искомых устройств
     LPDIENUMCALLBACK lpCallback, // функция обратного вызова для перечисления
     LPVOID           pvRef,      // указатель на пользовательский набор данных
     DWORD            dwFlags);   // флаги перечисления

 

ПРИМЕЧАНИЕ
Чтобы использовать значения GUID_SysKeyboard и GUID_SysMouse вы должны определить константу INITGUID прежде всех других директив препроцессора или включить в проект библиотеку DXGuid.lib.
#define INITGUID
Вы должны определить INITGUID только один раз в вашем проекте (в одном файле с исходным кодом); если вы сделаете это несколько раз, то при компиляции возникнет ошибка.

Параметр dwDevType — это набор битовых полей, описывающих устройства каого типа будут перечисляться. Возможен диапазон от общих устройств ввода, таких как джойстики и мыши, до более специализированных, таких как трекболы и рукоятки управления. За полным списком устройств обратитесь к документации DX SDK, а в таблице 3.3. приведено несколько наиболее часто используемых значений.


Таблица 3.3. Типы перечисляемых устройств DirectInput



Значение Описание

DI8DEVCLASS_ALL Все устройства
DI8DEVCLASS_GAMECTRL Все игровые контроллеры (джойстики)
DI8DEVCLASS_KEYBOARD Все клавиатуры
DI8DEVCLASS_POINTER Все указывающие устройства (мыши)
DI8DEVCLASS_DEVICE Все устройства, не относящиеся к трем предыдущим типам
DI8DEVTYPE_MOUSE Мыши и подобные им устройства, такие как трекболы
DI8DEVTYPE_KEYBOARD Клавиатуры и подобные им устройства
DI8DEVTYPE_JOYSTICK Джойстики и подобные им устройства, такие как рули
DI8DEVTYPE_DEVICE Устройства, которые не относятся к трем предыдущим типам
DI8DEVTYPEMOUSE_TOUCHPAD Сенсорные панели (подтип)
DI8DEVTYPEMOUSE_TRACKBALL Трекболы (подтип)


Переменная lpCallback — это указатель на функцию перечисления, которая будет вызываться каждый раз, когда в системе обнаружено подходящее устройство. Я опишу ее чуть позже. В параметре pvRef вы задаете указатель на буфер, обычно это структура данных в которой вы храните информацию. При вызове функции перечисления ей будет передан этот указатель на предоставленную вами структуру (или на другие данные), что предоставляет возможность получить или установить данные.

Последний набор флагов, dwFlags, сообщает DirectInput как должно выполняться перечисление устройств. Вы можете указать в аргументе dwFlags любое значение из перечисленных в таблице 3.4.


Таблица 3.4. Флаги перечисления



Значение Описание

DIEDFL_ALLDEVICES Перечисляем все установленные устройства (значение по умолчанию)
DIEDFL_ATTACHEDONLY Перечисляем только подключенные устройства
DIEDFL_FORCEFEEDBACK Перечисляем только устройства с обратной связью
DIEDFL_INCLUDEALIASES Включаем устройства, которые являются псевдонимами других устройств
DIEDFL_INCLUDEPHANTOMS Включаем фантомные устройства (заглушки)


Ранее упомянутый указатель на функцию lpCallback должен указывать на определяемую пользователем функцию перечисления с типом DIEnumDeviceProc, которая должна соответствовать следующему прототипу:

BOOL CALLBACK DIEnumDevicesProc(
     LPDIDEVICEINSTANCE lpddi,  // структура данных устройства
     LPVOID             pvRef); // задаваемый пользователем указатель

Параметр lpddi — это указатель на структуру DIDEVICEINSTANCE, которая содержит информацию об обнаруженном в данном вызове устройстве. Вот как она выглядит:

typedef struct {
    DWORD dwSize;       // Размер структуры
    GUID  guidInstance; // GUID устройства
    GUID  guidProduct;  // Предоставляемый OEM GUID устройства
    DWORD dwDevType;    // Тип устройства
    TCHAR tszInstanceName[MAX_PATH]; // Название устройства
    TCHAR tszProductName[MAX_PATH];  // Название продукта
    GUID  guidFFDriver; // GUID драйвера обратной связи
    WORD  wUsagePage;   // Используемая страница для устройств HID
    WORD  wUsage;       // Используемый код для устройств HID
} DIDEVICEINSTANCE;

 

ПРИМЕЧАНИЕ
При работе со структурой DIDEVICEINSTANCE вам потребуются только поля guidInstance, dwDevType и tszInstanceName. Поле guidInstance — это GUID, необходимый для инициализации устройства (это именно то, что мы ищем). Поле dwDevType — это тип устройства, перечисляемого в данный момент.
И, наконец, tszInstanceName — это текстовый буфер, который содержит имя устройства (такое, как «Joystick 1»), которое можно использовать, например, для формирования списка из которого пользователь будет выбирать устройство.

Позвольте мне теперь отнять у вас несколько минут и построить пару функций, которые инициализируют DirectInput и перечисляют все устройства, отображая имена найденных устройств по одному в окне сообщений. При этом вы можете каждый раз выбрать, прекратить перечисление или продолжить, щелкая по кнопке Cancel или OK соответственно.

ПРИМЕЧАНИЕ
Еще одно замечание о перечислении: в функции перечисления вы должны определить надо ли продолжать перечисление, или следует остановиться, для чего необходимо вернуть значение DIENUM_CONTINUE или DIENUM_STOP соответственно.
// Глобальный COM-объект DirectInput
IDirectInput8 *g_pDI;

// Прототипы функций
BOOL InitDIAndEnumAllDevices(HWND hWnd, HINSTANCE hInst);
BOOL CALLBACK EnumDevices(LPCDIDEVICEINSTANCE pdInst,
                          LPVOID pvRef);

BOOL InitDIAndEnumAllDevices(HWND hWnd, HINSTANCE hInst)
{
    if(FAILED(DirectInput8Create(hInst, DIRECTINPUT_VERSION,
                      IID_IDirectInput8, (void**)&g_pDI, NULL)))
        return FALSE;

    g_pDI->EnumDevices(DI8DEVCLASS_ALL, EnumDevices,
                      (LPVOID)hWnd, DIEDFL_ALLDEVICES);

    return TRUE;
}

BOOL CALLBACK EnumDevices(LPCDIDEVICEINSTANCE pdInst,
                          LPVOID pvRef)
{
    int Result;

    // Отображаем окно сообщений с именем найденного устройства
    Result = MessageBox((HWND)pvRef, pdInst->tszInstanceName,
                        "Device Found", MB_OKCANCEL);

    // Продолжаем перечисление, если нажата кнопка OK
    if(Result == IDOK)
        return DIENUM_CONTINUE;

    // Завершаем перечисление
    return DIENUM_STOP;
}

Просто вставьте приведенный выше фрагмент кода в вашу собственную программу и вызовите функцию InitDIAndEnumAllDevices().

ПРИМЕЧАНИЕ
Предположим, вы хотите перечислить только джойстики, подключенные к системе. Просто замените строку g_pDI->EnumDevices на следующую:
g_pDI->EnumDevices(DI8DEVTYPE_JOYSTICK,
                   EnumDevices, (LPVOID)hWnd,
                   DIEDFL_ATTACHEDONLY);

Создание COM-объекта устройства

Теперь, когда вы знаете GUID устройства, можно создать COM-объект IDirectInputDevice8. Это работа функции IDirectInput8::CreateDevice:

HRESULT IDirectInput8::CreateDevice(
    REFGUID rguid, // GUID создаваемого устройства жестко
                   // заданный или определенный при перечислении
    LPDIRECTINPUTDEVICE *lplpDirectInputDevice, // Указатель на
                                                // создаваемый объект
    LPUNKNOWN pUnkOuter); // NULL - не используется

Вот пример использования IDirectInput8::CreateDevice:

IDirectInputDevice8 *pDIDevice;
HRESULT hr = g_pDI->CreateDevice(DeviceGUID, &pDIDevice, NULL);

Вот еще пример создания объекта системной клавиатуры с использованием предопределенного GUID:

IDirectInputDevice8 *pDIDevice;
HRESULT hr = pDI->CreateDevice(GUID_SysKeyboard, &pDIDevice, NULL);

 

ПРИМЕЧАНИЕ
Вы можете встроить вызов функции CreateDevice в функцию перечисления, как будет показано в разделе «DirectInput и джойстики» далее в этой главе.

Установка формата данных

У каждого устройства есть собственный формат данных, используемый при чтении. В нем необходимо учесть множество вещей: клавиши, кнопки, оси и т.д. Чтобы ваша программа могла начать читать данные устройства, необходимо сообщить DirectInput этот формат.

Делается это через функцию IDirectInputDevice8::SetDataFormat:

HRESULT IDirectInputDevice8::SetDataFormat(LPCDIDATAFORMAT lpdf);

У функции SetDataFormat только один аргумент, являющийся указателем на структуру DIDATAFORMAT, определение которой выглядит так:

typedef struct {
    DWORD dwSize;     // Размер структуры
    DWORD dwObjSize;  // Размер структуры DIOBJECTDATAFORMAT
    DWORD dwFlags;    // Флаг, определяющий работает ли устройство 
                      // в абсолютном режиме (DIDF_ABSAXIS)
                      // или в относительном режиме (DIDF_RELAXIS)
    DWORD dwDataSize; // Размер получаемого от устройства
                      // пакета данных (в двойных словах)
    DWORD dwNumObjs;  // Количество объектов в массиве rgodf
    LPDIOBJECTDATAFORMAT rgodf; // Адрес массива структур
                                // DIOBJECTDATAFORMAT
} DIDATAFORMAT, *LPDIDATAFORMAT;

В большинстве случаев вам не придется заниматься инициализацией этой структуры, потому что DirectInput предоставляет вам для использования несколько предопределенных ее экземпляров, которые перечислены в таблице 3.5.


Таблица 3.5. Предустановленные структуры данных устройств DirectInput



Устройство Пример структуры данных

Клавиатура c_dfDIKeyboard pDIDevice->SetDataFormat(&c_dfDIKeyboard);
Мышь c_dfDIMouse pDIDevice->SetDataFormat(&c_dfDIMouse);
Джойстик c_dfJoystick pDIDevice->SetDataFormat(&c_dfDIJoystick);


Я не буду обсуждать специфику создания собственного формата данных, поскольку перечисленных в таблице 3.5 структур данных устройств вполне достаточно для этой книги. Если вы хотите создать собственную структуру формата данных устройства, обратитесь за информацией к документации DirectX SDK.

Установка уровня кооперации

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

Следующий этап в использовании устройства — установка уровня кооперации, за которую отвечает следующая функция:

HRESULT IDirectInputDevice8::SetCooperativeLevel(
     HWND  hWnd,      // Дескриптор родительского окна
     DWORD dwFlags);  // Флаги, определяющие вид совместного доступа

В аргументе hWnd функции SetCooperativeLevel укажите дескриптор окна вашего приложения. Чтобы использовать доступ к устройству совместно с другими приложениями, укажите в аргументе dwFlags одно из перечисленных в таблице 3.6 значений, которые определяют вид совместного доступа.


Таблица 3.6. Уровни кооперации IDirectInputDevice8



Уровень Описание

DISCL_NONEXCLUSIVE Использование данного параметра позволяет нескольким приложениям совместно использовать одно устройство ввода не влияя друг на друга
DISCL_EXCLUSIVE Этот параметр делает вашу программу захватчиком. При захвате устройства вы получаете единоличный доступ к нему, даже если другие установили эксклюзивный режим использования
DISCL_FOREGROUND Приложению требуется доступ при активности. Это означает, что для использования устройства программа должна быть активной. Если ваша программа становится неактивной, устройство будет автоматически освобождено, и вам надо будет восстановить контроль, когда программа вновь получит фокус
DISCL_BACKGROUND Вашей программе требуется фоновый доступ. Это значит, что программа может обращаться к устройству, даже когда она неактивна
DISCL_NOWINKEY Блокирует клавишу с логотипом Windows

 

ПРИМЕЧАНИЕ
Использовать флаг DISCL_NOWINKEY необязательно, но я рекомендую указывать его для полноэкранных приложений, чтобы предотвратить нежелательные прерывания работы программы.

Когда вы задаете уровень кооперации, то должны указать либо флаг DISCL_EXCLUSIVE, либо флаг DISCL_NONEXCLUSIVE и скомбинировать его с флагом DISCL_BACKGROUND или DISCL_FOREGROUND. Я рекомендую для рассматриваемых устройств использовать комбинацию DISCL_FOREGROUND и DISCL_NONEXCLUSIVE:

pDIDevice->SetCooperativeLevel(hWnd, DISCL_FOREGROUND | DISCL_NONEXCLUSIVE);

Установка специальных свойств

Теперь вы можете установить любые специальные свойства для устройства, такие как режимы осей, буферизация или максимальные и минимальные значения диапазона. Для режима осей есть два варианта — относительный и абсолютный. Абсолютный режим сообщает координаты относительно центральной точки. Позиции левее или выше этой точки передаются как отрицательные числа, а позиции правее или ниже — как положительные.

Относительные координаты сообщают о перемещении относительно последней зафиксированной позиции. Например, если указатель мыши находился в точке с координатами 100, 40 и переместился на 5 единиц вправо, то DrectInput вернет значеие 5, а не 105. Ваша задача — использовать эти относительные перемещения так, как считаете нужным.

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

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

Установкой специальных свойств заведует функция IDirectInputDevice8::SetProperty:

HRESULT IDirectInputDevice8::SetProperty(
    REFGUID         rguidProp, // GUID свойства
    LPCDIPROPHEADER pdiph);    // DIPROPHEADER содержащий данные
                               // об устанавливаемом свойстве

 

ПРИМЕЧАНИЕ
За один раз может быть установлено только одно свойство. Если необходимо установить другое свойство, вызовите функцию еще раз.

GUID, указаный в rguidProp, представляет свойство, которое вы хотите установить, такое как диапазон сообщаемых джойстиком значений или режим осей. Список GUID свойств приведен в таблице 3.7.


Таблица 3.7. GUID свойств



REFGUID Описание

DIPROP_AUTOCENTER Указывает, является ли устройство самоцентрируемым
DIPROP_AXISMODE Задает обсуждавшийся ранее режим осей
DIPROP_BUFFERSIZE Определяет размер входного буфера
DIPROP_CALIBRATIONMODE Указывает должен ли DirectInput получать от устройства калиброванные данные. По умолчанию все данные калиброваны
DIPROP_DEADZONE Размер мертвой зоны, в которой не регистрируются изменения. Может изменяться от 0 (отсутствует) до 10000 (100 процентов)
DIPROP_RANGE Устанавливает минимальное и максимальное значение диапазона
DIPROP_SATURATION Значение, определяющее максимальный диапазон устройства. Может находиться в диапазоне от 0 до 10000. Например, значение 9500 означает, что устройство, такое как джойстик, отклоненное на 95 процентов заданного диапазона будет считаться полностью отклоненным


Структура DIPROPHEADER, которая используется в вызове SetProperty, определена следующим образом:

typedef struct {
    DWORD dwSize;       // Размер объемлющей структуры
    DWORD dwHeaderSize; // Размер данной структуры
    DWORD dwObj;        // Параметр, который мы устанавливаем
    DWORD dwHow;        // Как мы устанавливаем значение
} DIPROPHEADER, *LPDIPROPHEADER

Поля dwSize и dwHeaderSize задают размеры структур в байтах, как описано в комментариях. Параметр dwObj может принимать множество различных значений, в зависимости от того, какое свойство мы устанавливаем. Например, чуть позже мы будем указывать значения DIJOFS_X и DIJOFS_Y, которые представляют ось X и ось Y джойстика, соответственно.

Для последнего параметра, dwHow, мы будем задавать значение DIPH_BYOFFSET. DirectX SDK по этому параметру определяет, что будет указано смещение в текущем формате данных для того объекта, к которому мы хотим получить доступ. Это значит, что DIOFS_X и DIOFS_Y — это смещения устанавливаемых значений в текущем формате данных.

Позднее в этой главе я покажу вам, как устанавливать свойства для джойстика.

Захват устройства

Перед использованием любое устройство должно быть захвачено (acquired). Захват устройства обеспечивает вашей программе доступ к нему, независимо от того, использует ли ваша программа совместный с другими приложениями доступ, или получает полный контроль над устройством. Будьте бдительны — другие программы могут бороться за те же права и похитить у вас контроль. Чтобы исправить это вы должны заново захватить устройство для использования.

Как узнать, когда нужно захватывать устройство? Во-первых, всегда при создании интерфейса — вы должны выполнить захват перед его использованием. Во-вторых, в любое другое время, когда другая программа перехватывает контроль и DirectInput сообщает об этом вашей программе.

Захват устройства выполняет вызов IDirectInputDevice8::Acquire:

HRESULT IDirectInputDevice8::Acquire();

 

ПРИМЕЧАНИЕ
Обратившись к документации DX SDK, вы увидите, что функции IDirectInputDevice8 возвращают стандартный код ошибки DIERR_INPUTLOST, сообщающий, что устройство необходимо захватить (поскольку доступ к нему был утрачен).

Когда вы закончите работу, необходимо отпустить устройство. Это работа функции IDirectInputDevice8::Unacquire:

HRESULT IDirectInputDevice8::Unacquire();

 

ВНИМАНИЕ!
Убедитесь, что завершив работу с устройством вы вызываете для него функцию Unacquire. Отсутствие этого вызова может привести к зависанию системы.

Опрос устройства

Опрос подготавливает устройство и, в ряде случаев, считывает для вас данные устройства, поскольку этот процесс может оказаться критичным по времени. Например, в случае джойстика, компьютер должен послать устройству импульс, чтобы чуть позже прочитать его данные. Это справедливо для некоторых джойстиков и не требуется для мышей и клавиатур.

Это не должно отвращать нас от использования опроса, поскольку с ним ядро кода будет более общим и сможет работать с любым устройством. Не беспокойтесь — опрос устройства, для которого он не требуется, не производит никакого эффекта. В конце концов, код будет более ясным.

Опрос устройства выполняется через IDirectInputDevice8::Poll:

HRESULT IDirectInputDevice8::Poll();

Чтение данных

Наконец! Вы достигли цели — завершающего чтения данных устройства, которое выполняет функция IDirectInputDevice8::GetDeviceState. Вы должны передать этой функции буфер данных, в котором будет сохранена информация устройства для использования в вашей программе. Как вы вскоре увидите, у разных устройств эти данные отличаются.

Вот прототип функции:

HRESULT IDirectInputDevice8::GetDeviceState(
    DWORD  cbData,   // Размер буфера для хранения данных устройства
    LPVOID lpvData); // Указатель на буфер для хранения данных устройства

Независимо от устройства, для чтения данных применяется приведенный ниже фрагмент кода. Он учитывает, что вы можете потерять фокус устройства и его потребуется снова захватить. Вы должны передать указатель на буфер, размер которого достаточно большой для сохранения данных устройства, а также количество считываемых данных. В следующих разделах («DirectInput и клавиатура», «DirectInput и мышь» и «DirectInput и джойстик») я покажу вам, как использовать приведенную ниже функцию для получения данных каждого из упомянутых устройств.

BOOL ReadDevice(IDirectInputDevice8 *pDIDevice,
                void *DataBuffer, long BufferSize)
{
    HRESULT hr;

    while(1) {
        // Опрос устройства
        g_pDIDevice->Poll();

        // Чтение состояния
        if(SUCCEEDED(hr = g_pDIDevice->GetDeviceState(BufferSize,
                                               (LPVOID)DataBuffer)))
            break;

        // Возвращаем нераспознанную ошибку
        if(hr != DIERR_INPUTLOST && hr != DIERR_NOTACQUIRED)
            return FALSE;

        // Вновь захватываем устойство и пытаемся
        // получить данные еще раз
        if(FAILED(g_pDIDevice->Acquire()))
            return FALSE;
    }
    // Сообщаем об успехе
    return TRUE;
}

netlib.narod.ru< Назад | Оглавление | Далее >

Сайт управляется системой uCoz