netlib.narod.ru | < Назад | Оглавление | Далее > |
Для того чтобы главное окно приложения выглядело и работало так, как мы желаем, нам предстоит изрядно потрудиться. При этом нам придется заниматься множеством мелких подробностей, однако я рассмотрю их все — если вы не относитесь к числу экспертов по MFC, то сможете лучше представить себе, как работает эта библиотека. После этого мы оставим в покое структурный код и сосредоточим свои усилия на более интересных аспектах, связанных с отображением трехмерных объектов (самое время налить себе очередную чашку кофе, как это только что сделал я).
AppWizard включает в функцию OnCreate класса CMainFrame довольно много кода, предназначенного для создания самого окна, панели инструментов и строки состояния. Нам придется добавить специальный фрагмент, в котором трехмерное окно будет создаваться как потомок главного обрамленного окна. Заодно мы создадим исходный макет с трехмерным объектом — по крайней мере, мы сможем проверить, работает ли наша программа. Одно из худших разочарований в жизни программиста — ввести несколько сотен строк программы, откомпилировать и запустить ее лишь для того, чтобы увидеть большое черное окно (вряд ли кто-нибудь при этом станет прыгать от радости). Я привожу текст функции OnCreate за исключением фрагментов, сгенерированных AppWizard, чтобы вы могли увидеть, что же именно мы добавили в нее (полный текст функции находится в проекте Stage на прилагаемом к книге CD-ROM):
int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct) { . . . // Создать трехмерное окно if (!m_wnd3d.Create(this, IDC_3DWND)) { return -1; } . . . NewScene(); ASSERT(m_pScene); // Создать трехмерный объект C3dShape sh1; sh1.CreateCube(2); m_pScene->AddChild(&sh1); sh1.SetRotation(1, 1, 1, 0.02); return 0; }
Этот фрагмент во многом напоминает тот, который использовался в программе Basic для отображения исходного макета. Обратите внимание на то, что константа IDC_3DWND включается в проект командой View | Resource Symbols в меню Visual C++. Класс CMainFrame пополнился двумя членами: m_wnd3d и m_pScene. Переменная m_wnd3d была включена нами в MainFrm.h ранее, после редактирования функции CStageApp::InitInstance. Переменная m_pScene добавляется следующим образом:
class CMainFrame : public CFrameWnd { . . . public: C3dWnd m_wnd3d; C3dScene* m_pScene; . . . };
ЗАМЕЧАНИЕ |
Блюстители чистоты C++ могут неодобрительно отнестись к тому, что объекты окна и макета были объявлены мной как открытые. Тем не менее я часто поступаю так в своих примерах, чтобы не возиться со специальными функциями доступа (например, GetScene). Прямой доступ к объектам позволяет получить более компактный код, даже если при этом нарушается принцип инкапсуляции. |
Теперь в нашем главном окне содержится трехмерное окно и указатель на текущий макет. Возвращаясь к функции OnCreate, проследим за последовательностью действий: сначала мы создаем трехмерное окно, затем при помощи функции NewScene строим трехмерный макет (работа этой функции будет рассмотрена ниже), после чего мы создаем куб, присоединяем его к макету и начинаем вращать. Если взглянуть на текст функции C3dWnd::Create в библиотеке 3dPlus, нетрудно увидеть, что она создает трехмерное окно в качестве окна-потомка, а передаваемый при ее вызове указатель this используется для определения окна-родителя. Конечно, достижением программистской мысли это не назовешь, и все же данный факт достаточно важен для понимания основ.
На моем компьютере установлено разрешение экрана 1280 х 1024. Microsoft Windows обладает одной скверной привычкой — по умолчанию она создает громадное окно лишь потому, что у меня установлено большое разрешение экрана. При работе с приложениями, для которых такое большое окно не требуется, я обычно устанавливаю исходный размер окна, включая пару лишних строк в CMainFrame::PreCreateWindow. В приведенном ниже фрагменте задается начальный размер окна 300 х 350 пикселей:
BOOL CMainFrame::PreCreateWindow(CREATESTRUCT& cs) { // Задать исходный размер окна cs.cx = 300; cs.су = 350; return CFrameWnd::PreCreateWindow(cs); }
Настало время поближе познакомиться с функцией NewScene, о которой было сказано выше:
BOOL CMainFrame::NewScene() { // Удалить макет, если он существует if (m_pScene) { m_wnd3d.SetScene(NULL); delete m_pScene; m_pScene = NULL; } // Создать исходный макет m_pScene = new C3dScene; if (!m_pScene->Create()) return FALSE; // Установить источники света C3dDirLight dl; dl.Create(0.8, 0.8, 0.8); m_pScene->AddChild(&dl); dl.SetPosition(-2, 2, -5); dl.SetDirection(l, -1, 1); m_pScene->SetAmbientLight(0.4, 0.4, 0.4); m_wnd3d.SetScene(m_pScene); return TRUE; }
Мы включим функцию NewScene в файл MainFrm.cpp. Она удаляет существующий макет и создает новый, со стандартным расположением источников света. Затем макет присоединяется к сцене, которая является частью трехмерного окна. Экспериментируя с трехмерными объектами, я хочу быть уверенным в том, что они уничтожаются и создаются без всяких проблем. Данная функция позволяет удалить все созданные ранее объекты и заново начать работу с макетом (она принесла большую пользу, когда мой малолетний сын схватил джойстик и загнал все объекты куда-то за пределы экрана).
Так как AppWizard создал панель инструментов и строку состояния, занимающие место в клиентской области окна, нам необходимо иметь возможность заново вычислить размеры трехмерного окна в том случае, если пользователь перемещает или убирает панель инструментов или же скрывает строку состояния. Воспользуйтесь ClassWizard и создайте функцию CMainFrame::RecalcLayout, которая переопределяет функцию CFrameWnd::RecalcLayout:
void CMainFrame::RecalcLayout(BOOL bNotify) { // Заново разместить служебные области // и поместить трехмерное окно в центр. // Размещение служебных областей выполняется // обрамленным окном. CFrameWnd::RecalcLayout(bNotify); // Определить размеры свободного места //в клиентной области // для размещения трехмерного окна CRect rc; RepositionBars(0, 0xFFFF, IDC_3DWND, CWnd::reposQuery, &rc); if (IsWindow(m_wnd3d.GetSafeHwnd())) { m_wnd3d.MoveWindow(&rc, FALSE); } }
В сущности, данный фрагмент определяет размеры свободного места в клиентской области и использует его для размещения трехмерного окна. Более подробные объяснения можно найти в документации по MFC.
Все хорошее когда-нибудь приходит к концу — в том числе и окна. Включая в программу функцию OnDestroy, мы сможем освободить память, занятую нашими объектами (для этого следует вызвать ClassWizard и указать обрабатываемое сообщение WM_DESTROY):
void CMainFrame::OnDestroy() { CFrameWnd::OnDestroy() ; // Уничтожить текущий макет m_wnd3d.SetScene(NULL); if (m_pScene) { delete m_pScene; } }
Если при завершении программы не удалить объекты, созданные во время ее активности, это приведет к «утечке памяти». Благодаря механизму работы с памятью, который используется MFC в отладочных версиях программ, вы получите сообщение о таких «утечках» после завершения приложения, так что обнаружить их просто, а вот ликвидировать посложнее, так что давайте сразу будем программировать аккуратно.
Одна из самых интересных особенностей механизма визуализации Direct3D заключается в том, что он работает непосредственно с видеопамятью и не пользуется интерфейсом графических устройств Windows (GDI). Следовательно, крайне важно, чтобы механизм визуализации точно знал экранное положение того окна, с которым он работает. Если это положение будет указано неверно, то он либо не станет рисовать вообще, либо, что еще хуже, примется рисовать поверх других окон. Кроме того, механизм визуализации должен знать, активно приложение или нет и получило ли оно какие-либо сообщения, связанные с палитрой. Если приложение переходит в фоновый режим, механизм визуализации должен освободить палитру, чтобы ей могли воспользоваться другие приложения. Все эти требования выполняются функциями, предназначенными для обработки сообщений WM_ACIVATEAPP, WM_PALETTECHANGED и WM_MOVE:
void CMainFrame::OnActivateApp(BOOL bActive, HTASK hTask) { CFrameWnd::OnActivateApp(bActive, hTask); // Сообщить трехмерному окну об изменении состояния m_wnd3d.SendMessage(WM_ACTIVATEAPP, (WPARAM)bActive, (LPARAM)hTask); } void CMainFrame::OnPaletteChanged(CWnd* pFocusWnd) { // Сообщить трехмерному окну об изменении палитры m_wnd3d.SendMessage(WM_PALETTECHANGED, pFocusWnd ? (WPARAM)pFocusWnd->GetSafeHwnd() : 0); } void CMainFrame::OnMove(int x, int y) { CFrameWnd::OnMove(x, y); // Сообщить трехмерному окну о перемещении обрамленного окна m_wnd3d.SendMessage(WM_MOVE, 0, MAKELPARAM(0, 0)); }
Как видите, для нормальной работы Direct3D необходимо лишь, чтобы ваше приложение посылало сообщения трехмерному окну, которое берет на себя частные аспекты взаимодействия с механизмом визуализации.
Остается лишь предусмотреть обработку команд меню File | New и File | Open. Ранее мы уже построили функцию для удаления текущего и создания нового макета, поэтому команда File | New реализуется тривиально (воспользуйтесь ClassWizard для добавления идентификатора объекта ID_FILE_NEW):
void CMainFrame::OnFileNew() { NewScene(); }
Команда File | Open обрабатывается двумя следующими функциями:
BOOL CMainFrame::OpenFile(const char* pszPath) { // Попытаться открыть файл с фигурой C3dShape sh; const char* pszFile = sh.Load(pszPath); if (!pszFile) return FALSE; // Создать новый макет NewScene(); ASSERT(m_pScene); // Присоединить новую фигуру к макету m pScene->AddChild(&sh); sh.SetRotation(1, 1, 1, 0.02); // Включить имя в список последних открывавшихся файлов AfxGetApp()->AddToRecentFileList(pszFile); return TRUE; } void CMainFrame::OnFileOpen() { OpenFile(NULL); }
Теперь давайте посмотрим, как работает функция OpenFile.
Сначала мы создаем новый объект C3dShape и вызываем его функцию Load. Эта функция либо пытается открыть файл, либо, при отсутствии заданного имени файла, выводит окно диалога, в котором пользователю предлагается выбрать файл. В том случае, если файл имеет правильный формат, код класса C3dShape открывает его и создает трехмерный объект на основании данных из файла. Понятно, правда? Далее мы присоединяем новый объект к макету и приводим его во вращение, чтобы увидеть макет во всей красе. Имя файла заносится в список последних открывавшихся файлов, что облегчает его повторное открытие в будущем (вспомните, что функция OpenFile также вызывается в функции OpenDocumentFile в Stage.cpp, при выборе пользователем одного из файлов в меню).
Осталось добавить несколько завершающих штрихов, после которых проект Stage будет нормально компилироваться и работать. Прежде всего необходимо инициализировать переменную m_pScene в конструкторе CMainFrame, для этого следует включить в конструктор строку
m_pScene = NULL
Кроме того, поскольку функции NewScene и OpenFile не были созданы с помощью ClassWizard, придется вручную добавить их объявления в конструктор CMainFrame в файле MainFrm.h.
Если вы еще не заснули, можете откомпилировать и скомпоновать проект. В вашем распоряжении появилась действующая программа для просмотра трехмерных объектов. Разумеется, вы пока не имеете ни малейшего представления о принципах ее работы, но мы рассмотрим их чуть позже, и они наверняка покажутся вам интереснее того, чем мы занимались до сих пор.
netlib.narod.ru | < Назад | Оглавление | Далее > |