Текущий раздел:

Описания RDS

Руководство программиста

Глава 2. Создание моделей блоков

§2.10. Программное рисование внешнего вида блока

§2.10.4. Панели блоков в окне подсистемы

# Руководство программиста

## Глава 2. Создание моделей блоков

### §2.10. Программное рисование внешнего вида блока

#### §2.10.4. Панели блоков в окне подсистемы

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

Программное рисование модели блока с помощью функций API Windows или сервисных функций RDS дает возможность строить сколь угодно сложные изображения в окне подсистемы. Однако, модель не имеет доступа к самому окну подсистемы, и, поэтому, не может создавать внутри него свои собственные объекты (поля ввода, элементы управления и т.п.) Проще говоря, модель блока не может средствами RDS получить дескриптор (типа HWND) окна подсистемы и вмешиваться в его работу, добавляя в него свои объекты. Технически, модель может получить этот идентификатор при помощи стандартной функции API Windows WindowFromDC и работать с ним, однако, разработчику модели лучше этого не делать из-за возможных проблем с совместимостью – механизм взаимодействия RDS с моделями блоков на это не рассчитан. Вместо этого модель блока, используя сервисные функции RDS, может создавать внутри окна родительской подсистемы специальные объекты – панели. Каждая такая панель является оконным объектом Windows и имеет дескриптор HWND, который RDS сообщает модели блока при создании панели. RDS также уведомляет модель об уничтожении оконного объекта при закрытии окна подсистемы, о необходимости перерисовать содержимое панели, о перемещении или изменении размера панели пользователем (если это разрешено) и т.д. Модель блока может создавать внутри панели любые другие оконные объекты – главное, чтобы она удалила их при закрытии панели в ответ на соответствующее сообщение от RDS. Следует помнить, что модель может создавать произвольное число панелей, но только в окне родительской подсистемы. Создавать панели в окнах других подсистем нельзя.

Общая процедура работы модели с панелями в окне подсистемы выглядит следующим образом:

- Модель вызывает сервисную функцию rdsPANCreate для создания объекта-панели (это один из многих вспомогательных объектов RDS типа RDS_HOBJECT). Независимо от того, открыто или закрыто в данный момент окно родительской подсистемы, RDS будет следить за данной подсистемой – как только окно будет открыто, внутри него будет создано дочернее окно-панель.
- Когда окно подсистемы открывается, RDS создает внутри него оконный объект с указанными моделью ранее параметрами и информирует об этом модель блока, передавая ей дескриптор HWND этого объекта. В этот момент модель может создать внутри указанного объекта все необходимые ей органы управления, поля ввода и т.п.
- Если при создании панели модель разрешила пользователю перемещать панель или изменять ее размеры, она будет получать уведомления об этих событиях. Также, если это было разрешено, RDS будет информировать ее о получении оконным объектом сообщения WM_PAINT, то есть о необходимости перерисовки содержимого.
- При закрытии окна подсистемы RDS сообщит модели о том, что оконный объект сейчас будет уничтожен. В этот момент модель должна уничтожить все органы управления, которые она поместила туда при создании объекта. После закрытия окна RDS снова будет следить за подсистемой, чтобы при очередном открытии окна создать объект и передать модели его идентификатор.
- Если панель больше не нужна модели, она удаляет созданный вспомогательный объект функцией rdsDeleteObject, что автоматически приводит к удалению панели. После этого RDS перестает следить за окном подсистемы.

Модель блока также может скрывать и показывать панель, не удаляя вспомогательный объект RDS. При скрытии панели связанный с ней оконный объект уничтожается (о чем, как обычно, RDS сообщает модели блока), а при показе – снова создается. Часто бывает удобно создать объект-панель в момент подключения модели к блоку (например, в конструкторе класса личной области данных, если используется C++), но не показывать эту панель немедленно – функция rdsPANCreate предоставляет такую возможность. Панель будет показана пользователю позже, когда возникнет такая необходимость. Затем она может быть снова скрыта, вновь показана и т.д. При этом все параметры панели (положение, размер и т.п.) сохраняются во вспомогательном объекте, поэтому их не нужно устанавливать заново при каждом показе панели. Уничтожение вспомогательного объекта в этом случае целесообразно производить в момент отключения модели от блока – если панель в этот момент видима, она автоматически закроется.

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

В качестве примера рассмотрим блок, выводящий трехмерное изображение средствами библиотеки OpenGL. Для того, чтобы с ее помощью отображать трехмерные сцены в Windows, нужно знать дескриптор окна, в которое будет осуществляться вывод. Конечно, модель блока может открыть отдельное окно средствами Windows API и рисовать в нем трехмерные изображения, но при этом это окно не будет привязано к подсистеме и будет перемещаться независимо от ее окна. Это будет не очень хорошо выглядеть, если подсистема имитирует какой-либо пульт управления, а трехмерные изображения являются частью одного из приборов. При перетаскивании окна такой подсистемы или изменении его масштаба пульт будет «разваливаться» на отдельные окна. Чтобы избежать этого, будем выводить трехмерные сцены на панель блока внутри окна подсистемы.

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

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

      #include <windows.h>
      #include <RdsDef.h>
      // Подготовка описаний сервисных функций
      #define RDS_SERV_FUNC_BODY GetInterfaceFunctions
      #include <RdsFunc.h>
      // Глобальная переменная для значения ошибки
      double DoubleErrorValue;
      //========== Главная функция DLL ==========
      int WINAPI DllMain(HINSTANCE /*hinst*/,
                               unsigned long reason,
                               void* /*lpReserved*/)
      { if(reason==DLL_PROCESS_ATTACH) // Загрузка DLL
          { // Получение доступа к функциям
            if(!GetInterfaceFunctions())
              RDS_SERV_ERROR_MSGW // Сообщение: старая версия RDS
            else
              rdsGetHugeDouble(&DoubleErrorValue);
          }
        return 1;
      }
      //========= Конец главной функции =========

Поскольку в процессе работы модели блока будет создана панель, идентификатор этой панели, возвращаемый функцией rdsPANCreate, нужно где-то хранить. Кроме того, для регулярного обновления панели в процессе расчета потребуется таймер (в отличие от самого окна подсистемы, обновляемого при расчете с заданным интервалом, панели в ней автоматически не перерисовываются – модель блока должна делать это сама при необходимости). Идентификатор панели и идентификатор таймера, который создаст модель, будут храниться в личной области данных блока, которую мы оформим как класс C++:

      //=================================================
      // Личная область данных блока
      //=================================================
      class TOpenGLInstr
      { private:
          RDS_TIMERID RefreshTimer; // Таймер обновления

        public:
          RDS_HOBJECT Panel;        // Объект-панель

          // Сохранение параметров блока
          void SaveText(void);
          // Загрузка параметров блока
          void LoadText(char *text);

          // Создание таймера обновления
          void CreateRefreshTimer(RDS_BHANDLE parent);

          // Конструктор класса
          TOpenGLInstr(void);
          // Деструктор класса
          ~TOpenGLInstr();
      };
      //=================================================

В закрытой области класса (private) размещаются поля и функции класса, к которым не нужен доступ снаружи этого класса, то есть из функции модели блока. В данном случае это идентификатор таймера обновления RefreshTimer. В открытой области (public) расположено поле Panel для хранения идентификатора созданной панели, а также функции, которые будут вызываться непосредственно из модели блока. Чтобы положение и размер панели, созданной блоком, не терялись при сохранении и последующей загрузке схемы, будем записывать и загружать их функциями SaveText и LoadText соответственно. Создание таймера обновления панели также вынесем в отдельную функцию-член класса CreateRefreshTimer (удаление таймера будет происходить в деструкторе).

Создавать панель мы будем в конструкторе класса, то есть в момент присоединения модели к блоку (при загрузке схемы или добавлении в схему блока с этой моделью из библиотеки блоков):

      // Конструктор класса личной области данных
      // ВАЖНО: Исходный текст программы должен быть записан в UTF8,
      //   в противном случае необходимо использовать версии функций
      //   с суффиксом "W" и символьные константы с префиксом "L"
      TOpenGLInstr::TOpenGLInstr(void)
      { // Создаем панель в окне подсистемы
        Panel=rdsPANCreate(0,0,0,300,300,
                           RDS_PAN_F_SCALABLE|RDS_PAN_F_BORDER|
                           RDS_PAN_F_SIZEABLE|RDS_PAN_F_MOVEABLE|
                           RDS_PAN_F_CAPTION|RDS_PAN_F_HIDDEN|
                           RDS_PAN_F_PAINTMSG,
                           "Прибор");
        // Инициализируем идентификатор еще не созданного таймера
        RefreshTimer=NULL;
      }
      //=================================================

Первой же командой конструктора создается объект-панель, и его идентификатор присваивается полю класса Panel. Для создания объекта используется функция rdsPANCreate:

      RDS_HOBJECT RDSCALL rdsPANCreateA( // Для UTF8
        int order,        // Близость панели к переднему плану
        int left,int top, // Верхний левый угол панели
        int width,        // Ширина панели
        int height,       // Высота панели
        int flags,        // Флаги
        RDSCSTR caption   // Заголовок панели (UTF8)
      );
      RDS_HOBJECT RDSCALL rdsPANCreateW( // Для UTF16
        int order,        // Близость панели к переднему плану
        int left,int top, // Верхний левый угол панели
        int width,        // Ширина панели
        int height,       // Высота панели
        int flags,        // Флаги
        RDSWCSTR caption  // Заголовок панели (UTF16)
      );
      RDS_HOBJECT RDSCALL rdsPANCreate( // Функция-псевдоним
        int order,        // Близость панели к переднему плану
        int left,int top, // Верхний левый угол панели
        int width,        // Ширина панели
        int height,       // Высота панели
        int flags,        // Флаги
        RDSXCSTR caption  // Заголовок панели (кодировка по умолчанию)
      );

