Взаимодействие классов



Взаимодействие классов

Класс CPropDlg должен обеспечить реакцию на изменение регулировок, а класс COpenGL должен учесть новые установки и перерисовать изображение. Общение классов, как мы уже отметили, происходит по законам СОМ, то есть с помощью указателя на интерфейс. Здесь нам на помощь приходит шаблон классов CComQiPtr. Литеры «QI» в имени шаблона означают Querylnterface, что обещает нам автоматизацию в реализации запроса указателя на этот интерфейс. В классе переопределены операции выбора (->), взятия адреса (&), разадресации (*) и некоторые другие, которые упрощают использование указателей на различные интерфейсы. При создании объекта класса CComQiPtr, например:

CComQIPtr<IOpenGL, &IID_IOpenGL> р(m_ppUnk[i]) ;

он настраивается на нужный нам интерфейс, и далее мы работаем с удобствами, не думая о функциях Querylnterface, AddRef и Release. При выходе из области действия объекта р класса CGomQiPtr<lOpenGL, &ilD_iOpenGL> освобождение интерфейса произойдет автоматически.

Для обмена с окном диалоговой вставки введите в protected-секцию класса CPropDlg массив текущих позиций регуляторов и переменную для хранения текущего режима изображения полигонов:

protected:

int m_Pos[11]; BOOL m_bQuad;

В конструктор класса добавьте код инициализации массива:

ZeroMemory (m_Pos, sizeof(m_Pos));

Другую переменную следует инициализировать при открытии диалога (вставки). Способом, который вы уже неоднократно применяли, введите в класс реакции на Windows-сообщения WM_INITDIALOG и WM_HSCROLL. Затем перейдите к созданной мастером заготовке метода Onl nit Dialog, которую найдете в файле PropDlg.cpp:


LRESULT CPropDlg::OnInitDialog(UINT uMsg, WPARAM wParam,

LPARAM IParam, BOOL& bHandled)

{

_super::OnInitDialog(uMsg, wParam, IParam, bHandled);

return 1;

}

Здесь вы увидите новое ключевое слово языка _ super, которое является спецификой Microsoft-реализации. Оно представляет собой не что иное, как явный вызов родительской версии функции метода базового или super-класса. Так как классы в ATL имеют много родителей, то _ super обеспечивает выбор наиболее подходящего из них. Теперь введите изменения, которые позволят при открытии вкладки привести наши регуляторы в соответствие со значениями переменных в классе COpenGL. Вы помните, что значения регулировок используются именно там. Там же они и хранятся:

LRESULT CPropDlg: :OnInitDialog (UINT uMsg, WPARAM wParam,

LPARAM IParam, BOOL& bHandled)

_super::OnInitDialog(uMsg, wParam, IParam, -bHandled);

//====== Кроим умный указатель по шаблону IQpenGL

CComQIPtr<IOpenGL> p(m_ppUnk[0]);

//=== Пытаемся связаться с классом COpenGL и выяснить

//=== значение переменной m_FillMode

//=== В случае неудачи даем сообщение об ошибке

DWORD mode;

if FAILED (p->GetFillMode(&mode))

{

ShowError();

return 0;

}

//====== Работа с combobox по правилам API

//====== Получаем Windows-описатель окна

HWND hwnd = GetDlgItem(IDC_FILLMODE);

//====== Наполняем список строками текста

