Книга: Графика для Windows средствами DirectDraw

Программа Cursor

Программа Cursor

Программа Cursor использует описанную выше методику и выводит на экран изображение вращающейся спирали, меню задержки и курсор мыши. По умолчанию программа выводит кадры максимально часто, но меню задержки позволяет уменьшить частоту вывода за счет задержки в основном потоке (максимальная задержка равна 500 миллисекундам, при этом приложение замедляется до 2 FPS). Если бы курсор не управлялся отдельным потоком, его обновление происходило бы лишь с выводом очередного кадра. Но поскольку курсор мыши не зависит от основного потока, он нормально реагирует на действия пользователя при любой частоте вывода. Программа Cursor изображена на рис. 7.1.


Рис. 7.1. Программа Cursor

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

СОВЕТ

Как создать собственный курсор

Программа Cursor может работать с курсором любого размера. В версии программы на CD-ROM использован небольшой курсор (12?20 пикселей), но вы можете легко изменить этот стандартный размер. Для этого достаточно заменить cursor_08.bmp и/или cursor_24.bmp файлами с более крупными изображениями курсоров.

По умолчанию приложение работает в 8-битном видеорежиме и соответственно с 8-битным курсором. Многое зависит от вашего графического редактора, но, скорее всего, вы избавитесь от проблем с палитрой, если воспользуетесь файлом cursor_08.bmp с CD-ROM как шаблоном для создания нестандартного курсора. С курсором формата True Color дело обстоит проще, но, чтобы воспользоваться им, придется слегка подправить функцию SelectInitialDisplayMode(), чтобы активизировать беспалитровый видеорежим вместо палитрового.

Класс CursorWin 

Программа Cursor, как и все остальные программы этой книги, построена на базе структурных классов DirectDrawWin и DirectDrawApp. Эти классы остались неизменными, а вся специфика приложения реализуется классом CursorWin. На практике функциональность курсора мыши, вероятно, следовало бы встроить в структурный класс. И все же для наглядности я объединил код для работы с курсором со специфическим кодом приложения. Класс CursorWin приведен в листинге 7.1.

Листинг 7.1. Класс CursorWin

class CursorWin : public DirectDrawWin {
public:
 CursorWin();
protected:
 //{{AFX_MSG(CursorWin)
 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:
 BOOL InitMouse();
 BOOL InitKeyboard();
 BOOL UpdateDelaySurface();
private:
 //------- Функции потока ввода ------
 static DWORD MouseThread(LPVOID);
 BOOL UpdateCursorSimpleCase(int curx, int cury, int oldcurx, int oldcury);
 BOOL UpdateCursorComplexCase(int curx, int cury, int oldcurx, int oldcury);
private:
 //------- Данные мыши -------
 static LPDIRECTINPUTDEVICE mouse;
 static CCriticalSection critsection;
 static CWinThread* mousethread;
 static CEvent* mouse_event[2];
 static int cursor_width;
 static int cursor_height;
 static LPDIRECTDRAWSURFACE cursor;
 static LPDIRECTDRAWSURFACE cursor_under;
 static LPDIRECTDRAWSURFACE cursor_union;
 static int curx, cury;
 static int oldcurx, oldcury;
 static CList<MouseClickData, MouseClickData> mouseclickqueue;
private:
 //------- Данные приложения -------
 LPDIRECTINPUT dinput;
 LPDIRECTINPUTDEVICE keyboard;
 LPDIRECTDRAWSURFACE coil[coil_frames];
 LPDIRECTDRAWSURFACE dm_surf;
 int dm_index;
 DWORD menubarfillcolor;
 HFONT largefont, smallfont;
};

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

Следующие пять функций наследуются от класса DirectDrawWin:

• SelectDriver()

• SelectInitialDisplayMode()

• CreateCustomSurfaces()

• DrawScene()

• RestoreSurfaces()

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

Затем объявляются функции InitMouse() и InitKeyboard(). Эти функции используются функцией OnCreate() и отвечают за инициализацию объектов DirectInput, предназначенных для работы с мышью и клавиатурой. Функция InitKeyboard() совпадает с одноименными функциями программ Qwerty и Smear из главы 6, поэтому она также не рассматривается. Однако функция InitMouse() помимо инициализации мыши запускает поток ввода. Вскоре мы рассмотрим эту функцию.

Функция UpdateDelaySurface() готовит к выводу поверхность меню задержки. Она выводит текст меню и выделяет текущую задержку.