Параметр order определяет близость панели к переднему плану – панели одного и того же блока с большим значением order будут перекрывать панели с меньшим значением. Перекрытие панелей разных блоков определяется взаимным расположением самих блоков, то есть если блок A расположен в окне подсистемы ближе к переднему плану, чем блок B, все панели блока A будут перекрывать панели блока B, независимо от того, с каким значением параметра order эти панели были созданы. При этом панели блока A с order=10 будут перекрывать панели этого же блока со значением order=5. Если блок создает единственную панель, как в нашем случае, значение параметра order может быть любым, поскольку от него ничего не зависит.

Параметры left, top, width и height задают положение левого верхнего угла панели и ее размеры в координатах рабочей области подсистемы. Значения ширины и высоты указываются для масштаба 100%. Будет ли размер панели изменяться в соответствии с масштабом окна подсистемы, или ее размер в точках экрана будет оставаться неизменным, определяется одним из флагов в параметре flags.

Параметр flags содержит один или несколько битовых флагов, определяющих внешний вид и поведение панели:

- RDS_PAN_F_SCALABLE – если указан этот флаг, размер панели будет меняться синхронно с изменением масштаба в окне подсистемы. Это удобно для блоков, выводящих на панель какую-либо абстрактную графику (как в нашем случае), но, обычно, нежелательно для блоков, размещающих в панели какие-либо поля ввода и органы управления. Как правило, размер поля ввода должен оставаться постоянным, иначе оно может стать слишком мелким для комфортной работы с ним. Если размер панели с полем ввода будет меняться, а размер самого поля – нет, в мелких масштабах поле просто не уместится в панель, а в крупных поле займет только верхний левый угол панели, большая часть которой будет закрывать рабочее поле окна безо всякого смысла.
- RDS_PAN_F_BORDER – если указать этот флаг, панель будет иметь вокруг себя рельефную рамку. Панель с рамкой визуально выделяется на рабочем поле системы, что, как правило, и требуется. Однако, если необходимо создать сложное изображение, разместив рядом несколько панелей и блоков, рамку лучше не включать, чтобы панели, расположенные рядом, воспринимались как единое целое. При одинаковых размерах, панель с рамкой имеет несколько меньшую площадь, доступную блоку, чем панель без рамки, поскольку параметры width и height задают внешние, с учетом рамки, размеры панели. Следует также помнить, что RDS поддерживает перемещение панели пользователем и изменение ее размеров только для панелей с рамками. Если необходимо дать пользователю возможность перемещать панель без рамки, разработчик модели блока должен заложить такую возможность самостоятельно, например, разместив на панели собственный оконный объект Windows и реагируя на перемещения курсора в его пределах.
- RDS_PAN_F_CAPTION – флаг указывает на наличие у панели заголовка с названием в стиле окон Windows. Текст названия панели при этом задается параметром caption. Заголовок могут иметь только панели с рамкой. Он также используется для перемещения панели пользователем, если это разрешено (пользователь «перетаскивает» панель за заголовок левой кнопкой мыши, как обычное окно Windows), поэтому, если необходимо дать пользователю возможность перемещать панель, заголовок нужно включить.
- RDS_PAN_F_MOVEABLE – панель может перемещаться пользователем (только при наличии у нее рамки и заголовка). При этом модель блока уведомляется о новом положении панели.
- RDS_PAN_F_SIZEABLE – размер панели может изменяться пользователем (только при наличии у нее рамки). Изменение размера панели производится точно так же, как и обычного окна Windows – перетаскивается один из углов или одна из сторон рамки. Модель блока при этом уведомляется о новом размере панели.
- RDS_PAN_F_NOBUTTON – в заголовке панели нет кнопки закрытия. По умолчанию эта кнопка, как и в обычных окнах Windows, расположена в крайней правой части полосы заголовка, нажатие на нее приводит к закрытию панели. Естественно, у панелей без заголовка этой кнопки нет, и пользователь не может самостоятельно закрыть их – соответствующие функции должна обеспечить модель блока.
- RDS_PAN_F_HIDDEN – панель скрыта от пользователя. Если указан этот флаг, функция rdsPANCreate создаст внутренний объект RDS для работы с панелью, но сама панель в окне подсистемы не появится до тех пор, пока модель блока не скомандует показать ее, вызвав одну из сервисных функций.
- RDS_PAN_F_PAINTMSG – модель блока получает уведомления о необходимости перерисовки содержимого панели. Если на панели размещены какие-либо объекты Windows, в этом нет необходимости – таким объектам не нужно сообщать о перерисовке, они сами получают сообщение Windows WM_PAINT и могут отреагировать на него. В нашем же случае мы не будем размещать что-либо на панели, вместо этого, мы свяжем с ней область вывода OpenGL. Поскольку оконный объект, связанный с панелью, создается и обслуживается RDS, модель не может вмешаться в его работу и перехватить поступившее ему от операционной системы сообщение о перерисовке. Вместо этого она может указать при создании панели флаг RDS_PAN_F_PAINTMSG, и RDS будет транслировать ей эти сообщения.

В этом примере в вызове rdsPANCreate указаны все флаги, кроме RDS_PAN_F_NOBUTTON. Модель создает скрытую панель с рамкой, заголовком «прибор» и кнопкой закрытия, причем размер и положение панели пользователь сможет, при желании, изменять. Модель блока будет получать уведомления о необходимости перерисовки панели.

В конце конструктора полю класса RefreshTimer, предназначенному для хранения идентификатора таймера для обновления панели в процессе расчета, присваивается нулевое значение – таймер пока не нужен, он будет создан позднее.

Деструктор класса будет включать всего два вызова: удаление созданного объекта-панели функцией rdsDeleteObject и удаление таймера обновления панели (если он был создан в процессе работы блока) функцией rdsDeleteBlockTimer:

      // Деструктор класса личной области данных
      TOpenGLInstr::~TOpenGLInstr()
      { // Удаление панели
        if(Panel)
          rdsDeleteObject(Panel);
        // Удаление таймера
        if(RefreshTimer)
          rdsDeleteBlockTimer(RefreshTimer);
      }
      //=================================================

Теперь напишем функцию CreateRefreshTimer, которая будет создавать таймер для обновления панели или настраивать его параметры, если таймер уже создан (почему мы не создаем таймер в конструкторе блока, будет объяснено позднее). Эта функция будет вызываться при запуске расчета. Пока расчет не запущен, у нас нет необходимости обновлять панель с заданной частотой – если панель будет перекрыта другим окном, а потом снова появится на переднем плане, Windows автоматически пошлет ей сообщение о необходимости перерисовки, которое будет передано в модель блока, поскольку при создании панели был указан флаг RDS_PAN_F_PAINTMSG. Если же не обновлять панель во время расчета, изображение блока никак не будет реагировать на изменение входных переменных – пока панель не перекрыта другими окнами и не изменяет свой размер, никаких сообщений о перерисовке она не получает. Можно, конечно, обновлять панель при каждом срабатывании модели блока, но это было бы неоправданной тратой вычислительных ресурсов. Перерисовка окон – одна из самых длительных процедур, и выполнение ее в каждом такте расчета приведет к существенному замедлению работы всей системы. Тем более, что такое частое обновление просто не нужно – обычно для достижения вполне приемлемого качества мультипликации достаточно перерисовывать изображение не чаще десяти раз в секунду.

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

      // Создание таймера обновления панели
      void TOpenGLInstr::CreateRefreshTimer(RDS_BHANDLE parent)
      { // Структура для получения параметров окна подсистемы
        RDS_EDITORPARAMETERS WinParams;
        // Определение интервала обновления окна подсистемы
        WinParams.servSize=sizeof(RDS_EDITORPARAMETERS);
        rdsGetEditorParameters(parent,&WinParams);
        // Интервал - в WinParams.RefreshDelay

        // Создание таймера
        RefreshTimer=rdsSetBlockTimer(RefreshTimer, // Идентификатор
                       WinParams.RefreshDelay,      // Интервал
                       RDS_TIMERM_LOOP | RDS_TIMERS_WINREF |
                       RDS_TIMERF_FIXFREQ,          // Режим и флаги
                       TRUE);                       // Запустить таймер
      }
      //=================================================

