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

Программа Qwerty

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

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

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

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

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

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


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

Рис. 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() инициализирует и настраивает 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 <AfxWin.h>
#include <AfxExt.h>
#include <AfxTempl.h>
#include <fstream.h>
#include <math.h>
#include <ddraw.h>
#define DIRECTINPUT_VERSION 0x0300
#include <dinput.h>

В приведенном фрагменте присутствуют директивы #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.


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

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