Далее в классе CursorWin объявляются три функции потока мыши:

• MouseThread()

• UpdateCursorSimpleCase()

• UpdateCursorComplexCase()

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

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

Обратите внимание: в число переменных мыши входят объекты классов CCriticalSection, CEvent и CWinThread, предназначенные для синхронизации двух потоков нашей программы.

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

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

Инициализация приложения 

Наше знакомство с программой Cursor начинается с функции OnCreate(), которая отвечает за инициализацию DirectDraw, DirectInput и потока ввода. Функция OnCreate() приведена в листинге 7.2.

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

int CursorWin::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;
 mousethread->ResumeThread();
 return 0;
}

Сначала OnCreate() инициализирует DirectInput функцией DirectInputCreate(). Затем мышь и клавиатура инициализируются функциями InitMouse() и InitKeyboard(), после чего вызывается функция DirectDrawWin::OnCreate(). Функция InitMouse(), которую мы рассмотрим чуть ниже, создает поток ввода, доступ к которому осуществляется через указатель mousepointer. Однако поток ввода создается в приостановленном состоянии, чтобы он не пытался преждевременно обращаться к первичной поверхности. Поток будет запущен лишь после инициализации DirectDraw. Приостановленный поток активизируется функцией CWinThread::ResumeThread().

Давайте рассмотрим функцию InitMouse(), чтобы получить общее представление об инициализации мыши и создании потока ввода. Функция InitMouse() приведена в листинге 7.3.

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

BOOL CursorWin::InitMouse() {
 HRESULT r;
 r = dinput->CreateDevice(GUID_SysMouse, &mouse, 0);
 if (r!=DI_OK) {
  TRACE("CreateDevice(mouse) failedn");
  return FALSE;
 }
 r = mouse->SetDataFormat(&c_dfDIMouse);
 if (r!=DI_OK) {
  TRACE("mouse->SetDataFormat() failedn");
  return FALSE;
 }
 r = mouse->SetCooperativeLevel(GetSafeHwnd(),    DISCL_NONEXCLUSIVE | DISCL_FOREGROUND);
 if (r!=DI_OK) {
  TRACE("mouse->SetCooperativeLevel() failedn");
  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;
 }
 mouse_event[mouse_event_index]=new CEvent;
 mouse_event[quit_event_index]=new CEvent;
 r = mouse->SetEventNotification(*mouse_event[mouse_event_index]);
 if (r!=DI_OK) {
  TRACE("mouse->SetEventNotification() failedn");
  return FALSE;
 }
 mousethread=AfxBeginThread((AFX_THREADPROC)MouseThread, this, THREAD_PRIORITY_TIME_CRITICAL, 0, CREATE_SUSPENDED);
 return TRUE;
}

Функция InitMouse() состоит из семи этапов:

1. Инициализация устройства DirectInput, которое представляет мышь.

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

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

4. Инициализация буфера данных мыши.

5. Создание двух объектов CEvent.

6. Инициализация механизма оповещений DirectInput.

7. Создание потока ввода.

На этапах 1-4 происходит нормальная инициализация DirectInput, подробно рассмотренная в главе 6, поэтому основное внимание будет уделено этапам 5, 6 и 7.

На этапе 5 создаются два динамических объекта CEvent, а полученные указатели сохраняются в маленьком массиве. Положение этих указателей в массиве определяется константами mouse_event_index и quit_event_index (которые равны 0 и 1 соответственно). Первое событие блокирует или активизирует поток ввода в зависимости от того, поступили ли от мыши новые данные. Второе событие сообщает потоку мыши о завершении приложения. Как мы вскоре увидим, указатели сохраняются в массиве для того, чтобы мы могли заблокировать поток мыши по двум событиям одновременно.

На этапе 6 функция SetEventNotification() интерфейса DirectInputDevice приказывает DirectInput устанавливать событие мыши при появлении новых данных. Функция SetEventNotification() получает один аргумент типа HANDLE, однако наш объект CEvent наследует оператор преобразования типа от класса CSyncObject, благодаря чему мы можем использовать объект CEvent так, словно он имеет тип HANDLE (тип HANDLE, в частности, используется потоковым API Win32 для представления событий).

На этапе 7 создается поток ввода от мыши. Я снова приведу соответствующий фрагмент листинга 7.2:

mousethread=AfxBeginThread((AFX_THREADPROC)MouseThread, this, THREAD_PRIORITY_TIME_CRITICAL, 0, CREATE_SUSPENDED);

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