В функцию передается единственный параметр – идентификатор родительской подсистемы блока parent, интервал обновления которой будет использован при создании таймера. Этот идентификатор содержится в структуре RDS_BLOCKDATA, указатель на которую передается в модель блока при каждом вызове, поэтому получение его внутри модели не представляет трудностей. Интервал обновления можно получить вместе с другими параметрами окна подсистемы сервисной функцией rdsGetEditorParameters. В эту функцию передается идентификатор подсистемы (parent) и указатель на структуру типа RDS_EDITORPARAMETERS, в которую функция записывает все параметры окна указанной подсистемы. Как и большинство сервисных функций RDS, работающих со структурами, функция rdsGetEditorParameters проверяет соответствие поля servSize переданной структуры ожидаемому размеру этой структуры, и отказывается работать в случае, если ожидаемый размер окажется больше. Это, в большинстве случаев, позволяет предотвратить фатальные сбои и ошибки общей защиты при ошибке разработчика модели, передавшего в функцию неправильный указатель. Таким образом, чтобы функция rdsGetEditorParameters сработала, перед ее вызовом неободимо присвоить полю servSize передаваемой структуры размер этой структуры, то есть sizeof(RDS_EDITORPARAMETERS). После вызова функции структура будет заполнена различными параметрами подсистемы – размерами и положением окна, числом слоев, цветами и т.п., но нас будет интересовать только поле RefreshDelay – интервал автоматического обновления окна в миллисекундах. Этот параметр и используется при вызове функции rdsSetBlockTimer для создания таймера. Таймер будет циклическим (константа RDS_TIMERM_LOOP), вызывать модель в режиме RDS_BFM_WINREFRESH (константа RDS_TIMERS_WINREF) и его частота не будет снижаться при слишком больших задержках обновления окна (константа RDS_TIMERF_FIXFREQ), поскольку сейчас для нас плавность анимации важнее экономии вычислительных ресурсов. Таймер сразу создается активным (последний параметр функции – TRUE), но считать он будет только в режиме расчета. Можно заметить, что первый параметр функции – идентификатор таймера RefreshTimer, а не NULL, как в моделях с таймерами, рассмотренных ранее. Это сделано для того, чтобы функцию CreateRefreshTimer можно было вызывать и в том случае, если таймер уже создан. Если таймера еще нет, поле класса RefreshTimer будет иметь значение NULL, и функция создаст новый таймер. Если же таймер уже создан, RefreshTimer будет содержать его идентификатор, и вызов rdsSetBlockTimer изменит параметры существующего таймера. Таким образом, перед вызовом функции CreateRefreshTimer не обязательно удалять таймер – это упрощает программу и предохраняет от возможных ошибок.

Разобравшись с созданием таймера, напишем функции сохранения и загрузки положения и размера панели. Будем хранить данные блока в формате INI-файла Windows, то есть в виде строк «ключевое_слово=значение». Начнем с функции сохранения параметров:

      // Функция сохранения параметров блока
      void TOpenGLInstr::SaveText(void)
      { RDS_HOBJECT ini; // Идентификатор вспомогательного объекта

        // При сохранении блока в отдельный файл на диске параметры
        // панели записывать не нужно
        if(rdsGetSystemInt(RDS_GSISAVELOADACTION)==RDS_LS_SAVETOFILE)
          return;

        // Создание вспомогательного объекта для работы с данными
        ini=rdsINICreateTextHolder(TRUE);

        // Параметры записываются в секцию "[Window]"
        rdsSetObjectStr(ini,RDS_HINI_CREATESECTION,0,"Window");

        // Получение параметров панели и запись их в объект
        rdsINIWriteInt(ini,"Left",rdsGetObjectInt(Panel,RDS_PAN_LEFT,0));
        rdsINIWriteInt(ini,"Top",rdsGetObjectInt(Panel,RDS_PAN_TOP,0));
        rdsINIWriteInt(ini,"Width",rdsGetObjectInt(Panel,RDS_PAN_WIDTH,0));
        rdsINIWriteInt(ini,"Height",rdsGetObjectInt(Panel,RDS_PAN_HEIGHT,0));
        rdsINIWriteInt(ini,"Visible",rdsGetObjectInt(Panel,RDS_PAN_VISIBLE,0));

        // Сохранение получившегося текста в файл, в который в данный
        // момент идет запись
        rdsCommandObject(ini,RDS_HINI_SAVEBLOCKTEXT);

        // Удаление вспомогательного объекта
        rdsDeleteObject(ini);
      }
      //=================================================

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

Для получения информации о том, куда в данный момент сохраняется блок, используется сервисная функция rdsGetSystemInt с параметром RDS_GSISAVELOADACTION. Эта функция предназначена для получения различных глобальных параметров RDS, в данном случае она возвращает целое число, характеризующее выполняющуюся в данный момент операцию сохранения или загрузки. Нас интересует сохранение блока в файл, поэтому возвращаемое значение сравнивается с константой RDS_LS_SAVETOFILE, и, в случае равенства, функция завершается – ничего сохранять не надо. В противном случае функцией rdsINICreateTextHolder (см. пример в §2.8.5) создается вспомогательный объект RDS для работы с текстом, после чего в нем создается секция «Window». Далее функциями rdsINIWriteInt в эту секцию записываются пять целых параметров: координаты левого верхнего угла панели, ее ширина, высота и видимость (1 – для видимой панели, 0 – для скрытой). Параметры читаются непосредственно из объекта-панели Panel, который был создан в конструкторе личной области данных блока, при помощи функций rdsGetObjectInt с соответствующими параметрами. Независимо от того, видима ли панель в окне подсистемы и открыто ли вообще окно этой подсистемы, объект будет хранить параметры панели, и их можно считывать и устанавливать стандартными функциями для взаимодействия со вспомогательными объектами RDS. После того, как текст с параметрами панели сформирован во вспомогательном объекте, он сбрасывается туда, куда в данный момент идет запись (в файл, буфер обмена и т.д.), после чего объект уничтожается и функция завершается.

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

      // Функция загрузки параметров блока
      void TOpenGLInstr::LoadText(char *text)
      { RDS_HOBJECT ini; // Идентификатор вспомогательного объекта

        // Создание вспомогательного объекта для работы с данными
        ini=rdsINICreateTextHolder(TRUE);

        // Запись в объект полученного от RDS текста с параметрами блока
        rdsSetObjectStr(ini,RDS_HINI_SETTEXT,0,text);

        // Установка "[Window]" в качестве текущей секции и чтение
        // из нее пяти целых параметров
        if(rdsINIOpenSection(ini,"Window"))
          { rdsSetObjectInt(Panel,RDS_PAN_LEFT,0,rdsINIReadInt(ini,"Left",0));
            rdsSetObjectInt(Panel,RDS_PAN_TOP,0,rdsINIReadInt(ini,"Top",0));
            rdsSetObjectInt(Panel,RDS_PAN_WIDTH,0,rdsINIReadInt(ini,"Width",0));
            rdsSetObjectInt(Panel,RDS_PAN_HEIGHT,0,rdsINIReadInt(ini,"Height",0));
            rdsSetObjectInt(Panel,RDS_PAN_VISIBLE,0,rdsINIReadInt(ini,"Visible",0));
          }
        // Удаление вспомогательного объекта
        rdsDeleteObject(ini);
      }
      //=================================================

Внутри функции создается вспомогательный объект ini для работы с текстом, в этот объект записывается текст для загрузки, полученный моделью из RDS (функция LoadText получает его как параметр), в объекте выбирается секция «Window», и, если она существует, из нее в объект-панель читаются пять тех же целых параметров окна. Каждый параметр считывается из текста функцией rdsINIReadInt и передается в панель функцией rdsSetObjectInt. После считывания параметров объект ini уничтожается.

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

      // Модель блока с панелью
      extern "C" __declspec(dllexport) int RDSCALL OpenGLInstr(
            int CallMode,
            RDS_PBLOCKDATA BlockData,
            LPVOID ExtParam)
      { // Указатель на личную область, приведенный к нужному типу
        TOpenGLInstr *data=(TOpenGLInstr*)(BlockData->BlockData);
        // Вспомогательная – указатель на структуру параметров при
        // действиях с панелью (будет использована в реакциях)
        RDS_PPANOPERATION param;

        switch(CallMode)
          { // Инициализация блока
            case RDS_BFM_INIT:
              // Создание личной области данных
              // (при этом в конструкторе будет создана панель)
              BlockData->BlockData=data=new TOpenGLInstr();
              break;

            // Очистка данных блока
            case RDS_BFM_CLEANUP:
              // В деструкторе класса панель будет уничтожена
              delete data;
              break;

            // Запуск расчета
            case RDS_BFM_STARTCALC:
              // Создание таймера обновления
              data->CreateRefreshTimer(BlockData->Parent);
              break;

            // Вызов функции настройки или двойной щелчок
            // левой кнопки мыши
            case RDS_BFM_SETUP:
            case RDS_BFM_MOUSEDBLCLICK:
              // Показать панель
              rdsSetObjectInt(data->Panel,RDS_PAN_VISIBLE,0,1);
              break;

            // Сохранение параметров блока
            case RDS_BFM_SAVETXT:
              data->SaveText();
              break;

            // Загрузка параметров блока
            case RDS_BFM_LOADTXT:
              data->LoadText((char*)ExtParam);
              break;

            // Действия с панелью
            case RDS_BFM_BLOCKPANEL:
              // Приведение указателя на структуру, переданного в
              // ExtParam, к нужному типу
              param=(RDS_PPANOPERATION)ExtParam;
              // Разные действия в зависимости от операции с панелью
              switch(param->Operation)
                { // Создание оконного объекта для панели
                  case RDS_PANOP_CREATE:
                    // Здесь будет инициализация рисования на панели
                    break;
                  // Уничтожение оконного объекта панели
                  case RDS_PANOP_DESTROY:
                    // Здесь будет очистка инициализированного
                    break;
                  // Размер панели изменен
                  case RDS_PANOP_RESIZED:
                    // Здесь будет реакция на изменение размера
                    // и перерисовка панели
                    break;
                  // Необходимо перерисовать изображение
                  case RDS_PANOP_PAINT:
                    // Здесь будет перерисовка панели
                    break;
                }
              break;

           // Необходимо обновить окна блока (вызывается таймером)
           case RDS_BFM_WINREFRESH:
              // Здесь будет перерисовка панели
              break;
          }
        return RDS_BFR_DONE;
      }
      //=================================================