SendMessage(hwnd, CB_ADDSTRING, 0, (LPARAM)(LPCTSTR)"Points"

SendMessage(hwnd, CB_ADDSTRING, 0, (LPARAM)(LPCTSTR)"Lines")

SendMessage(hwnd, CB_ADDSTRING, 0, (LPARAM)(LPCTSTR)"Fill");

// Выбираем текущую позицию списка в соответствии

// со значением, полученным из COpenGL WPARAM

w = mode == GL_POINT ? 0

: mode == GL_LINE ?1:2;

SendMessage(hwnd, CB_SETCURSEL, w, 0);

// Повторяем сеанс связи, выясняя позиции ползунков

if FAILED (p->GetLightParams(m_Pos))

{

ShowError();

return 0;

}

// Мы не надеемся на упорядоченность идентификаторов

// элементов и поэтому заводим массив отображений

UINT IDs[] =

{

IDC_XPOS,

IDC_YPOS,

IDC_ZPOS,

IDC_AMBIENT,

IDC_DIFFUSE,

IDC_SPECULAR,

IDC_AMBMAT,

IDC_DIFFMAT,

IDC_SPECMAT,

IDC_SHINE,

IDC_EMISSION

};

//=== Пробег по всем регуляторам и их установка

for (int i=0;

Ksizeof (IDs)/sizeof (IDs [0] ) ; i++)

{

//====== Получаем описатель окна

hwnd = GetDlgItem(IDs[i]);

UINT nID;

//====== Узнаем идентификатор элемента

int num = GetSliderNum(hwnd, nID);

//====== Выставляем позицию

~ SendMessage(hwnd,TBM_SETPOS,TRUE,(LPARAM)m_Pos[i]

//=== Приводим в соответствие текстовый ярлык

char s [ 8 ] ;

sprintf (s,"%d",m_Pos[i]);

SetDlgltemText(nID, s);

}

// Выясняем состояние режима изображения полигонов

if FAILED (p->GetQuad(&m_bQuad))

{

ShowError ();

return 0;

}

//====== Устанавливаем текст

SetDlgltemText (IDC_QUADS,m_bQuad ? '"Quads" : "Strips");

return 1 ;

}

В процессе обработки сообщения нам понадобились вспомогательные функции GetSliderNum и ShowError. Первая функция уже участвовала в проекте на основе MFC, поэтому мы лишь напомним, что она позволяет по известному Windows-описателю окна элемента управления получить его порядковый номер в массиве позиций регуляторов. Кроме этого, функция позволяет получить идентификатор элемента управления nio, который нужен для управления им, например: при вызове SetDlgltemText (nID, s);.

int CPropDlg: : GetSliderNum (HWND hwnd, UINT& nID)

{

// Получаем ID по известному описателю окна

switch (: :GetDlgCtrlI)(hwnd) )

{

case IDC_XPOS:

nID = IDC_XPOS_TEXT;

return 0; case IDC_YPOS:

nID = IDC_YPOS_TEXT;

return 1 ; case IDC_ZPOS:

nID = IDC_ZPOS_TEXT;

return 2; case IDC_AMBIENT:

nID = IDC_AMB_TEXT;

return 3; case IDC_DIFFUSE:

nID = IDC_DIFFUSE_TEXT;

return 4 ;

case IDC_SPECULAR:

nID = 1DC_SPECULAR_TEXT;

return 5; case IDC_AMBMAT:

nID = IDC_AMBMAT_TEXT;

return 6; case IDC_DIFFMAT:

nID = IDC_DIFFMAT_TEXT;

return 7; case IDC_SPECMAT:

nID = IDC_SPECMAT_TEXT;

return 8; case IDC_SHINE:

nID = IDC_SHINE_TEXT;

return 9; case IDC_EMISSION:

nID = IDC_EMISSION_TEXT;

return 10;

}

return 0;

}

Функция showError демонстрирует, как в условиях СОМ можно обработать исключительную ситуацию. Если мы хотим выявить причину ошибки, спрятанную в HRESULT, то следует воспользоваться методом GetDescription интерфейса lErrorinfо. Сначала мы получаем указатель на него с помощью объекта класса ccomPtr. Этот класс, так же как и CGomQiPtr, автоматизирует работу с методами главного интерфейса lUnknown, за исключением метода Queryinterface:

void CPropDlg::ShowError()

{

USES_CONVERSION;

//====== Создаем инерфейсный указатель

CComPtr<IErrorInfo> pError;

//====== Класс для работы с Unicode-строками

CComBSTR sError;

//====== Выясняем причину отказа

GetErrorlnfo (0, &pError);

pError->GetDescription(SsError);

// Преобразуем тип строкового объекта для вывода в окно MessageBox(OLE2T(sError),_T("Error"),MB_ICONEXCLAMATION);

}

Если вы построите сервер в таком виде, то вас встретит неприятное сообщение о том, что ни один из явных или неявных родителей CPropDlg не имеет в своем составе функции OninitDialog. Обращаясь за справкой к документации (по классу CDialogimpl), мы убеждаемся, что это действительно так. Значит, инструмент Studio.Net, который создал заготовку функции обработки, не прав. Но как же будет вызвана наша функция OninitDialog, если она не является виртуальной функцией одного из базовых классов? Ответ на этот вопрос, как и на большинство других, можно получить в режиме отладки.

Закомментируйте строку вызова родительской версии, которая производится с помощью многообещающего ключевого слова _super (это и есть лекарство), поставьте точку останова на строке, следующей за ней, и нажмите F5. Если вы не допустили еще одной, весьма вероятной, ошибки, то тестовый контейнер сообщит, что он не помощник в процессе отладки, так как не содержит отладочной информации. Согласитесь с очевидным фактом, но не делайте поспешного вывода о том, что невозможно отлаживать все СОМ-серверы. В тот момент, когда вы инициируете новую страницу свойств, отладчик возьмет управление в свои руки и остановится на нужной строке программы. Теперь вызовите одно из самых полезных окон отладчика по имени Call stack, в нем вы увидите историю вызова функции OninitDialog, то есть цепочку вызовов функций. Для этого:

  1. Дайте команду Debug > Windows > Call Stack (или Alt+7).
  2. Внедрите это окно, если необходимо, в блок окон отладчика (внизу экрана).
  3. Убедитесь, что вызов произошел из функции DialogРгос одного из базовых классов, точнее шаблонов классов, CDialoglmplBaseT.

Этот опыт иллюстрирует тот факт, что все необычно в мире ATL. Этот мир устроен совсем не так, как MFC. Шаблоны классов дают удивительную гибкость всей конструкции, способность приспосабливаться и подстраиваться. Теперь рассмотрим вторую, весьма вероятную, ошибку. Секцию protected в классе CPropDlg следует правильно разместить (странно, не правда ли?). Лучше это сделать так, чтобы сразу за ней шло объявление какой-либо из существующих секций public. Если поместить ее, например, перед макросом

DECLARE_REGISTRY_RESOURCEID(IDR__PROPDLG)

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

Сообщение о прокрутке в окне

Сообщение WM_HSCROLL приходит в окно диалога (читайте: объекту диалогового класса, связанного с окном) всякий раз, как пользователь изменяет положение одного из ползунков, расположенных на лице диалога. Это довольно удобно, так как мы можем в одной функции обработки (onHScroll) отследить изменения, произошедшие в любом из 11 регуляторов. Введите коды обработки этого сообщения, которые сходны с кодами, приведенными в приложении на основе MFC, за исключением СОМ-специфики общения между классами CPropDlg и COpenGL:

LRESULT CPropDlg::OnHScroll(UINT /*uMsg*/, WPARAM wParam,

LPARAM iParam, BOOL& /*bHandled*/)

{

//====== Информация о событии запакована в wParara

int nCode = LOWORD(wParam), nPos = HIWORD(wParam), delta, newPos;

HWND hwnd = (HWND) IParam;

// Выясняем номер и идентификатор активного ползунка

UINT nID;

int num = GetSliderNum(hwnd, nID);

//====== Выясняем суть события

switch (nCode)

{

case SB_THUMBTRACK:

case SBJTHUMBPOSITION:

m_Pos[num] = nPos;

break;

//====== Сдвиг до упора влево (клавиша Home)

case SB_LEFT:

delta = -100;

goto New_Pos;

//====== Сдвиг до упора вправо (клавиша End)

case SB_RIGHT:

delta = + 100;

goto New_Pos;

case SB_LINELEFT:

// И т.д.

delta = -1;

goto New_Pos;

case SB_LINERIGHT:

delta = +1;

goto New_Pos;

case SB_PAGELEFT:

delta = -20;

goto New_Pos;

case SB_PAGERIGHT:

delta = +20;

goto New_Pos;

New_Pos:

newPos = m_Pos[num] + delta;

m_Pos[num] = newPos<0 ? 0

: newPos>100 ? 100 : newPos;

break;

case SB_ENDSCROLL: default:

return 0;

}

//=== Готовим текстовое выражение позиции ползунка

char s[8];

sprintf (s,"%d",m_Pos[num]);

SetDlgltemText(nID, (LPCTSTR)s);

//====== Цикл пробега по всем объектам типа PropDlg

for (UINT i = 0; i < m_nObjects; )

//====== Добываем интеофейсн:

//====== Добываем интерфейсный указатель

CComQIPtr<IOpenGL, &IID_IOpenGL> p (m_ppUnk[i] ) ;

//====== Устанавливаем конкретный параметр

if FAILED (p->SetLightParam (num, m_Pos [num] ) )

ShowError();

return 0;

}

}