Первый аргумент AfxBeginThread — указатель на функцию, выполняемую новым потоком; в нашем случае используется функция MouseThread(). Второй аргумент — значение, которое передается функции потока при вызове. Мы передаем указатель this, чтобы функция MouseThread() могла обращаться к членам нашего класса.

Третий аргумент — приоритет потока. По умолчанию для потока устанавливается нормальный приоритет (флаг THREAD_PRIORITY_NORMAL), но мы переопределяем его и задаем флаг THREAD_PRIORITY_TIME_CRITICAL, чтобы добиться наискорейшего отклика курсора.

Четвертый аргумент — размер стека для нового потока. Ноль означает, что размер стека выбирается по умолчанию. Пятый и последний аргумент определяет исходное состояние потока. Если он равен нулю, создается активный поток; в нашем случае использован флаг CREATE_SUSPENDED, чтобы создавался приостановленный поток.

На создании потока ввода работа функции InitMouse() заканчивается. Благодаря флагу CREATE_SUSPENDED поток ввода приостанавливается до момента, когда основной поток завершит инициализацию DirectDraw. Затем, перед возвратом из функции OnCreate(), поток ввода активизируется функцией ResumeThread() (см. листинг 7.2).

Функция DrawScene() 

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

Листинг 7.4. Функция DrawScene()

void CursorWin::DrawScene() {
 //------ Проверить клавишу ESCAPE -------
 static char key[256];
 keyboard->GetDeviceState(sizeof(key), &key);
 if (key[DIK_ESCAPE] & 0x80) PostMessage(WM_CLOSE);
 //------ Обычные задачи ------
 ClearSurface(backsurf, 0);
 BltSurface(backsurf, dm_surf, 539, 0);
 static coil_idx;
 BltSurface(backsurf, coil[coil_idx], coilx, coily);
 coil_idx=(coil_idx+1)%coil_frames;
 //------ Начало синхронизированной секции ------
 critsection.Lock();
 //------ Сохранить область вторичного буфера под курсором
 RECT src;
 src.left=curx;
 src.top=cury;
 src.right=curx+cursor_width;
 src.bottom=cury+cursor_height;
 cursor_under->BltFast(0, 0, backsurf, &src, DDBLTFAST_WAIT);
 //------ Нарисовать курсор во вторичном буфере
 backsurf->BltFast(curx, cury, cursor, 0, DDBLTFAST_SRCCOLORKEY | DDBLTFAST_WAIT);
 primsurf->Flip(0, DDFLIP_WAIT);
 while (primsurf->GetFlipStatus(DDGFS_ISFLIPDONE)!=DD_OK);
 // ничего не делать (ждать, пока закончится
 // переключение страниц)
 int x, y;
 BOOL newclick=FALSE;
 int count=mouseclickqueue.GetCount();
 while (count--) {
  MouseClickData mc=mouseclickqueue.RemoveTail();
  if (mc.button==0) {
   x=mc.x;
   y=mc.y;
   newclick=TRUE;
  }
 }
 critsection.Unlock();
 //------ Конец синхронизированной секции -------
 //------ Сделать паузу в соответствии с выбранной задержкой ----
 if (delay_value[dm_index]!=0) Sleep(delay_value[dm_index]);
 //------ Обновить меню задержки --------
 if (newclick) {
  int max_index=sizeof(delay_value)/sizeof(int)-1;
  int menux=screen_width-dm_width+dm_margin;
  int menuw=dm_width-dm_margin*2;
  if (x>=menux && x<=menux+menuw) {
   int index=(y-dm_header)/dm_entrysize;
   if (index>=0 && index<=max_index && index!=dm_index) {
    dm_index=index;
    UpdateDelaySurface();
   }
  }
 }
}

Функция DrawScene() состоит из семи этапов:

1. Проверка клавиши Escape.

2. Подготовка нового кадра во вторичном буфере.

3. Обновление курсора (также во вторичном буфере).

4. Переключение страниц.

5. Проверка очереди событий мыши.

6. Проверка очереди событий мыши.

7. Обновление поверхности меню задержки.

Первый этап выполняется функцией GetDeviceState() интерфейса DirectInputDevice. Если будет обнаружено нажатие клавиши Escape, функция посылает сообщение WM_CLOSE, сигнализируя о завершении приложения.

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