При инициализации блока (RDS_BFM_INIT) и очистке его данных (RDS_BFM_CLEANUP) , как обычно, создается и удаляется личная область. В момент ее создания (в конструкторе класса) будет создан объект для панели в окне подсистемы, при этом сама панель в окне не появится, даже если окно подсистемы уже создано, поскольку объект в конструкторе создается с флагом RDS_PAN_F_HIDDEN. В деструкторе класса, соответственно, этот объект уничтожается, и, если панель видима, она тоже исчезнет из окна.

При запуске расчета (RDS_BFM_STARTCALC) вызывается функция CreateRefreshTimer, которая либо создает таймер, если это первый ее вызов, либо задает для таймера новый интервал обновления, если таймер уже существует. В функцию передается идентификатор родительской подсистемы блока BlockData->Parent, чтобы она считала из параметров этой подсистемы интервал обновления окна и установила для таймера такой же. В отличие от рассматривавшихся ранее моделей с таймерами, в этой модели таймер создается не при инициализации блока (в конструкторе класса), а позже, при запуске расчета. Дело в том, что в режиме редактирования пользователь может изменить частоту обновления окна подсистемы, и единственный способ отследить это – заново считывать интервал обновления каждый раз при входе в режим моделирования или расчета. В данном случае выбран именно режим расчета, поскольку таймер, создаваемый в функции CreateRefreshTimer, работает именно в этом режиме.

В конструкторе блока панель создается скрытой, поэтому необходимо дать пользователю возможность открыть ее. Поскольку у данного блока нет каких-либо настраиваемых пользователем параметров, логично использовать функцию настройки, предусмотренную интерфейсом RDS (RDS_BFM_MOUSEDBLCLICK). Разумеется, чтобы все это работало, необходимо в параметрах блока указать наличие функции настройки (при этом лучше изменить ее название на «открыть окно» или «показать панель») и включить реакцию на мышь (рис. 65). Если еще установить в параметрах блока флаг «двойной щелчок – вызывает функцию настройки» (см. рис. 5), панель будет вызываться на экран по двойному щелчку независимо от режима RDS.

Рис. 65. Параметры блока, создающего панель

Для показа панели в функцию модели введена одинаковая реакция на вызовы RDS_BFM_SETUP и RDS_BFM_MOUSEDBLCLICK. В ней при помощи функции rdsSetObjectInt, передающей целое число объекту data->Panel, параметру объекта RDS_PAN_VISIBLE устанавливается значение 1. Это приведет к появлению панели в окне подсистемы и созданию для нее оконного объекта Windows. Если панель уже видима, никаких действий выполнено не будет.

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

Для реакции на различные события, связанные с панелями блока, RDS вызывает функцию модели в режиме RDS_BFM_BLOCKPANEL, при этом в параметре ExtParam передается указатель на структуру типа RDS_PANOPERATION (именно для работы с этой структурой в функции модели объявлена вспомогательная переменная param). В поле Operation этой структуры содержится целый идентификатор события, произошедшего с панелью, в поле Panel – указатель на структуру описания самой панели. Структура описания панели, в свою очередь, содержит большой набор параметров панели: идентификатор, положение и размер, флаги, заголовок и т.п. Важнее всего то, что она содержит дескриптор оконного объекта Windows, связанного с панелью – поле Handle типа HWND. Не зная этого дескриптора, нельзя создавать внутри панели дочерние окна, привязывать к панели область вывода трехмерных изображений OpenGL и т.п. Пока мы пишем «скелет» модели, поэтому в данный момент она никак не реагирует на различные события панели. Однако, в модель уже включен оператор switch, анализирующий поле Opeation переданной структуры. Пока внутри него расставлены комментарии, поясняющие, как будет реагировать модель на события, позднее мы заменим их настоящими реакциями.

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

- RDS_PANOP_CREATE – для панели создан оконный объект Windows. Модель должна выполнить необходимые действия для создания дочерних оконных объектов, их инициализации и т.п.;
- RDS_PANOP_DESTROY – оконный объект панели сейчас будет уничтожен. Все созданные дочерние объекты, созданные моделью, должны быть уничтожены в этой реакции;
- RDS_PANOP_RESIZED – размер панели изменен. Модель может, при необходимости, перестроить дочерние окна так, чтобы они уложились в панель, изменить размер области вывода и т.п.;
- RDS_PANOP_MOVED – панель перемещена. Как правило, от модели в этом случае не требуется каких-либо действий, но, при необходимости, она может отреагировать и на это;
- RDS_PANOP_PAINT – панель необходимо перерисовать. Этот вызов производится только для панелей, созданных с флагом RDS_PAN_F_PAINTMSG, как в нашем случае.

Наша модель будет реагировать на все действия с панелью, кроме RDS_PANOP_MOVED.

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

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

Рис. 66. Блок с пустой панелью

Сделаем из нашего блока индикатор, который будет наглядно отображать три угловых координаты какого-либо объекта. Используя морскую терминологию, назовем эти координаты курсом, дифферентом и креном (рис. 67).

Рис. 67. Угловые координаты объекта

Блок будет иметь три вещественных входа: «Dir» (курс), «List» (крен) и «Pitch» (дифферент). В зависимости от их значений будем поворачивать внутри панели какой-нибудь трехмерный объект на соответствующие углы по трем координатам. Чтобы не усложнять пример, возьмем в качестве объекта-индикатора неправильную треугольную пирамиду, грани которой раскрашены в разные цвета (рис. 68, рис. 69). Нижнюю плоскость пирамиды (треугольник P1-P2-P3) будем считать горизонтальной плоскостью объекта, при этом точка P1 будет соответствовать носовой части объекта, а отрезок P2-P3 – его корме.

Рис. 68. Центральный объект блока

Рис. 69. Внешний вид панели блока

Левая грань пирамиды (P1-P2-P4) будет раскрашена в красный цвет, правая (P1-P4-P3) – в зеленый, задняя (P2-P3-P4) и нижняя (P1-P3-P2) – в белый. Для большей наглядности изображения будем строить его со включенным расчетом освещенности от одного точечного источника (без расчета освещенности трехмерные фигуры с заполненными гранями не выглядят объемными). Точкой «O» на рис. 68 обозначен центр объекта, вокруг этой точки он будет вращаться при изменении угловых координат. Вокруг треугольной пирамиды будут также изображаться две окружности: одна будет лежать в плоскости P1-P2-P3 (и, соответственно, поворачиваться вместе с объектом), а вторая, большего диаметра, в горизонтальной плоскости внешней («мировой») системы координат. По взаимному расположению этих окружностей будет легче оценить углы поворота объекта. Точка наблюдения (место расположения условной камеры, через которую пользователь видит трехмерную сцену) будет выбрана так, чтобы объект при нулевом курсе и дифференте был виден сверху с кормы (рис. 69).

Поскольку данная модель будет работать с функциями библиотеки OpenGL, в ее текст необходимо включить файлы заголовков «gl.h» и «glu.h». Кроме того, нам потребуются некоторые математические функции, поэтому подключим еще и файл «math.h» (добавления выделены цветом):

      #include <windows.h>
      #include <math.h>
      #include <gl/gl.h>
      #include <gl/glu.h>
      #include <RdsDef.h>
      // Подготовка описаний сервисных функций
      #define RDS_SERV_FUNC_BODY GetInterfaceFunctions
      // …

В главную функцию DLL этой модели никаких изменений вносить не будем.

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

      //=================================================
      // Личная область данных блока
      //=================================================
      class TOpenGLInstr
      { private:
          HGLRC Hrc;                 // Контекст OpenGL
          RDS_TIMERID RefreshTimer;  // Таймер обновления

          // Служебная функция – настройка параметров OpenGL
          BOOL SetupGLParams(HDC *pHdc);
        public:
          RDS_HOBJECT Panel;         // Объект-панель

          // Настройка вывода изображения на панель
          void InitWindow(HWND window);
          // Отключение OpenGL
          void Clear(void);
          // Рисование трехмерной сцены
          void RenderScene(double Dir,double List,double Pitch);
          // Сохранение параметров блока
          void SaveText(void);
          // Загрузка параметров блока
          void LoadText(char *text);

          // Создание таймера обновления
          void CreateRefreshTimer(RDS_BHANDLE parent);

          // Конструктор класса
          TopenGLInstr(void);
          // Деструктор класса
          ~TOpenGLInstr();
      };
      //=================================================

В закрытую область класса (private) добавлен контекст OpenGL Hrc (он будет использоваться в функциях-членах класса, отвечающих за рисование) и служебная функция SetupGLParams для настройки области рисования, освещения и прочих параметров OpenGL (эти настройки выделены в отдельную функцию для улучшения читаемости примера). В открытую область (public) добавлено несколько функций, которые будут вызываться непосредственно из модели блока для настройки оконного объекта, его уничтожения, и для рисования изображения.