return 0;

}

В данный момент вы можете проверить функционирование регуляторов в суровых условиях СОМ. Они должны работать.

Реакция на выбор в окне выпадающего списка

Теперь введем реакцию на выбор пользователем новой строки в окне выпадающего списка. Для этого выполните следующие действия:

  1. Откройте в окне редактора Studio.Net шаблон окна диалога IDD_PROPDLG.
  2. Поставьте фокус в окно выпадающего списка IDC_FILLMODE и переведите фокус окно Properties.
  3. Нажмите кнопку Control Events, расположенную на инструментальной панели окна Properties.
  4. Найдите строку с идентификатором уведомляющего сообщения CBN_SELCHANGE и в ячейке справа выберите действие <Add>, для того чтобы там появилось имя функции обработки OnSelchangeFillmode.
  5. Перейдите в окно PropDlg.cpp и введите следующие коды в заготовку функции OnSelchangeFillmode.

LRESULT CPropDlg

::OnSelchangeFillmode(WORD/*wNotifyCode*/, WORD /*wID*/,

HWND hWndCtl, BOOL& bHandled)

{

//====== Цикл пробега по всем объектам типа PropDlg

for (UINT i = 0; i < m_nObjects; i++)

{

CComQIPtr<IOpenGL, &IID_IOpenGL> p(m_ppUnk[i]);

// Выясняем индекс строки, выбранной в окне списка

DWORD sel = (DWORD)SendMessage(hWndCtl, CB_GETCURSEL,0,0);

// Преобразуем индекс в режим отображения полигонов

sel = sel==0 ? GL_POINT

: sel==l ? GL_LINE : GL_FILL;

//====== Устанавливаем режим в классе COpenGL

if FAILED (p->SetFillMode(sel))

{

ShowError();

return 0;

}

}

bHandled = TRUE;

return 0;

}