На этапах 3, 4 и 5 программа обращается к ресурсам, используемым потоком ввода, поэтому необходимо воспользоваться критической секцией. Объект класса CCriticalSection(critsection), объявленный в классе CursorWin (см. листинг 7.1), блокируется функцией Lock(). Эта функция пытается получить доступ к критической секции. Если попытка оказывается удачной, функция захватывает критическую секцию и завершается. После этого можно смело работать с совместными ресурсами — поток заведомо обладает монопольным правом доступа к ним. Если функции Lock() будет отказано в доступе (из-за того что критическая секция в данный момент захвачена потоком ввода), функция Lock() блокирует основной поток до освобождения критической секции.

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

На этапе 4 выполняется переключение страниц, однако оно происходит сложнее, чем обычно. Это связано с тем, что функция Flip() интерфейса DirectDrawSurface на самом деле не выполняет переключения. Она лишь приказывает видеокарте переключить страницы и после этого завершается. Фактическое переключение страниц происходит после того, как будут закончены все ранее начатые операции блиттинга во вторичный буфер. Для наших целей этого недостаточно. Нам нужно, чтобы переключение страниц было закончено до кода критической секции, потому что в противном случае поток ввода сможет обновить первичную поверхность во время переключения страниц. С помощью цикла while и функции GetFlipStatus() интерфейса DirectDrawSurface мы опрашиваем DirectDraw до тех пор, пока переключение страниц не закончится (в DirectDraw не предусмотрена блокировка по этой операции, но даже если бы она и была, переключение страниц происходит слишком быстро и не оправдывает блокировки потока).

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

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

Этап 6 не требует синхронизации, поэтому мы вызываем функцию CCriticalSection::Unlock(). Если к этому моменту поток ввода был заблокирован и ожидал доступа к своей критической секции, вызов Unlock() позволит ему войти в нее.

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

Теперь мы знаем, как происходит обновление экрана в основном потоке. Давайте посмотрим, как работает поток ввода.

Поток ввода 

Если не считать двух вспомогательных функций, весь поток ввода реализован в виде одной функции. Функция MouseThread() приведена в листинге 7.5.

Листинг 7.5. Функция MouseThread()

DWORD CursorWin::MouseThread(LPVOID p) {
 TRACE("starting mouse threadn");
 CursorWin* win=(CursorWin*)p;
 while(TRUE) {
  CMultiLock mlock((CSyncObject**)mouse_event, 2);
  DWORD event=mlock.Lock(INFINITE, FALSE);
  if (event-WAIT_OBJECT_0==quit_event_index)  {
   TRACE("got quit message: quitting mouse threadn");
   return 0;
  }
  critsection.Lock();
  oldcurx=curx;
  oldcury=cury;
  BOOL buffer_empty=FALSE;
  while (!buffer_empty) {
   DIDEVICEOBJECTDATA data;
   DWORD elements=1;
   if (mouse==0) {
    TRACE("invalid pointer: quitting mouse threadn");
    return 0;
   }
   HRESULT r=mouse->GetDeviceData(sizeof(data), &data, &elements, 0);
   if (r==DI_OK && elements==1)   {
    static MouseClickData mc;
    switch data.dwOfs) {
    case DIMOFS_X:
     curx+=data.dwData;
     break;
    case DIMOFS_Y:
     cury+=data.dwData;
     break;
    case DIMOFS_BUTTON0:
     if (data.dwData & 0x80) {
      mc.x=curx;
      mc.y=cury;
      mc.button=0;
      mouseclickqueue.AddHead(mc);
     }
     break;
    case DIMOFS_BUTTON1:
     if (data.dwData & 0x80) {
      mc.x=curx;
      mc.y=cury;
      mc.button=1;
      mouseclickqueue.AddHead(mc);
     }
     break;
    }
   } else buffer_empty=TRUE;
  }
  if (curx<0) curx=0;
  if (cury<0) cury=0;
  if (curx>=screen_width-cursor_width) curx=screen_width-cursor_width-1;
  if (cury>=screen_height-cursor_height) cury=screen_height-cursor_height-1;
  if (curx==oldcurx && cury==oldcury) {
   //----- обновление курсора не требуется ------
   goto nevermind;
  } else if (abs(curx-oldcurx) >= cursor_width || abs(cury-oldcury) >= cursor_height) {
   //----- простой случай: прямоугольники нового
   // и старого курсора не перекрываются -----
   win->UpdateCursorSimpleCase(curx, cury, oldcurx, oldcury);
  } else {
   //----- сложный случай: прямоугольники нового
   // и старого курсора перекрываются -----
   win->UpdateCursorComplexCase(curx, cury, oldcurx, oldcury);
  }
  nevermind:;
  critsection.Unlock();
 }
 TRACE("leaving mouse threadn");
 return 0;
};