Кроме внесения изменений в класс личной области, введем еще несколько служебных функций, которые помогут в геометрических расчетах и в построении трехмерного объекта. Можно было бы также сделать их членами класса личной области данных блока, но, поскольку им не нужен доступ к каким-либо данным блока, логичнее сделать их отдельными функциями.

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

      // Вычисление нормали к треугольнику
      // p1,p2,p3 – массивы координат вершин {x,y,z}
      // norm – массив, в котором возвращаются вычисленные
      // координаты нормали
      void NormalToTriangle(GLfloat *p1,GLfloat *p2,
                                       GLfloat *p3,GLfloat *norm)
      { double m[3],a[3],b[3],R; // Вспомогательные переменные

        // Символические имена для координат векторов
        #define x 0
        #define y 1
        #define z 2

        // Вычисляем векторное произведение векторов
        // a (p1->p2) и b (p1->p3)
        for(int i=0;i<3;i++)
          { a[i]=p2[i]-p1[i];
            b[i]=p3[i]-p1[i];
          }
        m[x]=a[y]*b[z]-a[z]*b[y];
        m[y]=a[z]*b[x]-a[x]*b[z];
        m[z]=a[x]*b[y]-a[y]*b[x];

        // Длина получившегося вектора
        R=sqrt(m[x]*m[x]+m[y]*m[y]+m[z]*m[z]);

        // Приводим вектор к единичной длине
        for(int i=0;i<3;i++)
          norm[i]=(GLfloat)(m[i]/R);

        // Отмена макроопределений символических имен координат
        #undef x
        #undef y
        #undef z
      }
      //=================================================

Координаты вершин треугольника передаются в функцию в виде трех массивов p1, p2 и p3, содержащих координаты вершин. Каждый массив содержит три элемента типа GLfloat – вещественного типа, используемого в библиотеке OpenGL. Координата x хранится в первом элементе массива, y – во втором, z – в третьем. Вычисленные координаты вектора нормали записываются в массив norm с точно такой же структурой. Для большей ясности текста функции внутри нее введены символические имена x, y и z для индексов массивов координат 0, 1 и 2 соответственно – p1[x] выглядит гораздо понятнее, чем p1[0]. Для работы с координатами выбраны именно массивы, а не структуры с полями x, y и z, поскольку функции OpenGL могут работать именно с такими массивами. Внутри функции вычисляется векторное произведение сторон треугольника p1-p2 и p1-p3 и длина R получившегося вектора, после чего все компоненты вектора делятся на его длину. В результате получается вектор единичной длины, перпендикулярный поверхности треугольника. Следует помнить, что многоугольники в OpenGL – ориентированные, то есть имеют «лицевую» и «изнаночную» стороны. При рисовании объемных фигур чаще всего изображается только лицевая сторона всех многоугольников, изнаночная считается внутренней, и на ее рисование ресурсы не тратятся. Для правильного расчета освещенности вектор нормали каждой грани объемной фигуры должен быть направлен наружу, поэтому при вызове функции NormalToTriangle точки треугольника необходимо задавать в правильном порядке, чтобы векторное произведение p1-p2 и p1-p3 также было направлено наружу фигуры. Например, для вычисления нормали к левому переднему (красному) треугольнику P1-P2-P4 необходимо сделать вызов

      NormalToTriangle(p2,p4,p1,norm);

Теперь, имея возможность вычислять нормаль к грани объемной фигуры, напишем функцию, которая будет строить треугольник по трем точкам при помощи команд OpenGL. Будем считать, что библиотека OpenGL уже инициализирована, вывод на панель в окне подсистемы настроен, и все необходимые режимы рисования уже установлены – все это будет рассмотрено позднее. Функция будет достаточно простой:

      // Построение треугольника p1-p2-p3
      void DrawTriangleGL(GLfloat *p1,GLfloat *p2,GLfloat *p3)
      { GLfloat norm[3]; // Массив для вычисления нормали

        // Вычислить нормаль
        NormalToTriangle(p1,p2,p3,norm);
        // Установить нормаль в OpenGL
        glNormal3fv(norm);
        // Задать три точки треугольника
        glVertex3fv(p1);
        glVertex3fv(p2);
        glVertex3fv(p3);
      }
      //=================================================

Как и в функцию NormalToTriangle, в новую функцию DrawTriangleGL координаты вершин треугольника передаются в трех трехэлементных массивах p1, p2 и p3. Функция вычисляет вектор нормали к заданному треугольнику и записывает его координаты во вспомогательный массив norm, после чего эти координаты передаются библиотеке OpenGL функцией glNormal3fv (суффикс «3fv» во всех функциях OpenGL указывает на то, что данная функция принимает в качестве входного параметра массив из трех элементов Glfloat, именно поэтому во всех написанных нами функциях для хранения координат будут использоваться такие массивы). Затем функцией glVertex3fv в библиотеку передаются координаты точек p1, p2 и p3. На самом деле, перед установкой координат вершин треугольника и нормалей, необходимо указать библиотеке, что передаваемые точки являются именно вершинами треугольника, а не точками ломаной линии, концами отрезка, и т.п. Для этого служат специальные функции OpenGL glBegin и glEnd, которые будут вызываться во внешней функции, строящей всю объемную фигуру и вызывающей DrawTriangleGL.

В OpenGL в каждой точке многоугольника может быть задана своя нормаль (это позволяет более реалистично отображать кривые поверхности, аппроксимированные многоугольниками), но, в данном случае, для всех точек треугольника будет использован один и тот же вектор нормали, вычисленный в массиве norm. Именно поэтому на три вызова glVertex3fv в функции приходится всего один вызов glNormal3fv. Задав нормаль один раз, можно не повторять задание для каждой очередной точки многоугольника.

Обычно «лицевой» стороной многоугольника считается та, при взгляде на которую вершины следуют друг за другом против часовой стрелки. В связи с этим, при вызове функции DrawTriangleGL важно передавать ей точки треугольника в правильном порядке. Функция написана так, что этот порядок совпадает с порядком аргументов функции NormalToTriangle. Например, для построения треугольника P1-P2-P4 необходимо сделать следующий вызов:

      DrawTriangleGL(p2,p4,p1);

Еще одна дополнительная функция, которая потребуется для рисования – это функция построения окружности. Изображаемый объект содержит две окружности: подвижную и неподвижную, эти окружности разного диаметра и, для улучшения восприятия, лучше всего их окрасить в разные цвета. Таким образом, функция должна иметь возможность строить окружность произвольного радиуса и произвольного цвета. Для определенности, будем строить окружность в плоскости XY (поворот подвижной окружности будет осуществляться преобразованиями системы координат перед ее построением, это одно из удобств, предоставляемых библиотекой OpenGL). Окружность будет аппроксимироваться замкнутой ломаной линией с числом звеньев N.

      // Рисование окружности в OpenGL
      void DrawCircleGL(GLfloat R,int N,GLfloat cR,GLfloat cG,GLfloat cB)
      { double aStep=(2.0*M_PI)/N; // Шаг ломаной по углу, радиан
        // Массивы для задания материала
        GLfloat MaterialAmbDiff[4],
                MaterialSpecular[4]={1.0,1.0,1.0,1.0};
        // Запись цвета материала в массив
        MaterialAmbDiff[0]=cR;
        MaterialAmbDiff[1]=cG;
        MaterialAmbDiff[2]=cB;
        MaterialAmbDiff[3]=1.0; // Непрозрачный
        // Установка отражающих свойств (материала) фигуры
        glMaterialfv(GL_FRONT_AND_BACK,GL_AMBIENT_AND_DIFFUSE,MaterialAmbDiff);
        glMaterialf(GL_FRONT_AND_BACK,GL_SHININESS, 50.0);
        glMaterialfv(GL_FRONT_AND_BACK,GL_SPECULAR,MaterialSpecular);
        // Нормаль – по оси Z (окружность в плоскости XY)
        glNormal3f(0.0,0.0,1.0);
        // Рисуем замкнутую ломаную линию
        glBegin(GL_LINE_LOOP);
          for(int j=0;j<N;j++) // Цикл по углу
            { double a=aStep*j; // Угол
              double x=R*cos(a),y=R*sin(a); // Координаты точки
              glVertex3f((GLfloat)x,(GLfloat)y,0);
            }
        glEnd();
      }
      //=================================================

В функцию передаются радиус окружности R, число звеньев ломаной N и три компоненты цвета окружности cR, cG и cB (красный, зеленый и синий соответственно). При задании компонент цвета вещественными числами (например, типа GLfloat, как в данном случае) считается, что интенсивность компоненты изменяется от 0 (минимум) до 1 (максимальная интенсивность). Поскольку мы используем при построении фигур расчет освещенности, для трехмерных объектов недостаточно простого задания цветов их точек и поверхностей. Необходимо задать отражающие свойства этих поверхностей, в терминологии OpenGL – параметры материала. Фактически эти свойства аналогичны цвету: например, если материал отражает зеленый цвет на 100%, а красный и синий – на 0%, то при освещении белым светом он будет выглядеть зеленым. Кроме того, может задаваться степень прозрачности материала, также изменяющаяся от 0 (полностью прозрачный) до 1 (непрозрачный). В OpenGL задается несколько параметров материала для разных типов освещения (окружающего, рассеянного и т.п.). Целью данного примера не является подробный разбор особенностей библиотеки OpenGL, поэтому не будем подробно останавливаться на принципах расчета освещенности (все это подробно описано, например, в «OpenGL Programming Guide»).

