• Что такое DirectInput?
  • DirectInput API
  • Программа Qwerty
  • Инициализация DirectInput
  • Управление версией DirectInput
  • Программа Smear
  • Глава 6. DirectInput

    Давайте отдохнем от DirectDraw и познакомимся с библиотекой DirectInput. Конечно, эта книга посвящена компьютерной графике, но ввод информации пользователем — необходимая часть любого графического приложения. Применение DirectInput улучшает работу программ, так как ввод обнаруживается и обрабатывается с максимальной скоростью. После краткого знакомства с DirectInput API мы обсудим различные схемы получения пользовательского ввода, поддерживаемые DirectInput. Знакомство закончится созданием двух приложений: Qwerty и Smear. Программа Qwerty использует DirectInput для ввода с клавиатуры, а Smear работает с мышью.

    Для компиляции и запуска программ этой главы вам понадобится DirectX 3 SDK. Пользователи Windows NT 4.0 должны установить Service Pack 3 или более позднюю версию.

    Что такое DirectInput?


    DirectInput представляет собой DirectX API для работы с устройствами ввода — клавиатурой, мышью, джойстиками, рулями, авиационными рукоятками, шлемами виртуальной реальности и даже устройствами с обратной связью. В полном соответствии с идеологией DirectX библиотека DirectInput проектировалась в первую очередь для реализации высокого быстродействия и аппаратной независимости.

    Основная задача DirectInput — как можно быстрее обнаружить пользовательский ввод и доставить его приложению. Исключение составляют устройства с обратной связью; для них в DirectInput предусмотрены функции как ввода, так и вывода.

    Поддерживаемые устройства

    DirectInput поддерживает практически все устройства ввода, подключаемые к PC. Конечно, речь идет лишь о тех устройствах, для которых существуют драйверы DirectInput. Библиотека обеспечивает настолько исчерпывающую поддержку любых устройств ввода, что она (скорее всего) сможет поддерживать и те устройства, которые еще не изобретены.

    Я не пытаюсь обсуждать все устройства, поддерживаемые DirectInput, потому что для этого потребовалась бы отдельная книга. Здесь же DirectInput рассматривается как высокопроизводительная альтернатива традиционному механизму Windows для получения данных.

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

    Быстродействие

    Наверное, вас интересует, каким образом DirectInput обгоняет традиционные механизмы Windows. DirectInput однозначно приходится выбирать для устройств, не поддерживаемых Win32 API, но зачем использовать его для работы с мышью и клавиатурой?

    DirectInput, как и DirectDraw, обходит традиционные механизмы Windows и обеспечивает прямой доступ к устройствам, не утрачивая аппаратной независимости. Поскольку Windows в этой схеме не используется, установленные в системе параметры устройств ввода (например, частота повтора символов для клавиатуры или чувствительность мыши) не влияют на DirectInput.

    Схемы получения данных

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

    С помощью непосредственных данных можно, например, определить, нажата ли некоторая клавиша в данный момент. Для клавиатуры получение непосредственных данных напоминает применение функции Win32 GetAsyncKeyState(). Непосредственные данные лучше всего работают при частом опросе устройства ввода (как правило, не реже 30 раз в секунду). Редкий опрос может привести к потере данных; если за время нахождения клавиши в нажатом состоянии клавиатура не опрашивалась, то приложение не узнает о наличии ввода.

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

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

    Опросы и оповещения

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

    Кроме опроса устройств существует и альтернативный вариант — оповещение (notification). В этом случае программный поток (thread) блокируется и ожидает поступления оповещающего события. С наступлением такого события поток автоматически активизируется. Оповещение позволяет приложению реагировать на изменения в состоянии устройства ввода без расходов процессорного времени, связанных с опросом.

    Для однопоточных приложений оповещение используется редко, потому что во время ожидания события поток блокируется и не может ничего делать. Если только вся работа приложения не сводится к получению ввода от пользователя, для оповещения потребуется по крайней мере два потока. В программах этой главы оповещение не используется, однако мы встретимся с ним в главе 7.

    Уровни кооперации

    Чтобы приложение могло задать нужную степень контроля над устройством, в DirectInput используются уровни кооперации. DirectInput, как и DirectDraw, позволяет установить монопольный (exclusive) и совместный (nonexclusive) режим доступа для каждого устройства. Если приложение DirectInput обладает монопольным доступом к устройству ввода, то никакое другое приложение заведомо не сможет получить монопольного доступа к тому же устройству (хотя сможет получить совместный доступ).

    DirectInput также позволяет задать уровень кооперации для активного (foreground) и фонового (background) режимов работы — эти два термина часто приводят к недоразумениям. Активный доступ (foreground access) означает, что приложение работает с устройством только тогда, когда обладает фокусом ввода (по аналогии с тем, как ввод с клавиатуры по умолчанию передается приложению, обладающему фокусом). Фоновый доступ (background access) означает, что приложение может обращаться к устройству независимо от того, обладает ли оно фокусом ввода. Интуитивно кажется, будто активный доступ обладает большими возможностями, но на самом деле это не так. В программах Qwerty и Smear используется совместный активный уровень кооперации.

    Данные об осевых смещениях

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

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

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

    Приложение DirectDraw в случае необходимости может уступить видеопамять другому приложению и восстановить ее, когда исходное приложение снова получит фокус. В DirectInput приложение тоже может потерять устройство и восстановить контроль над ним перед тем, как продолжить работу. В таких случаях говорят, что приложение захватывает устройство (acquire) или уступает его (unacquire). Для получения данных необходимо захватить устройство. Приложение может уступить устройство по требованию (доступ к устройству передается Windows или другому приложению) или автоматически (например, если DirectInput отбирает право доступа к устройству, чтобы передать его другому приложению).

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

    DirectInput API

    До выхода DirectX 3 библиотека DirectInput была построена на существующих функциях Win32 и не поддерживала ввода с клавиатуры или от мыши. В DirectX 3 появились COM-интерфейсы для клавиатуры и мыши, но все остальные устройства продолжали зависеть от функций Win32 (и особенно от функции joyGetPosEx()). В DirectX 5 зависимость DirectInput от Win32 полностью устранена, а все устройства ввода переведены на использование COM-интерфейсов. Работа с устройствами ввода реализована через три интерфейса:

    • DirectInput

    • DirectInputDevice

    • DirectInputEffect

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

    Интерфейс DirectInputDevice представляет устройство ввода. Его функции выполняют инициализацию, настройку, захват и отпускание устройств. Что еще важнее, DirectInputDevice содержит функции для получения данных от устройства.

    Интерфейс DirectInputEffect применяется для обслуживания устройств с обратной связью. В этой книге он не используется.

    Интерфейс DirectInput

    Инициализация DirectInput происходит в тот момент, когда вы получаете указатель на интерфейс DirectInput функцией DirectInputCreate(). Затем полученным интерфейсом можно воспользоваться для создания экземпляров интерфейса DirectInputDevice, составления списка доступных устройств и даже для вызова панели управления DirectInput. Интерфейс DirectInput содержит следующие четыре функции:

    • CreateDevice()

    • EnumDevice()

    • GetDeviceStatus()

    • RunControlPanel()

    Функция CreateDevice() создает новые экземпляры интерфейса DirectInputDevice. Она получает три аргумента: GUID нужного устройства, адрес инициализируемого указателя на интерфейс и показатель агрегирования (aggregation) COM, который обычно должен быть равен нулю. Для системной мыши и клавиатуры в DirectX предусмотрены стандартные значения GUID:

    • GUID_SysKeyboard

    • GUID_SysMouse

    Значения GUID остальных устройств можно получить функцией EnumDevices().

    Функция EnumDevices() обнаруживает устройства ввода, установленные на данном компьютере. Она позволяет составить список устройств по их типу, по факту их текущего подключения к компьютеру и по тому, являются ли они устройствами с обратной связью. Для каждого устройства, обнаруженного функцией EnumDevices(), предоставляются два значения GUID: GUID экземпляра и GUID продукта. GUID экземпляра идентифицирует конкретное устройство, а GUID продукта — его тип. При вызове функции CreateDevice() используется GUID экземпляра.

    С помощью функции GetDeviceStatus() можно определить, доступно ли устройство для DirectInput в данный момент. Код возврата DI_OK означает, что устройство подключено и доступно. Функция RunControlPanel() вызывает приложение DirectInput Control Panel. Точнее, она вызывает приложение DirectX Control Panel и активизирует вкладку DirectInput.

    Интерфейс DirectInputDevice

    Доступ ко всем устройствам ввода, представленным в DirectInput, осуществляется через интерфейс DirectInputDevice. Интерфейс DirectInputDevice содержит следующие функции:

    • Acquire()

    • Unacquire()

    • GetCapabilities()

    • GetDeviceData()

    • GetDeviceInfo()

    • GetDeviceState()

    • SetDataFormat()

    • SetEventNotification()

    • EnumObjects()

    • GetObjectInfo()

    • GetProperty()

    • SetProperty()

    • SetCooperativeLevel()

    • RunControlPanel()

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

    Функции Acquire() и Unacquire() устанавливают и разрывают связь между устройством ввода и DirectInput. Перед тем как получать от устройства данные, необходимо захватить его.

    Функция Acquire() используется для начального захвата устройства и для восстановления нарушенной связи с устройством. Обычно связь между устройством ввода и DirectInput разрывается из-за того, что приложение теряет фокус ввода. Функция Unacquire() используется, как правило, для того, чтобы вернуть Windows право доступа к устройству. Например, при работе с меню необходимо уступить объекты DirectInputDevice, представляющие мышь и клавиатуру, чтобы Windows могли нормально обработать выбор команды меню.

    Функция GetCapabilities() с помощью структуры DIDEVCAPS описывает возможности устройства, в том числе количество кнопок, количество осей и поддержку обратной связи. Кроме того, в структуру включаются описания конкретных особенностей (или ограничений) устройства, влияющих на его использование. Например, установка флага DIDC_POLLEDDEVICE означает, что от устройства можно получать лишь непосредственные данные.

    Функция GetDeviceData() получает от устройства буферизованные данные. Она позволяет извлечь из буфера один или несколько элементов, а также просмотреть его содержимое (то есть прочитать элементы буфера без их удаления).

    Функция GetDeviceInfo() заполняет структуру DIDEVICEINSTANCE информацией об устройстве. В структуру заносятся значения GUID экземпляра и продукта для данного устройства, а также строки с неформальными описаниями. Ту же самую информацию можно получить функцией EnumDevices() интерфейса DirectInput, так что, строго говоря, эта функция не является обязательной.

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

    Функция SetDataFormat() описывает формат, в котором вводимые данные возвращаются устройством. Приложение может определить собственный формат, а в DirectInput предусмотрены три стандартных формата для стандартных устройств:

    • c_dfDIKeyboard

    • c_dfDIMouse

    • c_dfDIJoystick

    Перед захватом устройства необходимо вызвать для него функцию SetDataFormat().

    Функция SetEventNotification(), особенно часто используемая в многопоточных приложениях, предназначена для обработки оповещений. При изменении состояния данного устройства DirectInput сигнализирует о наступлении события. Таким образом, поток может перейти в режим блокировки (например, с помощью функции WaitForSingleObject()) и ожидать изменений в устройстве ввода. При наступлении события поток автоматически отреагирует на него.

    Каждое устройство ввода содержит один или несколько объектов. Для клавиатуры объект представляет отдельную клавишу. Для мыши объекты представляют каждую кнопку и каждую ось. Функция EnumObjects() составляет список объектов заданного устройства и возвращает для каждого объекта GUID и строку. Строка содержит неформальное описание объекта (например, «ось X» или «Right Shift»). GUID описывает тип объекта и может принимать одно из следующих значений:

    • GUID_XAxis

    • GUID_YAxis

    • GUID_ZAxis

    • GUID_RAxis

    • GUID_UAxis

    • GUID_VAxis

    • GUID_Button

    • GUID_Key

    • GUID_POV

    Чтобы успокоить вас, скажу, что функция EnumObjects() обычно не нужна, особенно для стандартных устройств. Например, для работы с клавиатурой вам не придется составлять список всех клавиш.

    Функция GetObjectInfo() позволяет получить ту же информацию, что и EnumObjects(), но без предварительного составления списка объектов. Интересующий вас объект задается значением его смещения или идентификатора.

    Функции GetProperty() и SetProperty() применяются для просмотра и установки параметров устройств (свойств), отсутствующих в DirectInput API. В DirectInput предусмотрен ряд стандартных свойств (например, свойства autocenter и deadzone для джойстиков), однако эти функции могут применяться и для других, нестандартных свойств.

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

    Функция SetCooperativeLevel() определяет степень контроля над заданным устройством. Допускаются следующие значения:

    • DISCL_BACKGROUND

    • DISCL_EXCLUSIVE

    • DISCL_FOREGROUND

    • DISCL_NONEXCLUSIVE

    Вызов функции SetCooperativeLevel() наряду с вызовом SetDataFormat() необходим для получения данных от устройства.

    Наконец, функция RunControlPanel() запускает приложение Control Panel для типа устройства, представленного интерфейсом DirectInputDevice. Например, для стандартной клавиатуры эта функция запускает приложение Keyboard, которое также можно запустить из стандартной Control Panel. Не путайте ее с функцией RunControlPanel() интерфейса DirectInput, которая запускает общее приложение DirectInput Control Panel вместо приложения для конкретного устройства ввода.

    Программа Qwerty

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

    Примеров хватает в избытке. Скажем, когда кто-нибудь чихнет, мы привыкли говорить: «Будь здоров». Когда-то считалось, что чихание — акт (или во всяком случае попытка) изгнания злого духа из тела. Сегодня даже самые религиозные люди вряд ли считают чихание чем-то вроде экзорцизма в миниатюре, и все равно, если вы чихнете, кто-нибудь непременно пожелает вам здоровья. Мы просто не задумываемся над тем, что означают эти слова.

    То же самое происходит и с законами. Я вырос в северной части Нью-Мексико — области, где в первой половине века спасалось множество больных туберкулезом. Жертвы туберкулеза приходили с запада и востока в надежде, что чистый, сухой воздух исцелит их или по крайней мере задержит развитие болезни (некоторые из моих родственников переехали сюда именно по этой причине). Для предотвращения новых заболеваний в Вальморе (маленький городок с туберкулезным санаторием) и нескольких близлежащих городах был принят закон, запрещающий плевать в общественных местах. Хотя туберкулез с тех пор был практически искоренен, в некоторых местах этот закон все еще действует (хотя и не очень строго соблюдается).

    Клавиатура вашего компьютера тоже появилась не случайно. Раскладка клавиш, известная под названием Qwerty (по первым шести буквам верхнего алфавитного ряда), была спроектирована для механических пишущих машинок. Если вы работали на пишущей машинке (или хотя бы видели ее в музее), то знаете, что при нажатии каждой клавиши поднимается рычаг, который бьет по бумаге через тонкую красящую ленту. Раскладка Qwerty разработана так, чтобы свести к минимуму вероятность одновременного нажатия двух соседних клавиш и заклинивания рычагов. Для этого часто используемые клавиши были разбросаны по всей клавиатуре, чтобы один рычаг успевал опуститься до того, как поднимется другой.

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

    Так или иначе, название программы Qwerty происходит от раскладки Qwerty — отчасти потому, что я не смог придумать другого имени, отчасти из уважения к исторической традиции. Программа Qwerty изображена на рис. 6.1.

    Рис. 6.1. Программа Qwerty


    На экране изображены буквы Qwerty и названия еще нескольких клавиш. Программа с помощью DirectInput обнаруживает нажатые клавиши и рисует их в наклонном начертании. Все остальные клавиши рисуются прямо.

    Класс QwertyWin

    В программе Qwerty, как и во всех остальных программах этой книги, специализированный класс окна порождается от базового класса DirectDrawWin. В данном случае производный класс называется QwertyWin (см. листинг 6.1).


    Листинг 6.1. Класс QwertyWin

    class QwertyWin : public DirectDrawWin {

    public:

     QwertyWin();

    protected:

     //{{AFX_MSG(QwertyWin)

     afx_msg int OnCreate(LPCREATESTRUCT lpCreateStruct);

     afx_msg void OnDestroy();

     afx_msg void OnActivate(UINT nState, CWnd* pWndOther,    BOOL bMinimized);

     //}}AFX_MSG

     DECLARE_MESSAGE_MAP()

    private:

     int SelectDriver();

     int SelectInitialDisplayMode();

     BOOL CreateCustomSurfaces();

     void DrawScene();

     void RestoreSurfaces();

    private:

     LPDIRECTINPUT dinput;

     LPDIRECTINPUTDEVICE keyboard;

     BOOL esc_pressed;

     LPDIRECTDRAWSURFACE esc_up, esc_dn;

     LPDIRECTDRAWSURFACE space_up, space_dn;

     LPDIRECTDRAWSURFACE q_up, q_dn;

     LPDIRECTDRAWSURFACE w_up, w_dn;

     LPDIRECTDRAWSURFACE e_up, e_dn;

     LPDIRECTDRAWSURFACE r_up, r_dn;

     LPDIRECTDRAWSURFACE t_up, t_dn;

     LPDIRECTDRAWSURFACE y_up, y_dn;

     LPDIRECTDRAWSURFACE rctrl_up, rctrl_dn;

     LPDIRECTDRAWSURFACE lctrl_up, lctrl_dn;

     LPDIRECTDRAWSURFACE lalt_up, lalt_dn;

     LPDIRECTDRAWSURFACE ralt_up, ralt_dn;

    };

    Прежде чем двигаться дальше, обратите внимание на отсутствие обработчика OnKeyDown(). Во всех программах, рассмотренных нами ранее, функция OnKeyDown() обрабатывала сообщения от клавиатуры. В программе Qwerty мы пользуемся услугами DirectInput и потому не нуждаемся в OnKeyDown().

    В самом начале объявляются три обработчика сообщений:

    • OnCreate()

    • OnDestroy()

    • OnActivate()

    Функция OnCreate() инициализирует и настраивает DirectInput, а функция OnDestroy() освобождает объекты DirectInput. Функция OnActivate(), вызываемая MFC при получении или потере фокуса, будет использована для повторного захвата клавиатуры.

    Две следующие функции, SelectDriver() и SelectInitialDisplayMode(), присутствуют почти во всех наших программах. Они остались в том виде, в котором их создал AppWizard, и потому не требуют обсуждения.

    Функции CreateCustomSurfaces() и RestoreSurfaces() делают то же, что и раньше, так что они тоже не рассматриваются. Достаточно сказать, что эти функции инициализируют и восстанавливают поверхности, указатели на которые объявляются в нижней части листинга 6.1.

    Функция DrawScene() с помощью DirectInput определяет, какие клавиши были нажаты, и обеспечивает соответствующий вывод. Вскоре мы рассмотрим эту функцию.

    После функций следуют переменные класса. Сначала объявляется указатель на интерфейс DirectInput(dinput), через него выполняется инициализация и осуществляются обращения к DirectInput. Переменная key — указатель на интерфейс DirectInputDevice, используемый для обращений к клавиатуре. Логическая переменная esc_pressed сигнализирует о завершении приложения.

    Оставшаяся часть определения класса состоит из указателей на интерфейсы DirectDrawSurface. Для каждой клавиши, поддерживаемой приложением, создаются две поверхности (для нажатого и отпущенного состояния).

    Инициализация DirectInput

    Инициализация DirectInput и DirectDraw выполняется в функции OnCreate(). DirectInput инициализируется версией OnCreate() класса QwertyWin, а DirectDraw — версией из DirectDrawWin. Функция QwertyWin::OnCreate() приведена в листинге 6.2.


    Листинг 6.2. Функция QwertyWin::OnCreate()

    int QwertyWin::OnCreate(LPCREATESTRUCT lpCreateStruct) {

     HRESULT r=DirectInputCreate(AfxGetInstanceHandle(),    DIRECTINPUT_VERSION, &dinput, 0);

     if (r!=DI_OK) {

      AfxMessageBox("DirectInputCreate() failed");

      return -1;

     }

     r = dinput->CreateDevice(GUID_SysKeyboard, &keyboard, 0);

     if (r!=DI_OK) {

      AfxMessageBox("CreateDevice(keyboard) failed");

      return -1;

     }

     r = keyboard->SetDataFormat(&c_dfDIKeyboard);

     if (r!=DI_OK) {

      AfxMessageBox("keyboard->SetDataFormat() failed");

      return -1;

     }

     r=keyboard->SetCooperativeLevel(GetSafeHwnd(), DISCL_FOREGROUND | DISCL_NONEXCLUSIVE);

     if (r!=DI_OK) {

      AfxMessageBox("keyboard->SetCooperativeLevel() failed");

      return -1;

     }

     if (DirectDrawWin::OnCreate(lpCreateStruct)==-1) return -1;

     return 0;

    }

    Прежде всего обратите внимание — версия OnCreate() базового класса вызывается лишь в конце функции. Это сделано для того, чтобы при неудачной инициализации DirectInput программа выводила окно сообщения и прекращала работу без инициализации DirectDraw.

    Сначала функция OnCreate() инициализирует указатель dinput с помощью функции DirectInputCreate(), которой необходимо передать четыре аргумента. Вызов этой функции выглядит так:

    HRESULT r=DirectInputCreate(AfxGetInstanceHandle(), DIRECTINPUT_VERSION, &dinput, 0);

    Первый аргумент - логический номер экземпляра приложения, получаемый функцией AfxGetInstanceHandle(). Второй аргумент — номер версии DirectInput. В нашем случае используется константа DIRECTINPUT_VERSION, она определяется DirectInput в зависимости от версии SDK, использованной для компиляции приложения. Различные версии DirectInput более подробно рассматриваются в этой главе ниже. Третий аргумент DirectInputCreate() — адрес инициализируемого указателя, а четвертый — показатель агрегирования COM, который обычно равен нулю (агрегированием называется разновидность наследования, используемая в COM). Если инициализация DirectInput проходит успешно (то есть если DirectInputCreate() возвращает DI_OK), указатель dinput может использоваться для работы с DirectInput.

    Затем мы создаем экземпляр интерфейса DirectInputDevice, который представляет клавиатуру. Я снова приведу соответствующую строку листинга 6.2:

    r = dinput->CreateDevice(GUID_SysKeyboard, &keyboard, 0);

    Функция CreateDevice() интерфейса DirectInput применяется для инициализации устройств DirectInput. В нашем случае первым аргументом является стандартная константа GUID_SysKeyboard, показывающая, что мы собираемся работать с системной клавиатурой. Второй аргумент — адрес указателя keyboard, через который мы впоследствии будем обращаться к клавиатуре. Третий аргумент — показатель агрегирования COM, в нашем случае он должен быть равен нулю.

    Следующий шаг — выбор формата данных устройства. Для клавиатуры он выполняется просто:

    r = keyboard->SetDataFormat(&c_dfDIKeyboard);

    Функции SetDataFormat() интерфейса DirectInputDevice передается единственный аргумент — константа стандартного формата c_dfDIKeyboard. Программа Qwerty работает лишь с одним устройством (клавиатурой), но, как мы убедимся в программе Smear, формат данных должен задаваться отдельно для каждого устройства, используемого программой.

    Затем мы задаем уровень кооперации устройства с помощью функции SetCooperativeLevel() интерфейса DirectInputDevice. Соответствующий фрагмент листинга 6.2 выглядит так:

    r=keyboard->SetCooperativeLevel(GetSafeHwnd(), DISCL_FOREGROUND | DISCL_NONEXCLUSIVE);

    Функция SetCooperativeLevel() получает два аргумента: логический номер окна и набор флагов, определяющих уровень кооперации. Функция GetSafeHwnd() определяет логический номер окна, а флаги DISCL_FOREGROUND и DISCL_NONEXCLUSIVE задают нужный уровень кооперации. Флаг активного режима DISCL_FOREGROUND присутствует потому, что на время активности другого приложения нам не потребуется ввод от клавиатуры, а флаг DISCL_NONEXCLUSIVE — потому, что DirectInput не позволяет установить монопольный доступ к клавиатуре.

    До получения данных с клавиатуры остался всего один шаг: мы должны захватить устройство функцией Acquire(). Эта задача решается функцией OnActivate(), которую мы рассмотрим ниже.

    Функция QwertyWin::OnCreate() завершается вызовом функции DirectDrawWin::OnCreate(), инициализирующей DirectDraw. Эта функция обсуждалась в главе 3.

    Захват клавиатуры

    Итак, мы инициализировали DirectInput и подготовили клавиатуру к работе; теперь необходимо захватить ее. Для этой цели используется функция OnActivate(), потому что клавиатуру приходится захватывать при каждой активизации нашего приложения. Функция OnActivate() выглядит так:

    void QwertyWin::OnActivate(UINT nState, CWnd* pWndOther, BOOL bMinimized) {

     DirectDrawWin::OnActivate(nState, pWndOther, bMinimized);

     if (nState!=WA_INACTIVE && keyboard) {

      TRACE("keyboard->Acquire()\n");

      keyboard->Acquire();

     }

    }

    После вызова версии OnActivate() базового класса мы проверяем, происходит ли активизация приложения (функция OnActivate() вызывается и в случае деактивизации, когда активным становится другое приложение). Если проверка дает положительный результат, мы вызываем функцию Acquire() интерфейса DirectInputDevice.

    Перед вызовом Acquire() можно проверить, не была ли клавиатура захвачена ранее, но в этом нет необходимости. DirectInput игнорирует лишние вызовы функции Acquire().

    Определение состояния клавиш

    Теперь по указателю на интерфейс клавиатуры можно определить состояние отдельных клавиш. В нашей программе это происходит в функции DrawScene(), перед обновлением экрана. Функция DrawScene() приведена в листинге 6.3.


    Листинг 6.3. Функция QwertyWin::DrawScene()

    void QwertyWin::DrawScene() {

     static char key[256];

     keyboard->GetDeviceState(sizeof(key), &key);

     //---------- Клавиши QWERTY --------

     if (key[DIK_Q] & 0x80) BltSurface(backsurf, q_dn, 213, 70);

     else BltSurface(backsurf, q_up, 213, 70);

     if (key[DIK_W] & 0x80) BltSurface(backsurf, w_dn, 251, 70);

     else BltSurface(backsurf, w_up, 251, 70);

     if (key[DIK_E] & 0x80) BltSurface(backsurf, e_dn, 298, 70);

     else BltSurface(backsurf, e_up, 298, 70);

     if (key[DIK_R] & 0x80) BltSurface(backsurf, r_dn, 328, 70);

     else BltSurface(backsurf, r_up, 328, 70);

     if (key[DIK_T] & 0x80) BltSurface(backsurf, t_dn, 361, 70);

     else BltSurface(backsurf, t_up, 361, 70);

     if (key[DIK_Y] & 0x80) BltSurface(backsurf, y_dn, 393, 70);

     else BltSurface(backsurf, y_up, 393, 70);

     //---------------- LEFT CONTROL ---------------

     if (key[DIK_LCONTROL] & 0x80) BltSurface(backsurf, lctrl_dn, 50, 180);

     else BltSurface(backsurf, lctrl_up, 49, 180);

     //---------------- RIGHT CONTROL ---------------

     if (key[DIK_RCONTROL] & 0x80) BltSurface(backsurf, rctrl_dn, 490, 180);

     else BltSurface(backsurf, rctrl_up, 490, 180);

     //---------------- LEFT ALT ---------------

     if (key[DIK_LMENU] & 0x80) BltSurface(backsurf, lalt_dn, 100, 260);

     else BltSurface(backsurf, lalt_up, 100, 260);

     //---------------- RIGHT ALT ---------------

     if (key[DIK_RMENU] & 0x80) BltSurface(backsurf, ralt_dn, 440, 260);

     else BltSurface(backsurf, ralt_up, 440, 260);

     //---------------- SPACE -----------------

     if (key[DIK_SPACE] & 0x80) BltSurface(backsurf, space_dn, 170, 340);

     else BltSurface(backsurf, space_up, 170, 340);

     //---------- ESCAPE -------------

     if (key[DIK_ESCAPE] & 0x80) {

      BltSurface(backsurf, esc_dn, 0, 0);

      esc_pressed=TRUE;

     } else {

      BltSurface(backsurf, esc_up, 0, 0);

      if (esc_pressed) PostMessage(WM_CLOSE);

     }

     primsurf->Flip(0, DDFLIP_WAIT);

    }

    Состояние устройства определяется функцией GetDeviceState() интерфейса DirectInputDevice. Тип и размер второго аргумента GetDeviceState() зависят от типа устройства, а также от формата данных, заданного функцией SetDataFormat(). Для клавиатуры функция должна получать массив из 256 байт, где каждый байт соответствует одной клавише. В DirectInput предусмотрен набор клавиатурных констант, которые используются как индексы массива и позволяют ссылаться на нужные клавиши. DirectInput обозначает нажатие клавиши установкой старшего бита того байта, который представляет данную клавишу. Объявление массива и вызов функции GetDeviceState() находятся в верхней части листинга 6.3, я снова привожу их:

    static char key[256];

    keyboard->GetDeviceState(sizeof(key), &key);

    Адрес массива клавиш передается во втором аргументе GetDeviceState(). Первый аргумент определяет размер данных в байтах.

    Все готово к проверке элементов массива. Сначала мы проверяем, была ли нажата клавиша Q:

    if (key[DIK_Q] & 0x80) BltSurface(backsurf, q_dn, 213, 70);

    else BltSurface(backsurf, q_up, 213, 70);

    Константа DIK_Q определяет индекс клавиши Q в массиве. Мы проверяем значение старшего бита; если бит установлен, значит, клавиша Q нажата, и мы копируем поверхность, изображающую клавишу Q в нажатом состоянии (q_dn), функцией BltSurface(). Если клавиша не нажата, копируется поверхность q_up.

    Обратите внимание: каждой клавише соответствует отдельный элемент массива, даже для функционально одинаковых клавиш. Например, две клавиши Alt обрабатываются по отдельности. Кроме того, DirectInput не отличает прописных букв от строчных. Чтобы учесть регистр буквы, придется дополнительно проверить состояние обеих клавиш Shift.

    Оставшаяся часть функции состоит из аналогичных проверок состояния других клавиш. После того как все клавиши будут проверены, функция Flip() интерфейса DirectDrawSurface выводит новое изображение на экран.

    Завершение приложения

    Завершить работу DirectInput несложно — для этого достаточно освободить все интерфейсы DirectInput. В нашей программе это происходит в функции OnDestroy():

    void QwertyWin::OnDestroy() {

     DirectDrawWin::OnDestroy();

     if (dinput) dinput->Release(), dinput=0;

     if (keyboard) {

      keyboard->Unacquire();

      keyboard->Release(), keyboard=0;

     }

    }

    Управление версией DirectInput

    По умолчанию приложения DirectInput требуют, чтобы версия runtime-части DirectX, установленной на компьютере пользователя, совпадала с версией SDK, использованной для компиляции приложения, или превышала ее. Например, если вы создаете приложение с использованием DirectX 5 и запускаете его на компьютере с установленным DirectX 3, вызов функции DirectInputCreate() закончится неудачей.

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

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

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

    #include

    #include

    #include

    #include

    #include

    #include

    #define DIRECTINPUT_VERSION 0x0300

    #include

    В приведенном фрагменте присутствуют директивы include для всех заголовочных файлов программы. Символическая константа DIRECTINPUT_VERSION должна быть определена до включения файла dinput.h (номер версии определяется старшим байтом).

    В результате программа будет компилироваться как версией 3, так и версией 5 DirectX SDK (Microsoft пропустила версию DirectX 4). Получившиеся выполняемые файлы будут работать с runtime-частью DirectX 3 и выше. Поскольку Windows NT Service Pack 3 включает поддержку DirectX 3 для DirectInput, программы успешно компилируются и работают в Windows NT.

    Программа Smear

    Перейдем к поддержке мыши в DirectInput. Она будет рассматриваться на примере программы Smear, которая отображает поверхность в центре экрана и затем сдвигает изображение в соответствии с перемещением мыши (см. рис. 6.2).

    Структура приложения

    Хотя программа Smear демонстрирует работу с мышью, она также использует клавиатуру для проверки клавиши Escape. Работа с клавиатурой уже рассматривалась на примере программы Qwerty, поэтому мы не будем надолго задерживаться на ней.

    Рис. 6.2. Программа Smear


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

    Часть программы Smear, работающая с DirectDraw, отличается от всех остальных программ книги, потому что в данном случае не применяется переключение страниц — вместо этого мы непосредственно обновляем содержимое экрана. Такой подход имеет ряд последствий.

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

    Во-вторых, мы уже не можем просто стереть фоновое изображение. В других программах мы стираем весь вторичный буфер, строим новый кадр и обновляем экран; никакого мерцания при этом не возникает. Стирание вторичного буфера в программе Smear вызовет заметное мерцание, потому что стертый фон будет отображаться во время вывода нового кадра. Программа названа Smear (то есть «размазывание») как раз потому, что фон не стирается, в результате при перемещении поверхности остается смазанный след.

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

    Класс SmearWin

    Основная функциональность программы Smear обеспечивается классом SmearWin (см. листинг 6.4).


    Листинг 6.4. Класс SmearWin

    class SmearWin : public DirectDrawWin {

    public:

     SmearWin();

    protected:

     //{{AFX_MSG(SmearWin)

     afx_msg int OnCreate(LPCREATESTRUCT lpCreateStruct);

     afx_msg void OnDestroy();

     afx_msg void OnActivate(UINT nState, CWnd* pWndOther, BOOL bMinimized);

     //}}AFX_MSG

     DECLARE_MESSAGE_MAP()

    private:

     BOOL CreateFlippingSurfaces();

    private:

     int SelectDriver();

     int SelectInitialDisplayMode();

     BOOL CreateCustomSurfaces();

     void DrawScene();

     void RestoreSurfaces();

    private:

     BOOL InitKeyboard();

     BOOL InitMouse();

    private:

     LPDIRECTINPUT dinput;

     LPDIRECTINPUTDEVICE mouse;

     LPDIRECTINPUTDEVICE keyboard;

     LPDIRECTDRAWSURFACE sphere;

     int x, y;

    };

    В классе объявлены три обработчика:

    • OnCreate()

    • OnDestroy()

    • OnActivate()

    Функция OnCreate() инициализирует DirectInput, а также готовит к работе мышь и клавиатуру. Функция OnDestroy() освобождает объекты DirectInput, инициализированные функцией OnCreate(). Функция OnActivate() захватывает клавиатуру в начале работы и при повторной активизации приложения.

    Затем следует переопределенная функция DirectDrawWin::CreateFlippingSurfaces(). Нам не нужна переключаемая первичная поверхность, которая по умолчанию предоставляется классом DirectDrawWin, поэтому мы переопределяем эту функцию и создаем первичную поверхность, неспособную к переключению страниц.

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

    Затем класс SmearWin объявляет функции InitMouse() и InitKeyboard(). Функция OnCreate() возлагает на них ответственность за инициализацию устройств.

    Наконец, мы объявляем несколько переменных. Переменная dinput — указатель на интерфейс DirectInput, она используется для работы с DirectInput после инициализации. Переменные mouse и keyboard указывают на интерфейсы DirectInputDevice, они инициализируются функциями InitMouse() и InitKeyboard() соответственно. Указатель на поверхность sphere и целые переменные x и y предназначены для вывода и позиционирования единственной поверхности приложения.

    Инициализация DirectInput

    Функция OnCreate() инициализирует DirectInput, а затем инициализирует мышь и клавиатуру функциями InitMouse() и InitKeyboard(). Она выглядит так:

    int SmearWin::OnCreate(LPCREATESTRUCT lpCreateStruct) {

     HRESULT r=DirectInputCreate(AfxGetInstanceHandle(), DIRECTINPUT_VERSION, &dinput, 0);

     if (r!=DI_OK) {

      AfxMessageBox("DirectInputCreate() failed");

      return -1;

     }

     if (InitMouse()==FALSE) return -1;

     if (InitKeyboard()==FALSE) return -1;

     if (DirectDrawWin::OnCreate(lpCreateStruct) == -1) return -1;

     return 0;

    }

    DirectInput инициализируется функцией DirectInputCreate(). При успешном вызове в переменную dinput заносится указатель на созданный объект DirectInput. Остальные аргументы DirectInputCreate() рассматривались в программе Qwerty.

    Затем мы вызываем функции, которые инициализируют мышь и клавиатуру. Они рассматриваются ниже. Функция OnCreate() завершается вызовом версии OnCreate() базового класса, инициализирующим DirectDraw.

    Инициализация мыши

    Функция InitMouse() (см. листинг 6.5) готовит мышь к работе.


    Листинг 6.5. Функция InitMouse()

    BOOL SmearWin::InitMouse() {

     HRESULT r;

     r = dinput->CreateDevice(GUID_SysMouse, &mouse, 0);

     if (r!=DI_OK) {

      TRACE("CreateDevice(mouse) failed\n");

      return FALSE;

     }

     r = mouse->SetDataFormat(&c_dfDIMouse);

     if (r!=DI_OK) {

      TRACE("mouse->SetDataFormat() failed\n");

      return FALSE;

     }

     r = mouse->SetCooperativeLevel(GetSafeHwnd(), DISCL_NONEXCLUSIVE | DISCL_FOREGROUND);

     if (r!=DI_OK) {

      TRACE("mouse->SetCooperativeLevel() failed\n");

      return FALSE;

     }

     DIPROPDWORD property;

     property.diph.dwSize=sizeof(DIPROPDWORD);

     property.diph.dwHeaderSize=sizeof(DIPROPHEADER);

     property.diph.dwObj=0;

     property.diph.dwHow=DIPH_DEVICE;

     property.dwData=64;

     r = mouse->SetProperty(DIPROP_BUFFERSIZE, &property.diph);

     if (r!=DI_OK) {

      TRACE("mouse->SetProperty() failed (buffersize)\n");

      return FALSE;

     }

     return TRUE;

    }

    Функция InitMouse() включает в себя четыре этапа:

    1. Создание объекта DirectInputDevice, представляющего мышь.

    2. Определение формата данных, получаемых от мыши.

    3. Установку уровня кооперации для мыши.

    4. Инициализацию буфера данных устройства.

    Функция CreateDevice() интерфейса DirectInput (первый этап) создает экземпляр интерфейса DirectInputDevice, представляющего системную мышь:

    r = dinput->CreateDevice(GUID_SysMouse, &mouse, 0);

    DirectInput предоставляет константу GUID_SysMouse, поэтому для получения нужного GUID можно обойтись без составления списка системных устройств. Если приложение должно поддерживать аппаратные конфигурации, в которых используется более одной мыши, придется вызывать функцию EnumDevices().

    Если вызов функции CreateDevice() прошел успешно, переменной mouse присваивается указатель на созданный объект DirectInputDevice. Третий аргумент должен быть равен нулю, если только вы не пользуетесь агрегированием COM.

    На втором этапе функция SetDataFormat() интерфейса DirectInputDevice сообщает DirectInput формат ожидаемых данных:

    r = mouse->SetDataFormat(&c_dfDIMouse);

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

    На третьем этапе определяется уровень кооперации для мыши:

    r = mouse->SetCooperativeLevel(GetSafeHwnd(), DISCL_NONEXCLUSIVE | DISCL_FOREGROUND);

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

    Остается лишь задать размер буфера данных функцией SetProperty() интерфейса DirectInputDevice. Размер буфера определяет количество событий, сохраняемых в очереди DirectInput. Если буфер слишком мал, возникает риск потери данных из-за его переполнения. Я снова привожу соответствующий фрагмент листинга 6.5:

    DIPROPDWORD property;

    property.diph.dwSize=sizeof(DIPROPDWORD);

    property.diph.dwHeaderSize=sizeof(DIPROPHEADER);

    property.diph.dwObj=0;

    property.diph.dwHow=DIPH_DEVICE;

    property.dwData=64;

    r = mouse->SetProperty(DIPROP_BUFFERSIZE, &property.diph);

    Функция SetProperty() получает два аргумента: величину, которая определяет задаваемое свойство, и адрес структуры DIPROPDWORD. Среди прочего эта структура содержит значение свойства.

    В нашем случае константа DIPROP_BUFFERSIZE говорит о том, что SetProperty() задает размер буфера. Поле dwSize структуры property равно 64; это значит, что мы заказываем буфер данных из 64 элементов. Размер буфера выбирается достаточно произвольно. Он должен быть достаточно большим, чтобы избежать переполнения, и достаточно малым, чтобы не тратить память напрасно.

    Подготовка мыши закончена; осталось лишь захватить ее перед получением данных. Перед тем как захватывать мышь, мы кратко рассмотрим процесс инициализации клавиатуры.

    Инициализация клавиатуры

    Инициализация клавиатуры выполняется функцией InitKeyboard():

    BOOL SmearWin::InitKeyboard() {

     HRESULT r;

     r = dinput->CreateDevice(GUID_SysKeyboard, &keyboard, 0);

     if (r!=DI_OK) {

      TRACE("CreateDevice(keyboard) failed");

      return FALSE;

     }

     r = keyboard->SetDataFormat(&c_dfDIKeyboard);

     if (r!=DI_OK)  {

      TRACE("keyboard->SetDataFormat() failed\n");

      return FALSE;

     }

     r=keyboard->SetCooperativeLevel(GetSafeHwnd(), DISCL_FOREGROUND | DISCL_NONEXCLUSIVE);

     if (r!=DI_OK) {

      TRACE("keyboard->SetCooperativeLevel() failed\n");

      return FALSE;

     }

     return TRUE;

    }

    Инициализация клавиатуры происходит так же, как и в программе Qwerty.

    Захват мыши и клавиатуры

    Перед тем как получать данные от клавиатуры или мыши, необходимо предварительно захватить их. Кроме того, устройство приходится захватывать заново, если приложение некоторое время было неактивным. И исходный, и все последующие захваты устройства выполняются в обработчике OnActivate():

    void SmearWin::OnActivate(UINT nState, CWnd* pWndOther, BOOL bMinimized) {

     DirectDrawWin::OnActivate(nState, pWndOther, bMinimized);

     if (nState!=WA_INACTIVE) {

      if (keyboard) {

       TRACE("keyboard->Acquire()\n");

       keyboard->Acquire();

      }

      if (mouse) {

       TRACE("mouse->Acquire()\n");

       mouse->Acquire();

      }

     }

    }

    Функция Acquire() вызывается для каждого устройства независимо от того, уступалось ли оно. DirectInput игнорирует лишние вызовы Acquire().

    Получение данных от мыши

    Хлопоты с инициализацией мыши и клавиатуры закончены, теперь можно получать от них данные. Функция DrawScene() (см. листинг 6.6) через указатели mouse и keyboard обращается к обоим устройствам и получает от них данные.


    Листинг 6.6. Функция SmearWin::DrawScene()

    void SmearWin::DrawScene() {

     static char key[256];

     keyboard->GetDeviceState(sizeof(key), &key);

     if (key[DIK_ESCAPE] & 0x80) PostMessage(WM_CLOSE);

     BOOL done=FALSE;

     while (!done) {

      DIDEVICEOBJECTDATA data;

      DWORD elements=1;

      HRESULT r=mouse->GetDeviceData(sizeof(data), &data,     &elements, 0);

      if (r==DI_OK && elements==1) {

       switch(data.dwOfs) {

       case DIMOFS_X:

        x+=data.dwData;

        break;

       case DIMOFS_Y:

        y+=data.dwData;

        break;

       }

      } else if (elements==0) done=TRUE;

     }

     BltSurface(primsurf, sphere, x, y, TRUE);

    }

    Функция DrawScene() сначала проверяет состояние клавиатуры функцией GetDeviceState() интерфейса DirectInputDevice. Если была нажата клавиша Escape, она посылает сообщение WM_CLOSE, что приводит к завершению приложения. О функции GetDeviceState() и проверке состояния клавиш рассказано в программе Qwerty, поэтому сейчас мы займемся кодом, относящимся к мыши. DrawScene() в цикле извлекает элементы буфера мыши. Для получения данных, а также для проверки отсутствия элементов при пустом буфере используется функция GetDeviceData() интерфейса DirectInputDevice.

    Каждый элемент буфера представлен структурой DIDEVICEOBJECTDATA. Эта структура используется независимо от типа устройства, поэтому ее поля были сделаны универсальными. DirectInput определяет структуру DIDEVICEOBJECTDATA следующим образом:

    typedef struct {

     DWORD dwOfs;

     DWORD dwData;

     DWORD dwTimeStamp;

     DWORD dwSequence;

    } DIDEVICEOBJECTDATA, *LPDIDEVICEOBJECTDATA;

    Для мыши поле dwOfs определяет тип события. В DirectInput определены следующие константы, описывающие ввод от мыши:

    • DIMOFS_X

    • DIMOFS_Y

    • DIMOFS_Z

    • DIMOFS_BUTTON0

    • DIMOFS_BUTTON1

    • DIMOFS_BUTTON2

    • DIMOFS_BUTTON3

    Программа Smear реагирует только на перемещение мыши по осям x и y, поэтому после вызова функции GetDeviceData() поле dwOfs сравнивается с константами DIMOFS_X и DIMOFS_Y.

    Поле dwData определяет новые значения осевых координат и кнопок. Поскольку мы используем относительные значения, содержимое этого поля равно смещению по данной оси с момента получения последних данных. Следовательно, оно может быть и отрицательным. Поле dwData используется для обновления переменных x и y.

    Поля dwTimeStamp и dwSequence содержат информацию о том, когда произошло данное событие. Поле dwTimeStamp определяет время в миллисекундах (о том, как интерпретируется эта величина, можно подробно узнать в описании функции Win32 GetTickCount()). Поле dwSequence определяет порядок наступления событий. События с меньшими номерами наступили раньше, однако несколько событий могут иметь одинаковые порядковые номера. Например, если мышь или рукоять джойстика смещается по диагонали, события для координат x и y будут иметь одинаковые номера.

    Вернемся к функции DrawScene(). Цикл ввода извлекает элементы буфера до тех пор, пока буфер не опустеет. Этот цикл выглядит так:

    while (!done) {

     DIDEVICEOBJECTDATA data;

     DWORD elements=1;

     HRESULT r=mouse->GetDeviceData(sizeof(data), &data, &elements, 0);

     if (r==DI_OK && elements==1) {

      switch (data.dwOfs) {

      case DIMOFS_X:

       x+=data.dwData;

       break;

      case DIMOFS_Y:

       y+=data.dwData;

       break;

      }

     } else if (elements==0) done=TRUE;

    }

    Третий аргумент GetDeviceData() используется двояко. Значение, передаваемое функции, определяет количество элементов, извлекаемых из буфера. В нашем случае используется всего одна структура DIDEVICEOBJECTDATA, поэтому передается число 1. При возврате из функции это значение показывает количество полученных элементов.

    Если вызов функции прошел успешно и значение elements осталось равным 1, значит, элемент буфера был прочитан, а поля dwOfs и dwData определяют тип события. Нулевое значение elements говорит о том, что буфер пуст и цикл завершается.

    После извлечения всех элементов буфера остается лишь вывести поверхность в позиции, определяемой переменными x и y. Для этого используется функция BltSurface():

    BltSurface(primsurf, sphere, x, y, TRUE);

    Обратите внимание: поверхность изображения копируется непосредственно на первичную поверхность, как говорилось при обсуждении структуры программы Smear.

    Завершение приложения

    Перед завершением приложения MFC вызывает функцию OnDestroy(); мы воспользуемся ею для освобождения объектов DirectInput. Функция OnDestroy() выглядит так:

    void SmearWin::OnDestroy() {

     DirectDrawWin::OnDestroy();

     if (dinput) dinput->Release(), dinput=0;

     if (keyboard) {

      keyboard->Unacquire();

      keyboard->Release(), keyboard=0;

     }

     if (mouse) {

      mouse->Unacquire();

      mouse->Release(), mouse=0;

     }

    }

    Функция OnDestroy() просто освобождает каждый объект DirectInput (и вызывает одноименную функцию базового класса).

    Заключение

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

    В следующей главе мы займемся одной неприятной проблемой, связанной с курсором мыши. Как вы вскоре убедитесь, DirectInput станет неотъемлемой частью ее решения.







     

    Главная | В избранное | Наш E-MAIL | Добавить материал | Нашёл ошибку | Наверх