Обратите внимание на то, что нам пришлось убирать два комментария, чтобы сделать видимым параметры hWndCtl и bHandled.

Реакция на нажатия кнопок

При создании отклика на выбор режима изображения полигонов следует учесть попеременное изменение текста и состояния кнопки. Поставьте курсор на кнопку IDC_QUADS и в окне Properties нажмите кнопку Control Events. Затем найдите строку с идентификатором уведомляющего сообщения BN_CLICKED и в ячейке справа выберите действие <Add>. Текст в ячейке должен измениться и стать OnClickedQuads. Введите следующие коды в заготовку функции:

LRESULT CPropDlg::OnClickedQuads(WORD /*wNotifyCode*/,

WORD /*wID*/, HWND /*hWndCtl*/, BOOL& bHandled)

{

//====== По всем объектам PropDlg

for (UINT i = 0; i < m_nObjects; i++)

{

//====== Добываем интерфейсный указатель

CComQIPtr<IOpenGL, &IID_IOpenGL> p(m_ppUnk[i]) ;

//====== Переключаем режим

m_bQuad = !m_bQuad;

//====== Устанавливаем текст на кнопке

SetDlgltemText(IDC_QUADS, m_bQuad ? "Quads" : "Strip");

if FAILED (p->SetQuad(m_bQuad))

{

ShowError();

return 0;

bHandled = TRUE;

return 0;

}

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

LRESULT CPropDlg: rOnCl'ickedFilename (WORD /*wNotif yCode*/,

WORD /*wID*/, HWND /*hWndCtl*/, BOOL& bHandled)

{

for (UINT i = 0; i < m_nObjects; i++)

{

CComQIPtr<IOpenGL, &IID_IOpenGL> p (m_ppUnk [i] ) ;

//====== Вызываем функцию класса COpenGL

if FAILED (p->ReadData() )

{

ShowError () ;

return 0 ;

}

bHandled = TRUE;

return 0;

}

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

Управление объектом с помощью мыши

Алгоритм управления ориентацией объекта с помощью мыши мы разработали ранее. Вы помните, что перемещение курсора мыши при нажатой кнопке должно вращать изображение, причем горизонтальное перемещение вращает его вокруг вертикальной оси Y, а вертикальное — вокруг горизонтальной оси X. Если одновременно с мышью нажата клавиша Ctrl, то объект перемещается (glTranslatef) вдоль осей X и Y. Наконец, с помощью правой кнопки изображение перемещается вдоль оси Z, то есть приближается или отдаляется. Таймер помогает нам в том, что продолжает вращение, если очередной квант перемещения мышью стал выше порога чувствительности. Скорость вращения имеет два пространственных компонента, которые пропорциональны разности двух последовательных во времени координат курсора. Чем быстрее движется курсор при нажатой левой кнопке, тем большая разность координат будет обнаружена в обработчике сообщения WM_MOUSEMOVE. Именно в этой функции оценивается желаемая скорость вращения.

Описанный алгоритм обеспечивает гибкое и довольно естественное управление ориентацией объекта, но, как вы помните, он имеет недостаток, который проявляется, когда модуль угла поворота вдоль первой из вращаемых (с помощью glRotate) осей, в нашем случае — это ось X, превышает 90 градусов. Вам, читатель, я рекомендовал самостоятельно решить эту проблему и устранить недостаток. Ниже приводится одно из возможных решений. Если вы, читатель, найдете более изящное, буду рад получить его от вас. Для начала следует ввести в состав класса COpenGL функцию нормировки углов вращения, которая, учитывая периодичность процесса, ограничивает их так, чтобы они не выходили из диапазона (-360°, 360°):

void COpenGL::LimitAngles()

{

//====== Нормирование углов поворота так,

//====== чтобы они были в диапазоне (-360°, +360°)

while (m_AngleX < -360.f)

m_AngleX += 360.f;

while (m_AngleX > 360.f)

m_AngleX -= 360.f;

while (m_AngleY < -360.f)

m_AngleY += 360.f;

while (m_AngleY > 360.f)

m_AngleY -= 360.f;

}

Затем следует вставить вызовы этой функции в те точки программы, где изменяются значения углов. Кроме того, надо менять знак приращение m_dx, если абсолютная величина угла m_AngleX попадает в диапазон (90°, 270°). Это надо делать при обработке сообщения WM_MOUSEMOVE. Ниже приведена новая версия функции обработки этого сообщения, а также сообщения WM_TIMER, в которое также следует ввести вызов функции нормировки:

LRESULT COpenGL::OnMouseMove(UINT /*uMsg*/, WPARAM wParam, LPARAM IParam, BOOL& bHandled)

{

//====== Если был захват

if (m_bCaptured)

{

//====== Вычисляем желаемую скорость вращения

short xPos = (short)LOWORD(IParam);

short yPos = (short)HIWORD(1Param);

m_dy = float(yPos - m_yPos)/20.f;

m_dx = float(xPos - m_xPos)/20.f;

//====== Если одновременно была нажата Ctrl,

if (wParam & MK_CONTROL)

{

//=== Изменяем коэффициенты сдвига изображения

m_xTrans += m_dx;

m_yTrans -= m_dy;

}

else

{

//====== Если была нажата правая кнопка

if (m_bRightButton)

//====== Усредняем величину сдвига

m_zTrans += (m_dx + m_dy)/2.f;

else

{

//====== Иначе, изменяем углы поворота

//====== Сначала нормируем оба угла

LiraitAngles();

//=== Затем вычисляем модуль одного из них

double a = fabs(m_AngleX);

// и изменяем знак приращения(если надо)

if (90. < а && а < 270.) m_dx = -m_dx;

m_AngleX += m_dy;

m_AngleY += m_dx;

}

}

// В любом случае запоминаем новое положение мыши

m_xPos = xPos;

m_yPos = yPos;

FireViewChange();

}

bHandled = TRUE; return 0;

}

LRESULT COpenGL: :OnTimer (UINT /*uMsg*/, WPARAM

/*wParam*/, LPARAM /*lParam*/, BOOL& bHandled)

{

//====== Нормировка углов поворота

LimitAngles () ;

//====== Увеличиваем эти углы

m_AngleX += m_dy; m_AngleY += m_dx;

//====== Просим перерисовать окно

FireViewChange();

bHandled = TRUE;

return 0;

}

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

LRESULT COpenGL::OnLButtonDown(UINT /*uMsg*/, WPARAM

/*wParam*/, LPARAM IParam, BOOL& bHandled)

{

//====== Останавливаем таймер

KillTimer(1);

//====== Обнуляем кванты перемещения

m_dx = O.f;

m_dy = 0.f;

//====== Захватываем сообщения мыши,

//====== направляя их в свое окно

SetCapture();

//====== Запоминаем факт захвата

m_bCaptured = true;

//====== Запоминаем координаты курсора

m_xPos = (short)LOWORD(IParam);

m_yPos = (short)HIWORD(IParam);

bHandled = TRUE; return 0;

}

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

LRESULT COpenGL::OnLButtonUp(UINT /*uMsg*/, WPARAM

/*wParam*/, LPARAM /*lParam*/, BOOL& bHandled)

{

//====== Если был захват,

if (m_bCaptured)

{

//=== то анализируем желаемый квант перемещения

//=== на превышение порога чувствительности

if (fabs(m_dx) > 0.5f || fabs(m_dy) > 0.5f)

//====== Включаем режим постоянного вращения

SetTimer(1,33) ;

else

//====== Выключаем режим постоянного вращения

KillTimer(1);

//====== Снимаем флаг захвата мыши

m_bCaptured = false;

//====== Отпускаем сообщения мыши

ReleaseCapture();

}

bHandled = TRUE;

return 0;

}

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

LRESULT COpenGL::OnRButtonDown(UINT uMsg, WPARAM wParam,

LPARAM IParam, BOOL& bHandled)

{

//====== Запоминаем факт нажатия правой кнопки

m_bRightButton = true;

//====== Воспроизводим реакцию на левую кнопку

OnLButtonDown(uMsg, wParam, IParam, bHandled);

return 0;

}

Отпускание правой кнопки просто отмечает факт прекращения перемещения вдоль оси Z и отпускает сообщения мыши (ReleaseCapture), для того чтобы они могли правильно обрабатываться другими окнами, в том числе и нашим окном-рамкой. Если этого не сделать, то будет невозможно использоваться меню:

LRESULT COpenGL::OnRButtonUp(UINT /*uMsg*/, WPARAM

/*wParam*/, LPARAM /*lParam*/, BOOL& bHandled)

{

m_bRightButton = false;

m_bCaptured = false;

ReleaseCapture();

bHandled = TRUE;

return 0;

}

Запустите и проверьте управляемость объекта. Введите коррективы чувствительности мыши. В заключение отметим, что при выборе параметров заготовки ATL мы могли на вкладке Miscellaneous (Разное) поднять не только флажок Insertable, но и windowed Only. Это действие сэкономило бы те усилия, которые были потрачены на поиск неполадок, вызванных отсутствием флага m bWindowOnly.



Содержание раздела