В начале функции вычисляется угол aStep между условными линиями, соединяющими соседние точки ломаной, изображающей окружность, и ее центр (при числе сегментов ломаной N он будет равен 2π/N) и отводятся два вспомогательных массива, которые будут использованы для задания свойств материала окружности. Далее в первые три элемента массива MaterialAmbDiff записываются переданные компоненты цвета окружности cR, cG и cB. В четвертый элемент записывается число 1, указывающее на непрозрачность данного объекта. После этого занесенные в массивы параметры устанавливаются в качестве текущего материала для разных типов освещения функциями glMaterialfv и glMaterialf. Теперь все точки многоугольников будут получать эти параметры материала.

Затем функцией glNormal3f устанавливается нормаль к окружности. В отличие от функции glNormal3fv, использовавшейся ранее, функция принимает координаты не в массиве, а в трех отдельных параметрах GLfloat, о чем говорит суффикс «3f» Окружность строится в плоскости XY, поэтому нормаль будем считать направленной вдоль оси Z, то есть координаты единичного вектора будут (0,0,1).

Далее начинаем строить ломаную линию. Необходимо указать библиотеке OpenGL, что последовательно передаваемые ей координаты точек должны быть соединены отрезками, причем последняя переданная точка должна соединяться с первой, то есть линия должна быть замкнутой. Построение любой геометрической фигуры в OpenGL начинается с вызова функции glBegin, в параметре которой указывается тип фигуры. В данном случае это константа GL_LINE_LOOP, обозначающая замкнутую ломаную линию. Завершает построение фигуры вызов функции glEnd. Между этими вызовами находится цикл, в котором координаты x и y точек окружности радиусом R последовательно вычисляются тригонометрическими функциями и передаются в библиотеку вызовом glVertex3f. Для точки окружности с номером i угол a к этой точке будет равен i*aStep, а ее координаты x и y будут равны R*cos(a) и R*sin(a) соответственно. Поскольку окружность лежит в плоскости XY, координата z для всех точек равна нулю. При вызове glBegin был указан параметр GL_LINE_LOOP, поэтому в момент вызова glEnd последняя переданная точка будет соединена отрезком с первой.

Наконец, напишем функцию, которая будет строить центральный объект блока согласно рис. 68. Все параметры, указанные на рисунке (R, b, l, w, h) будут параметрами этой функции. Нижний треугольник объекта P1-P3-P2 будет находиться в плоскости XY, причем точка P1 будет лежать на оси Y, а отрезок P2-P3 будет параллелен оси X. Точка O будет совпадать с началом координат. В этой функции точки объекта не будут пересчитываться при изменении углов курса, дифферента и крена – этим будет заниматься преобразование координат OpenGL. Функция будет иметь следующий вид:

      // Рисование центрального объекта блока
      void DrawArrowGL(double R,double l,double w,double h,double b)
      { // Массивы для координат точек фигуры
        GLfloat p1[3],p2[3],p3[3],p4[3];
        // Материал – дно и корма (белый цвет)
        GLfloat MaterialBack[]  = {1.0, 1.0, 1.0, 1.0};
        // Материал – левый борт (красный цвет)
        GLfloat MaterialLeft[]  = {1.0, 0.5, 0.5, 1.0};
        // Материал – правый борт (зеленый цвет)
        GLfloat MaterialRight[] = {0.5, 1.0, 0.5, 1.0};

        // Символические имена для координат точек
        #define x 0
        #define y 1
        #define z 2

        // Вычисление координат точек фигуры
        p1[x]=0;  p1[y]=R;       p1[z]=0;
        p2[x]=-w; p2[y]=R-l;     p2[z]=0;
        p3[x]=w;  p3[y]=p2[y];   p3[z]=0;
        p4[x]=0;  p4[y]=p2[y]+b; p4[z]=h;

        // Построение граней
        glBegin(GL_TRIANGLES);
          // Свойства материала – общие для всех
          glMaterialf(GL_FRONT,GL_SHININESS,20.0);
          // Свойства материала – корма и дно
          glMaterialfv(GL_FRONT,GL_AMBIENT_AND_DIFFUSE,MaterialBack);
          glMaterialfv(GL_FRONT,GL_SPECULAR,MaterialBack);
          // Нижний треугольник (дно)
          DrawTriangleGL(p1,p3,p2);
          // Задний треугольник (корма)
          DrawTriangleGL(p2,p3,p4);
          // Свойства материала – левый борт
          glMaterialfv(GL_FRONT,GL_AMBIENT_AND_DIFFUSE,MaterialLeft);
          glMaterialfv(GL_FRONT,GL_SPECULAR,MaterialLeft);
          // Левый передний треугольник
          DrawTriangleGL(p2,p4,p1);
          // Свойства материала – правый борт
          glMaterialfv(GL_FRONT,GL_AMBIENT_AND_DIFFUSE,MaterialRight);
          glMaterialfv(GL_FRONT,GL_SPECULAR,MaterialRight);
          // Правый передний треугольник
          DrawTriangleGL(p4,p3,p1);
        glEnd();

        // Отмена макроопределений символических имен координат
        #undef x
        #undef y
        #undef z
      }
      //=================================================

Для всех четырех точек фигуры в начале функции отводятся трехэлементные массивы вещественных чисел p1, p2, p3 и p4 – в этих массивах будут храниться вычисленные координаты точек, они же буду передаваться в функцию DrawTriangleGL для построения граней фигуры. Кроме того, описываются три четырехэлементных массива параметров материала: MaterialBack для дна и кормы объекта, MaterialLeft для левого борта и MaterialRight для правого. Эти массивы сразу заполняются значениями, соответствующими белому, красному и зеленому цветам. Далее, как и в уже описанной функции NormalToTriangle, вводятся макроопределения для индексов массивов координат, и, согласно значениям R, l, w, h и b, в массивах p1, p2, p3 и p4 вычисляются координаты всех четырех точек фигуры.

Теперь, когда координаты вычислены, можно приступать к построению граней. Сначала вызывается функция glBegin с параметром GL_TRIANGLES, указывающим на то, что следующие за этим вызовом тройки точек будут описывать отдельные треугольники. Далее поочередно устанавливаются параметры материала каждой грани и вызывается функция DrawTriangleGL, описанная выше. Каждый ее вызов передает библиотеке тройку точек, которые интерпретируются как вершины треугольника соответствующей грани. После того, как все четыре грани построены, вызывается функция glEnd.

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

      // Настройка вывода изображения на панель
      void TOpenGLInstr::InitWindow(HWND window)
      { HDC Hdc; // Контекст устройства Windows
        // Структура для установки формата изображения
        PIXELFORMATDESCRIPTOR pfd = {
        sizeof(PIXELFORMATDESCRIPTOR),// Размер структуры
          1,                          // Номер версии
          PFD_DRAW_TO_WINDOW |        // Вывод в окно
            PFD_SUPPORT_OPENGL |      // Поддержка OpenGL
            PFD_DOUBLEBUFFER,         // Двойная буферизация
          PFD_TYPE_RGBA,              // Формат цвета – RGBA
          24,                         // Глубина цвета – 24 бита
          0,0,0,0,0,0,                // (здесь не используется)
          0,0,0,0,0,0,0,              // (здесь не используется)
          32,                         // Z-буфер - 32 бита
          0,0,                        // (здесь не используется)
          PFD_MAIN_PLANE,             // Главный слой
          0,                          // Зарезервировано
          0,0,                        // Маски слоев (не исп.)
          };
        int PixelFormat;

        // Получение контекста оконного объекта
        Hdc=GetDC(window);

        // Установка формата изображения по структуре pfd
        PixelFormat=ChoosePixelFormatChoosePixelFormat(Hdc,&pfd);
        SetPixelFormatSetPixelFormat(Hdc,PixelFormat,&pfd);

        // Создание контекста OpenGL
        Hrc=wglCreateContext(Hdc);

        // Установка этого контекста в качестве текущего
        wglMakeCurrent(Hdc,Hrc);
      }
      //=================================================

Эта функция получает в качестве параметра дескриптор оконного объекта, созданного RDS для панели. Прежде чем выводить что-либо в это окно при помощи OpenGL, необходимо настроить формат изображения окна, а для этого необходимо предварительно получить контекст рисования этого окна при помощи функции GetDC. Контекст записывается в локальную переменную Hdc (хотя этот контекст нам и потребуется позже в других функциях, лучше каждый раз получать его вызовом GetDC для оконного объекта панели). Теперь для полученного контекста можно установить формат изображения при помощи функций Windows API ChoosePixelFormat и SetPixelFormat, при этом сами параметры изображения задаются в структуре pfd типа PIXELFORMATDESCRIPTOR (подробные описания функций и структуры см. в Windows API). В данном случае важно, что мы запрашиваем поддержку OpenGL (флаг PFD_SUPPORT_OPENGL), двойную буферизацию (флаг PFD_DOUBLEBUFFER), двадцатичетырехбитный цвет формата RGBA (то есть цвет каждого цветового канала задается отдельно одни байтом) и тридцатидвухбитный Z-буфер. Двойная буферизация позволяет иметь два буфера изображения: в одном, невидимом, производится рисование, а другой в это время изображается на экране. Когда изображение нарисовано полностью, буферы меняются местами – новое изображение появляется на экране, а буфер со старым изображением становится невидимым и в нем можно готовить к показу следующий кадр. Такой режим работы существенно улучшает анимацию, убирая мигания изображения при перерисовке, т.к. она производится в невидимом буфере.