Функция MouseThread() имеет один параметр — значение, передаваемое функции AfxBeginThread() при создании потока (см. листинг 7.3). Мы передавали указатель this, поэтому сейчас сможем присвоить его значение указателю на класс CursorWin (переменная win). В функции MouseThread() указатель win будет использоваться для доступа к членам класса CursorWin.

Функция MouseThread() в цикле выполняет блокировку по двум событиям. Класс CMultiLock позволяет блокироваться как по событиям от мыши, так и по событию завершения потока. Фактическая блокировка выполняется функцией CMultiLock::Lock(). По умолчанию функция Lock() блокирует поток до установки всех (в данном случае  - двух) заданных событий. Мы изменяем это поведение и передаем FALSE в качестве второго аргумента Lock(), показывая тем самым, что функция должна снимать блокировку при установке хотя бы одного из этих событий.

Когда любое из двух событий переходит в установленное состояние, функция Lock() завершается, и мы проверяем код возврата. Если выясняется, что было установлено событие завершения потока (обозначенное константой quit_event_index), мы выходим из функции MouseThread(), тем самым завершая поток. В противном случае активизация потока вызвана событием мыши, поэтому мы переходим к обработке новых данных.

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

Мы в цикле получаем данные от объекта DirectInputDevice, представляющего мышь, с помощью функции GetDeviceData(). Если получены данные о перемещении мыши, происходит обновление переменных curx и cury. Если получены данные о нажатии кнопок, они заносятся в очередь событий.

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

Наконец, мы проверяем новое положение курсора. Если перемещение курсора не обнаружено, критическая секция освобождается, а объект CMultiLock снова используется для блокировки по обоим событиям. Если курсор переместился в другое положение, мы вызываем одну из двух функций обновления курсора в зависимости от того, перекрывается ли старая область курсора с новой. Если области перекрываются, вызывается функция UpdateCursorComplexCase(); в противном случае вызывается функция UpdateCursorSimpleCase().

Начнем с более простой функции UpdateCursorSimpleCase() (см. листинг 7.6).

Листинг 7.6. Функция UpdateCursorSimpleCase()

BOOL CursorWin::UpdateCursorSimpleCase(int curx, int cury, int oldcurx, int oldcury) {
 RECT src;
 HRESULT r;
 //------ Блиттинг 1: стирание старого курсора ----------
 r=primsurf->BltFast(oldcurx, oldcury, cursor_under, 0, DDBLTFAST_WAIT);
 if (r!=DD_OK) {
  TRACE("Blt 1 failedn");
  CheckResult(r);
 }
 //------ Блиттинг 2: сохранение области под новым курсором ------
 src.left=curx;
 src.top=cury;
 src.right=curx+cursor_width;
 src.bottom=cury+cursor_height;
 r=cursor_under->BltFast(0, 0, primsurf, &src, DDBLTFAST_WAIT);
 if (r!=DD_OK) {
  TRACE("Blt 2 failedn");
  CheckResult(r);
 }
 //------ Блиттинг 3: рисование нового курсора ----------
 r=primsurf->BltFast(curx, cury, cursor, 0, DDBLTFAST_SRCCOLORKEY | DDBLTFAST_WAIT);
 if (r!=DD_OK) {
  TRACE("Blt 3 failedn");
  CheckResult(r);
 }
 return TRUE;
}

С помощью трех последовательных вызовов функции BltFast() интерфейса DirectDrawSurface, функция UpdateCursorSimpleCase() стирает существующий курсор, сохраняет область под новым курсором и рисует новый курсор.

В UpdateCursorComplexCase() функция BltFast() вызывается пять раз. Два дополнительных блиттинга предназначены для копирования обновляемой части первичной поверхности на вспомогательную поверхность (cursor_union) и обратно. Функция UpdateCursorComplexCase() приведена в листинге 7.7.

Листинг 7.7. Функция UpdateCursorComplexCase()