В Z-буфере для каждой точки изображения хранится «глубина» – третья координата, перпендикулярная плоскости проекции. Этот буфер используется для отсечения частей трехмерных объектов, перекрытых другими объектами. Перед записью очередной точки объекта в буфер изображения ее глубина сравнивается с уже записанной в Z-буфере. Если новая точка располагается дальше от переднего плана, чем уже записанная, она отбрасывается, если ближе – записывается в буфер изображения и ее глубина записывается в Z-буфер. Чем выше разрядность Z-буфера, тем точнее будут сравниваться точки по глубине, и тем правильнее будет происходить рисование перекрывающихся (особенно, близко расположенных или пересекающихся) объектов. Тридцати двух бит хватит для большинства случаев, тем более, для такого простого изображения, как наше.

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

Перед завершением работы с трехмерной графикой необходимо уничтожить созданный контекст OpenGL. Для этого напишем функцию Clear и будем вызывать ее из модели перед уничтожением оконного объекта:

      // Настройка OpenGL
      void TOpenGLInstr::Clear(void)
      { // Отключение текущего контекста OpenGL
        wglMakeCurrent(NULL,NULL);
        // Уничтожение созданного контекста
        wglDeleteContext(Hrc);
      }
      //=================================================

В этой функции всего два вызова: сначала при помощи wglMakeCurrent с параметром NULL отключается текущий контекст (чтобы не удалить выбранный), а затем функцией wglDeleteContext созданный ранее в InitWindow контекст удаляется.

Теперь запишем служебную функцию, которая будет выполнять настройки OpenGL перед рисованием:

      // Служебная функция – настройка параметров OpenGL
      BOOL TOpenGLInstr::SetupGLParams(HDC *pHdc)
      { // Параметры перспективной проекции
        const GLfloat zNear=0.1;   // Ближняя плоскость отсечения
        const GLfloat zFar=1000.0; // Дальняя плоскость отсечения
        const GLfloat vAngle=30.0; // Угол зрения

        HDC Hdc;	// Контекст устройства (окна) Windows

        // Расположение источника освещения
        const double lightDistance=500.0; // Расстояние до объекта
        const double lightRotation=-55.0; // Азимут в градусах
        const double lightPitch=45.0;     // Угол места в градусах

        // Вспомогательные переменные
        RDS_PANDESCRIPTION descr;
        int width,height;
        GLfloat array[4];
        double l_r,l_p;

        // Получение описания панели и дескриптора ее окна
        descr.servSize=sizeof(RDS_PANDESCRIPTION);
        if(!rdsPANGetDescr(Panel,&descr)) // Не удалось
          return FALSE;
        if(descr.Handle==NULL) // Нет оконного объекта
          return FALSE;
        // Проверка высоты и ширины панели
        if(descr.Height==0 || descr.Width==0)
          return FALSE; // Негде рисовать

        // Получение контекста окна и передача в вызвавшую функцию
        // его через параметр-указатель
        Hdc=GetDC(descr.Handle);
        if(pHdc) *pHdc=Hdc;

        // Установка контекста Hrc, созданного при инициализации,
        // в качестве текущего
        if(!wglMakeCurrent(Hdc,Hrc))
          return FALSE; // Не удалось

        // Уcтановка различных параметров, влияющих на качество рисования
        // и способ закраски многоугольников
        glEnable(GL_DEPTH_TEST);
        glEnable(GL_CULL_FACE);
        glHint(GL_POLYGON_SMOOTH_HINT,GL_FASTEST);
        glEnable(GL_POINT_SMOOTH);
        glBlendFunc (GL_SRC_ALPHA,GL_ONE_MINUS_SRC_ALPHA);

        glClearColor(0.0f,0.0f,0.0f,1.0f); // Цвет фона - черный
        glClearDepth(1.0); // Значение для очистки Z-буфера

        //----------- Установка освещения ----------
        // Общее рассеянное освещение
        array[0]=array[1]=array[2]=0.5; // Белый, интенсивность 0.5
        array[3]=1.0;
        glLightModelfv(GL_LIGHT_MODEL_AMBIENT,array);
        // Вычисление координат источника света
        l_r=(lightRotation*M_PI)/180.0;
        l_p=(lightPitch*M_PI)/180.0;
        array[0]=(GLfloat)(lightDistance*cos(l_p)*sin(l_r)); // X
        array[1]=(GLfloat)(lightDistance*sin(l_p));          // Y
        array[2]=(GLfloat)(lightDistance*cos(l_p)*cos(l_r)); // Z
        array[3]=0.0;
        glLightfv(GL_LIGHT0,GL_POSITION,array);
        // Рассеянное освещение от источника отсутствует
        array[0]=array[1]=array[2]=0.0; // Отсутствует - интенсивность 0
        array[3]=1.0;
        glLightfv(GL_LIGHT0,GL_AMBIENT,array);
        // Другие виды освещения – белый цвет
        array[0]=array[1]=array[2]=0.5; // Белый, интенсивность 0.5
        glLightfv(GL_LIGHT0,GL_DIFFUSE,array);
        glLightfv(GL_LIGHT0,GL_SPECULAR,array);
        // Включение источника света
        glEnable(GL_LIGHT0);
        // Разрешение расчета освещения
        glEnable(GL_LIGHTING);
        // Режим расчета “затенения"
        glShadeModel(GL_SMOOTH);
        // Способ расчета освещения
        glLightModeli(GL_LIGHT_MODEL_TWO_SIDE, GL_FALSE);
        //------------------------------------------

        // Установка области окна для рисования
        glViewport(0,0,descr.Width,descr.Height);

        // Настройка перспективной проекции на эту область
        glMatrixMode(GL_PROJECTION);
        glLoadIdentity();
        gluPerspective(vAngle,
          (GLfloat)descr.Width/(GLfloat)descr.Height,
          zNear,zFar);

        // Переключение на матрицу моделей (для рисования объектов)
        glMatrixMode(GL_MODELVIEW);
        glLoadIdentity();

        return TRUE; // Установка параметров успешно завершена
      }
      //=================================================

Функция принимает единственный параметр – указатель на контекст устройства Windows. Через него она вернет в вызвавшую функцию контекст окна панели, на которой будет рисоваться трехмерное изображение. В вызвавшей функции, конечно, можно было бы получить этот контекст при помощи вызова GetDC, но, поскольку SetupGLParams все равно его получает, можно этим воспользоваться. Возвращает эта функция логическое значение, указывающее на успешность установки параметров. Если параметры установить не удалось (например, при невидимой панели, не имеющей связанного оконного объекта), пытаться что-либо рисовать бессмысленно.

Большая часть функции состоит из вызовов OpenGL для установки режимов, настройки освещения и перспективной проекции и т.п. Короткие пояснения к этим вызовам вставлены непосредственно в текст функции, мы не будем разбирать их более подробно. Исчерпывающая информация по этим функциям находится в описании OpenGL. Детально рассмотрим только привязку области вывода OpenGL к оконному объекту панели.

Сразу после описаний вспомогательных переменных вызывается сервисная функция RDS rdsPANGetDescr, которая записывает описание панели Panel в структуру descr типа RDS_PANDESCRIPTION. В этой структуре много полей, соответствующих различным параметрам панели, но нас будут интересовать только три: дескриптор окна Handle, ширина окна Width и его высота Height. Прежде всего, мы сравниваем полученный дескриптор с NULL – если дескриптор нулевой, окно для панели еще не создано, и настраивать OpenGL и рисовать что-либо бессмысленно (функция возвращает FALSE). В противном случае высота и ширина оконного объекта также сравниваются с нулем – если хотя бы один из размеров окна нулевой, рисовать будет негде. Следует помнить, что для панелей с рамкой создаваемый внутри них оконный объект меньше самой панели на размер рамки, при этом Width и Height в структуре RDS_PANDESCRIPTION всегда указывают именно размер оконного объекта, а не самой панели. Вызов glViewport, устанавливающий область рисования в окне, задает в качестве верхнего левого угла прямоугольной области точку (0,0), а в качестве правого нижнего – (descr.Width,descr.Height). Это полный размер внутреннего оконного объекта панели.

Последняя функция-член класса личной области данных блока, которую мы опишем – функция построения трехмерного изображения (часто называемого «сценой») RenderScene:

      // Рисование трехмерной сцены
      void TOpenGLInstr::RenderScene(double Dir,double List,double Pitch)
      { HDC Hdc; // Контекст устройства Windows
        // Угол места камеры в градусах
        const GLfloat Camera_Pitch=-50.0;
        // Расстояние до камеры
        const GLfloat Camera_Distance=300.0;

        // Настройка параметров OpenGL и получение контекста окна
        if(!SetupGLParams(&Hdc))
          return;

        // Очистка буфера изображения и Z-буфера
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

        // Записать матрицу трансформаций в стек
        glPushMatrix();
        // Трансформации камеры
        glTranslatef(0.0,0.0,-Camera_Distance); // Отодвигаем от камеры
        glRotatef(Camera_Pitch,1,0,0);          // Поворачиваем
        // Рисование неподвижной зеленой (0,1,0) окружности
        DrawCircleGL(60,100,0.0,1.0,0.0);

        if(Dir!=DoubleErrorValue &&
           List!=DoubleErrorValue &&
           Pitch!=DoubleErrorValue) // Можно рисовать
          { // Поворот на угол курса
            if(Dir!=0.0)
              glRotatef(Dir,0,0,1);
            // Поворот на угол дифферента
            if(Pitch!=0.0)
              glRotatef(Pitch,1,0,0);
            // Поворот на угол крена
            if(List!=0.0)
              glRotatef(-List,0,1,0);

            // Рисование подвижной белой (1,1,1) окружности
            DrawCircleGL(55,100,1.0,1.0,1.0);
            // Рисование центрального объекта
            DrawArrowGL(50,80,30,10,10);
          }
        // Восстановить матрицу трансформаций из стека
        glPopMatrix();
        // Завершить незавершенное рисование, если нужно
        glFlush();
        // Поменять местами видимый и рабочий буферы
        SwapBuffers(Hdc);
      }
      //=================================================

Функция принимает три вещественных параметра: Dir (курс), List (крен) и Pitch (дифферент), они используются для поворота центрального объекта при рисовании. Внутри функции прежде всего вызывается SetupGLParams, настраивающая параметры OpenGL и устанавливающая текущий контекст рисования. Если параметры настроить не удалось, функция вернет FALSE, и RenderScene немедленно завершится. Конечно, можно было бы не настраивать все параметры OpenGL при каждом рисовании, но целью этого примера является демонстрация принципиальной возможности работы с OpenGL на панелях в окне подсистемы, а не оптимизация рисования. К тому же, без этой оптимизации модель блока будет значительно проще, что сделает пример более понятным.

Сразу после настройки функция очищает рабочий буфер изображения и Z-буфер, заполняя их значениями по умолчанию. Эти значения задаются в SetupGLParams: для буфера изображения – черный цвет, для Z-буфера – максимально возможная дальность (1.0). После этого рабочая матрица OpenGL (в данный момент – матрица, определяющая трансформации системы координат) помещается в стек функцией glPushMatrix. Стек матриц – удобная возможность OpenGL, позволяющая быстро возвращаться к исходной системе координат. Например, если нужно нарисовать какой-то повернутый объект в стороне от начала текущей системы координат, можно поместить матрицу в стек, затем передвинуть систему координат, повернуть ее, нарисовать объект в начале координат (он будет смещенным и повернутым), а затем, вместо того, чтобы поворачивать и перемещать систему координат обратно, можно просто извлечь матрицу из стека, что приведет к немедленному возврату к старой системе координат. Помещая исходную систему координат в стек перед различными трансформациями, мы получаем возможность быстро вернуться к исходной.

Первыми выполняются трансформации «камеры», то есть точки наблюдения. Функция DrawArrowGL, которую мы написали ранее, строит объект в начале координат, поэтому, если систему координат не переместить, точка наблюдения (начало координат) будет находиться внутри объекта. Чтобы наблюдать объект со стороны, мы перемещаем систему координат на расстояние Camera_Distance вдоль оси Z в отрицательном направлении функцией OpenGL glTranslatef, а затем поворачиваем ее на угол Camera_Pitch вокруг оси X функцией glRotatef (Camera_Distance и Camera_Pitch – константы, описанные в начале функции). Теперь «камера» смотрит на начало координат, где будет построен объект, сзади и немного сверху.

Далее вызовом DrawCircleGL строится неподвижная окружность зеленого цвета (красный и синий компоненты цвета – нулевые, зеленый – единица) радиусом 60. Эта окружность лежит в горизонтальной плоскости и не поворачивается вместе с объектом, поэтому для нее никаких трансформаций не требуется.

Теперь нужно нарисовать объект и подвижную окружность, повернутые согласно трем заданным угловым координатам. Прежде всего, нужно сравнить полученные функцией значения углов со специальным значением, сигнализирующим об ошибке вычислений. Это значение получено из RDS в главной функции DLL и помещено в глобальную переменную DoubleErrorValue. Такие проверки, как уже не раз отмечалось, нужно выполнять для всех значений, которые передаются из других блоков. Если все три значения углов – допустимые, можно приступать к рисованию.

Для поворота системы координат, в которой будут строиться объект и окружность, используются функции OpenGL glRotatef. Каждый из трех поворотов выполняется вокруг определенной оси координат – координаты вектора оси поворота задают три последних параметра функции. Поворот на угол курса выполняется вокруг оси Z, на угол дифферента – вокруг оси X, на угол крена – вокруг оси Y, это в точности соответствует рис. 67. После поворотов вызывается функция DrawCircleGL для построения белой окружности радиусом 55, и DrawArrowGL для построения центрального объекта (размеры объекта, как и радиусы окружностей и расстояние до точки наблюдения, подобраны вручную).

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

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

Смещение | Имя | Тип | Размер | Вход/выход | Пуск | Начальное значение
--- | --- | --- | --- | --- | --- | ---
0 | Start | Сигнал | 1 | Вход | ✓ | 0
1 | Ready | Сигнал | 1 | Выход |  | 0
2 | Dir | double | 8 | Вход |  | 0
10 | List | double | 8 | Вход |  | 0
18 | Pitch | double | 8 | Вход |  | 0

Теперь добавим в функцию модели новые вызовы и переменные. Изменения, как всегда, выделены цветом:

      // Модель блока с панелью
      extern "C" __declspec(dllexport) int RDSCALL OpenGLInstr(
            int CallMode,
            RDS_PBLOCKDATA BlockData,
            LPVOID ExtParam)
      {
      // Макроопределения для статических переменных
      #define Start (*((char *)(pStart)))
      #define Ready (*((char *)(pStart+RDS_VSZ_S)))
      #define Dir (*((double *)(pStart+2*RDS_VSZ_S)))
      #define List (*((double *)(pStart+2*RDS_VSZ_S+RDS_VSZ_D)))
      #define Pitch (*((double *)(pStart+2*RDS_VSZ_S+2*RDS_VSZ_D)))
        // Указатель на личную область, приведенный к нужному типу
        TOpenGLInstr *data=(TOpenGLInstr*)(BlockData->BlockData);
        // Вспомогательная – указатель на структуру параметров при
        // действиях с панелью (будет использована в реакциях)
        RDS_PPANOPERATION param;

        switch(CallMode)
          { // Инициализация блока
            case RDS_BFM_INIT:
              // Создание личной области данных
              // (при этом в конструкторе будет создана панель)
              BlockData->BlockData=data=new TOpenGLInstr();
              break;

            // Очистка данных блока
            case RDS_BFM_CLEANUP:
              // В деструкторе класса панель будет уничтожена
              delete data;
              break;

            // Проверка типа переменных
            case RDS_BFM_VARCHECK:
              if(strcmp((char*)ExtParam,"{SSDDD}"))
                return RDS_BFR_BADVARSMSG;
              return RDS_BFR_DONE;
            // Запуск расчета
            case RDS_BFM_STARTCALC:
              // Создание таймера обновления
              data->CreateRefreshTimer(BlockData->Parent);
              break;

            // Вызов функции настройки или двойной щелчок
            // левой кнопки мыши
            case RDS_BFM_SETUP:
            case RDS_BFM_MOUSEDBLCLICK:
              // Показать панель
              rdsSetObjectInt(data->Panel,RDS_PAN_VISIBLE,0,1);
              break;

            // Сохранение параметров блока
            case RDS_BFM_SAVETXT:
              data->SaveText();
              break;

            // Загрузка параметров блока
            case RDS_BFM_LOADTXT:
              data->LoadText((char*)ExtParam);
              break;

            // Действия с панелью
            case RDS_BFM_BLOCKPANEL:
              // Приведение указателя на структуру, переданного в
              // ExtParam, к нужному типу
              param=(RDS_PPANOPERATION)ExtParam;
              // Разные действия в зависимости от операции с панелью
              switch(param->Operation)
                { // Создание оконного объекта для панели
                  case RDS_PANOP_CREATE:
                    // Настройка вывода изображения на панель
                    data->InitWindow(param->Panel->Handle);
                    break;
                  // Уничтожение оконного объекта панели
                  case RDS_PANOP_DESTROY:
                    // Отключение OpenGL
                    data->Clear();
                    break;
                  // Размер панели изменен
                  case RDS_PANOP_RESIZED:
                    // При изменении размера – просто перерисовка
                    data->RenderScene(Dir,List,Pitch);
                    break;
                  // Необходимо перерисовать изображение
                  case RDS_PANOP_PAINT:
                    data->RenderScene(Dir,List,Pitch);
                    break;
                }
              break;

           // Необходимо обновить окна блока (вызывается таймером)
           case RDS_BFM_WINREFRESH:
              // Перерисовка изображения
              data->RenderScene(Dir,List,Pitch);
              break;
          }
        return RDS_BFR_DONE;

      // Отмена макроопределений
      #undef Pitch
      #undef List
      #undef Dir
      #undef Ready
      #undef Start
      #undef pStart
      }
      //=================================================

Теперь, когда у блока появились статические переменные, в его модель добавлена обычная реакция на вызов в режиме RDS_BFM_VARCHECK для проверки допустимости их типа. Изменения также произошли в реакциях на вызовы RDS_BFM_BLOCKPANEL и RDS_BFM_WINREFRESH.

В режиме RDS_BFM_BLOCKPANEL (при получении от RDS сообщения об операции с панелью) модель теперь выполняет следующие действия:

- при создании окна внутри панели (RDS_PANOP_CREATE) вызывается функция InitWindow, настраивающая формат изображения в окне и создающая для окна контекст OpenGL;
- перед уничтожением окна (RDS_PANOP_DESTROY) вызывается функция Clear, уничтожающая ранее созданный контекст OpenGL;
- при изменении размеров панели (RDS_PANOP_RESIZED) и получении сообщения о необходимости перерисовки (RDS_PANOP_PAINT) вызывается функция рисования изображения RenderScene, в параметрах которой передаются входы блока.

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

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

Рис. 70. Работающий блок-индикатор