BOOL CursorWin::UpdateCursorComplexCase(int curx, int cury, int oldcurx, int oldcury) {
 RECT src;
 HRESULT r;
 int unionx=min(curx, oldcurx);
 int uniony=min(cury, oldcury);
 int unionw=max(curx, oldcurx)-unionx+cursor_width;
 int unionh=max(cury, oldcury)-uniony+cursor_height;
 //----- Блиттинг 1: копирование объединяющего прямоугольника
 // во вспомогательный буфер --------
 src.left=unionx;
 src.top=uniony;
 src.right=unionx+unionw;
 src.bottom=uniony+unionh;
 r=cursor_union->BltFast(0, 0, primsurf, &src, DDBLTFAST_WAIT);
 if (r!=DD_OK) {
  TRACE("Blt 1 failedn");
  CheckResult(r);
 }
 //------ Блиттинг 2: стирание старого курсора
 // во вспомогательном буфере ---------
 r=cursor_union->BltFast(oldcurx-unionx, oldcury-uniony, cursor_under, 0, DDBLTFAST_WAIT);
 if (r!=DD_OK) {
  TRACE("Blt 2 failedn");
  CheckResult(r);
 }
 //------ Блиттинг 3: сохранение области под новым курсором -----
 src.left=curx-unionx;
 src.top=cury-uniony;
 src.right=src.left+cursor_width;
 src.bottom=src.top+cursor_height;
 r=cursor_under->BltFast(0, 0, cursor_union, &src, DDBLTFAST_WAIT);
 if (r!=DD_OK) {
  TRACE("Blt 3 failedn");
  CheckResult(r);
 }
 //------ Блиттинг 4: рисование нового курсора
 // во вспомогательном буфере ---------
 r=cursor_union->BltFast(curx-unionx, cury-uniony, cursor, 0, DDBLTFAST_SRCCOLORKEY | DDBLTFAST_WAIT);
 if (r!=DD_OK) {
  TRACE("Blt 4 failedn");
  CheckResult(r);
 }
 //------- Блиттинг 5: копирование вспомогательного буфера
 // на первичную поверхность --------
 src.left=0;
 src.top=0;
 src.right=unionw;
 src.bottom=unionh;
 r=primsurf->BltFast(unionx, uniony, cursor_union, &src, DDBLTFAST_WAIT);
 if (r!=DD_OK) {
  TRACE("Blt 5 failedn");
  CheckResult(r);
 }
 return TRUE;
}

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

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

Осталось лишь поговорить о том, как завершается работа приложения. Эта тема неоднократно рассматривалась, и ее можно было бы пропустить, но для программы Cursor она важна из-за наличия дополнительного потока. Мы должны не только послать потоку ввода сигнал о завершении, но и проследить за тем, чтобы поток завершился до уничтожения объекта устройства мыши и поверхностей DirectDraw. В противном случае он может попытаться обратиться к мыши или обновить первичную поверхность после того, как соответствующие объекты перестанут существовать. Функция OnDestroy() выглядит так:

void CursorWin::OnDestroy() {
 critsection.Lock();
 DirectDrawWin::OnDestroy();
 if (mouse) {
  TRACE("mouse->Unacquire()n");
  mouse->Unacquire();
  TRACE("sending mouse quit message...n");
  mouse_event[quit_event_index]->SetEvent();
  Sleep(100);
  // дать потоку мыши возможность ответить
  TRACE("Releasing mouse pointer...n");
  mouse->Release(), mouse=0;
  delete mouse_event[mouse_event_index];
  delete mouse_event[quit_event_index];
 }
 if (keyboard) keyboard->Release(), keyboard=0;
 if (dinput) dinput->Release(), dinput=0;
 critsection.Unlock();
}

Когда MFC вызывает функцию OnDestroy(), основной поток заведомо не обновляет экран, потому что он занят выполнением этой функции. Тем не менее мы не знаем, не обновляется ли экран потоком ввода. Чтобы поток ввода закончил последнее обновление, мы блокируем критическую секцию.

Далее мы уступаем мышь. Устройство перестает генерировать новые события, которые заставили бы поток ввода попытаться снова обновить экран. Затем функция CEvent::SetEvent() посылает потоку ввода сигнал о завершении.

Нам осталось лишь освободить объекты DirectInput. Но перед тем, как это делать, мы вызываем функцию Sleep(), чтобы ненадолго приостановить основной поток. Поток ввода получает возможность обработать событие и завершиться. Наконец, мы освобождаем критическую секцию, и функция завершается — на этом работа приложения заканчивается.

Заключение 

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

Оглавление книги


Генерация: 0.048. Запросов К БД/Cache: 0 / 0
поделиться
Вверх Вниз