Библиотека VGUI 2
Обзор
Библиотека VGUI2 (vgui2.dll) - это составляющая графического
интерфейса пользователя (GUI) поставляемого с движком Source. Все
Source и Steam приложения используют VGUI для отображения окон, диалогов
или меню. Объектная архитектура иерархическаяи все имплементируемые
элементы управляются классами основанными на базе VGUI. Система событий
клавиатуры и мыши очень схожа с другими GUI библиотеками (подобно Java
Swing или .Net-подобным). Имплементация наиболее общих GUI элементов на
подобии кнопок, текстовых полей или изображений предоставляется
библиотекой VGUI-контроллеров (vgui_controls.lib). Кроме отрисовки GUI
элементов и обработки ввода, VGUI также управляет локализацией для
отображения текста предпочтительного для данного польхователя. Все
заголовочные файлы VGUI размещены в publicvgui, управляющие элементы
определяются в publicvgui_controls.
Обычно разработчик мода использует VGUI в проекте client.dll для
отображения меню, HUD элементов внутри игрового экрана (на оружии или
игровых терминалах и т.д.). Базовый класс всех VGUI элементов
наследуется от vgui::Panel, который определяет область экрана
определенного размера и позиции, может отрисовывать себя и обрабатывать
события. Диалоговые окна, текстовые поля и кнопки являются VGUI панелями
в иерархическом родительском дереве. Самая первая панель (корневая)
предоставляется Source движком. Клиентская корневая панель (client root
panel) покрывает весь экран, но ничего не отображает. Когда вы можете
использовать корневую панель, большинство клиентских панелей используют
общее окно проекции BaseViewport ( g_pClientMode->GetViewport() ).
Эта диаграмма показывает часть иерархии панелей клиента. Стрелка между двумя панелями означает "родитель ..." :
Для навигации по иерархии VGUI панелей во время выполнения игры можно
открыть инструмент VGUI Hierarchy выполнив в консоли команду vgui_drawtree 1. Все панели отобразятся в раскрывающемся древовидном виде. Цвета означают :
белый :
|
Панель видима
|
серый :
|
Панель невидима
|
желтый :
|
Выпадающая панель (фрейм)
|
зеленый :
|
Панель в фокусе
|
Доступны следующие опции:
Show Visible : |
отобразить все видимые
|
Show Hidden : |
отобразить все невидимые
|
Popups only : |
отобразить выпадающие панели (фреймв)
|
Highlight Mouse Over : |
Панели подсвечиваются цветной рамкой во время наведения на них мышью.
Дерево панелей будет развернуто для отображения текущей панели.
|
Freeze : |
Зафиксировать текущее состояние
|
Show Addresses : |
Отобразить адрес памяти панели
|
Show Alpha : |
отобразить альфа-значение, 0 = прозрачный, 255 = непрозрачный
|
In render Order : |
отобразить дерево панелей в порядке рендеринга
|
Класс Panel
VGUI класс Panel является базовым классом для более чем 50 различным
управляющим элементам, они определены в publicvgui_controls.
Заголовочные файлы обычно предоставляют хорошую информацию о
возможностях и поведении элементов. Следующая диаграмма показывает
иерархию для некоторых самых часто используемых элементов:
Поскольку все VGUI классы наследуются от Panel, рассмотрим общие методы и свойства.
наиболее часто используемый конструктор для создания панели- это Panel(Panel *parent, const char *panelName),
так как каждая панель нуждается в родительской панели она соответствует
ее уникальному идентификатору в иерархии. После того как панель создана
можно отображать/скрывать ее с помощью SetVisible(bool state).
VGUI хранит дескриптор для каждой следующей панели, который
используется для глобальной адресации панели без использования
указателей. Несколько функций оповещающих о событиях использкют эти
дескрипторы VPANEL для идентификации панелей источника и получателя. Для получения уникального дескриптора VPANEL панели используйте GetVPanel().
Для установки размеров и положения панели испрользуйте методы SetPos(int x,int y) и SetSize(int wide,int tall).
Размер всегда указывается в пикселах и никогда не изменяется
автоматически с изменением разрешения экрана. Положение всегда
относительно данной родительской панели, где (0,0) - верхний левый угол.
Для индексации в иерархии панелей используется определенная панель как точка отсчета, дескриптор VPANEL родительской панели извлекается с помощью GetParent() или GetVParent(). Естественно, панель может иметь только одну родительскую, но несколько дочерних паенлей, кнопок, списковых элементов и т.д.). GetChildCount() возвращает общее число дочерних элементов, где каждый из детей может быть извлечен по порядковому индексу через GetChild(int index).
Если панель должна реагировать на события ввода например нажатие клавиш или клики мыши, существует виртуальные методы OnEvent() на подобии OnMousePressed(MouseCode code) или OnKeyCodePressed(KeyCode code),
в вашем случае они должны быть переопределены. Ввод событий мыши или
клавиатуры может быть включен/выключен для панели используя SetMouseInputEnabled(bool state) или SetKeyBoardInputEnabled(bool state).
Другая важная виртуальная функция которая дложна быть переопределена - OnThink()
которая вызывается каждый раз когда панель перерисовывается. Иногда это
слишком большой промежуток времени и может понадобиться обновить
состояние. Для этого можно использовать обработчик OnTick()
который вызывается через очень короткие интервалы времени. Но перед тем
как его использовать нужно сообщить VGUI что будет вызываться OnTick() для данной панели и в заданные интервалы времени, это делается с помощью ivgui()->AddTickSignal(VPANEL panel, int intervalMilliseconds).
Окна и всплывающие окна
Окно описывается как лежащее выше и занимающее 100% пространства
родителя, расположение всегда относительно и фиксировано, смещение
можно изменить только с помощью кода. Также они отображают себя только в
области занимаемую родителем, если перемещать VGUI элемент вне
родительской панели, он будет обрезаться. Данные елементы хорошо
применять для управляющих елементов, изображений или текстовых полей на
панелях, не способных к независимому перемещению пользователем,
изменению размеров и сворачиванию. Для таких случаев VGUI панели
применяют функцию MakePopup() которая
отделяет панель от рендеринга родителяи делает его новым, независимым
окном. Но по прежнему оно принадлежит родительской панели и становится
невидимой вместе с ней.
Иногда может показаться неудобным применение MakePopup(),
тогда можно использовать класс Frame. Данный класс инкапсулирует все
присущие GUI окнам возможности: титульная строка, системное меню
(закрыть, мвернуть, развернуть), перетаскивание, изменение размеров,
фокус и управление форматом.
Ниже приводится небольшой пример как открывать фрейм и активировать
его. Заметьте, что фрейм удаляет себя когда пользователь закрывает его
функцией Close() (следует удостовериться что высвобождаются все ресурсы в деструкторе).
Frame *pFrame = new Frame( g_pClientMode->GetViewport(), "MyFrame" );
pFrame->SetScheme("ClientScheme");
pFrame->SetSize( 100, 100 );
pFrame->SetTitle("My First Frame", true );
pFrame->Activate(); // set visible, move to front, request focus
Если изменяются размеры фрейма, происходит вызов PerformLayout()
так что фрейм может реорганизовать расположение елементов для более
удобного размещения. Текущее положение и размер фрейма также можно
зафиксировать с помощью SetMoveable(bool state) и SetSizeable(bool state).
Сообщения о событиях
VGUI панели сообщаются через систему сообщений для сигнализации
состояния изменений или событий панелей и их дочерних элементов (или
других панелей). Сообщения не шлются напрямую (например вызовом
listener-функцией), вместо этого они управляются VGUI который доставляет
их панели получателю. Таким образом помимо содержания сообщения, панели
отправитель и получатель должны быть определены через VPANEL
дескрипторы. VGUI шлет сообщения о событиях messages чтобы информировать
о изменении или событии (движение мышью, изменение фокуса ввода и
т.д.).
Имя и содержание сообщения указывается объект KeyValues
(publicKeyValues.h). Класс KeyValues занимает одну из ключевых ролей -
он предоставляет гибкую структуру для хранения записей данных содержащих
строки, целые и вещественные числа. Объект KeyValues содержит имя и
набор записей с данными. Каждая запись имеет уникальный ключ и
соответствующее значение. С помощью KeyValues также создаются
иерархические структуры, использующие вложенные ключи, но в данном
случае, большинсво VGUI сообщений являются двухмерными записями.
KeyValues не имеет определения типов данных или чегото подобного,
поэтому можно добавлять/удалятьзаписи любого типа. Таким образом,
отправитель и получатель должны знать внутреннюю организацию (например
имена ключей, какие они типы означают) объектов сообщения KeyValues для
успешной коммуникации. Ниже приводится общий пример использования
KeyValues:
// create a new KeyValues Object named "MyName"
KeyValues *data = new KeyValues("MyName");
// add data entries
data->SetInt( "aInteger", 42 );
data->SetString( "aString", "Marvin" );
// read data entries
int x = data->GetInt("aInteger");
Con_Printf( data->GetString("aString") );
// destroy KeyValues object again, free data records
data->deleteThis();
Для отправки сообщения можно вызвать функцию PostMessage(…) класса Panel или непосредственно ivgui()-> PostMessage(…). Имя объекта KeyValues является также именем сообщения для дальнейшей отправки. VGUI вызывает функцию OnMessage(…)
панели-получаетля, которая отправляет сообщение предыдущему
определенному дескриптору сообщения. Панель может зарегистрировть новые
дескрипторы сообщений с помощью одного из MESSAGE_FUNC_* макросов, который добавляет дескриптор функции в список сообщений. Нельзя переопределять функцию OnMessage(…) для обработки новых сообщений, для этого всегда используется макрос.
Для начала объявите дескриптор сообщения для панели-получателя:
class MyParentPanel : public vgui::Panel
{
...
private:
MESSAGE_FUNC_PARAMS( OnMyMessage, "MyMessage", data );
}
void MyParentPanel::OnMyMessage (KeyValues *data)
{
const char *text = data->GetString("text");
}
Панель-отправитель создает объект KeyValues, добавляет параметры
сообщения и отправляет сообщение (в данном случае ее родителю). VGUI
уничтожает объект KeyValues после обработки.
void MyChildPanel::SomethingHappend()
{
if ( GetVParent() )
{
KeyValues *msg = new KeyValues("MyMessage");
msg->SetString("text", "Something happend");
PostMessage(GetVParent(), msg);
}
}
Используя PostMessage() отправляющая
панель должна адресовать одного определенного получателя, что означает
что все друге панели заинтересованные в изменении состояния должны быть
известны и адресованы отдельно. Для избежания изнурительного
программирования этих зависимостей, панели имеют публичную систему
сообщений называемую сигналы действий. Панель генерирует общие события с
помощью PostActionSignal(KeyValues *message) и интересуемые панели могут регистрироваться как listener-ы этих сигналов с помощью AddActionSignalTarget(Panel *target). Данные сигналы действий широко используются средствами VGUI, например сообщения типа "TextChanged" генерируются классом TextEntry или "ItemSelected"
используется классом ListPanel. Все сигналы действий содержат запись с
указателем "panel" который указывает на панель-отправитель.
Пример использования данных сигналов потребует панели-родителя для
регистрации как listene-ра, желательно в конструкторе после создания
дочерней панели. Дочерняя панель использует PostActionSignal() вместо PostMessage():
MyParentPanel::MyParentPanel()
{
...
m_pChildPanel->AddActionSignalTarget( this );
}
void MyParentPanel::OnMyMessage (KeyValues *data)
{
const char *text = data->GetString("text");
Panel *pSender = (Panel *) kv->GetPtr("panel", NULL);
}
void MyChildPanel::SomethingHappend()
{
KeyValues *msg = new KeyValues("MyMessage");
msg->SetString("text", "Something happend");
PostActionSignal ( msg );
}
Часто используемым сигналом является сообщение "Command", для
которого не требуется дескрипторов сообщений. Панели должны порождать
виртуальные функции OnCommand(const char *command)
и проверять правильную строку команды. Сообщение "Command" используется
всеми классами Button и генерируется каждый раз во время нажатия
клавиши. Ниже приводится пример использования сообщения Command:
class MyParentPanel : public vgui::Panel
{
...
protected:
virtual void OnCommand(const char *command);
}
MyParentPanel::MyParentPanel()
{
...
m_pChildPanel->AddActionSignalTarget( this );
}
void MyParentPanel::OnCommand(const char *command)
{
if (!stricmp(command, "SomethingHappend "))
{
DoSomething ();
}
else
{
BaseClass::OnCommand(command);
}
}
void MyChildPanel::SomethingHappend()
{
KeyValues *msg = new KeyValues("Command");
msg->SetString("command", "SomethingHappend");
PostActionSignal ( msg );
}
Схемы и режим редактировния (Build-Mode)
VGUI схемы описывают внешний вид панелей указывая используемые
цвета, шрифты и иконки элементов. Схемы описываются в ресурсных файлах,
например hl2resourceclientscheme.res. Новые панели наследуют по
умолчанию схему и установки их родителя. Менеджер схем VGUI может
загружатьновые схемы с помощью LoadSchemeFromFile(char *fileName, char *tag) возвращая дескриптор HScheme. Для использования загруженной схемы VGUI вызывает функциию панели SetScheme(HScheme scheme).
Схемы устанавливают внешний вид элементов панелей но не устанавливают
определенных элементов управления. Единственный путь добавлять елементы
панелей - с помощью кода. Вы можете создавать их (обычно в конструкторе
родительской панели) и установить свойства такие как размер и позиция
непосредственно используемые в функциях панелей. Все это дает полный
комплекс и экономию времени для длинных диалогов с большим количеством
управляющих елементов.
Наиболее удобный способ описать формат элементов панелей - это
описать все елементы в внешнем ресурсном файле на подобии
"hl2resourceUIclassmenu.res" (текстовый файл содержащий записи
KeyValues). Когда создается родительская панель, такой файл загружается и
выполняется с помощью LoadControlSettings(char *dialogResourceName). В этом ресурсном файле каждый управляющий элемент описан в отдельной секции. Типичное описание управления описано так:
"MyControlName"
{
"ControlName" "Label" // control class
"fieldName" "MyControlName" // name of the control element
"xpos" "8" // x position
"ypos" "72" // y position
"wide" "160" // width in pixels
"tall" "24" // height in pixels
"visible" "1" // it's visible
"enabled" "1" // and enabled
"labelText" "Hello world" // show this text
"textAlignment" "west" // right text alignment
}
Каждое управляющее свойство имеет ключевое имя и значение. Свойства
описаны в базовом классе Panel доступны для всех элементов (например
xpos, ypos, wide, tall и т.д.). Список все доступных имен ключей и
значений панели возвращаются функцией GetDescription().
Порождаемые классы могут добавлять их специфические свойства. Обработка
этих новых полей должна реализоваться переопределенной виртуальной
функцией ApplySettings(KeyValues *inResourceData). Сдесь также можно определять как интерпретируется новые значения для существующего свойства.
Довольно простым является использование встроенного режима
редактирования VGUI который позволяет изменять и сохранять ресурсный
файл во время того как выполняется игра. Для редактирования панели,
запустите игру, откройте эту панель, так чтоб она стала в фокусе. Затем
нажмите SHIFT+CTRL+ALT+B для перехода в режим
встроенного VGUI редактора. В встроенном режиме редактирования вы можете
просто перегруппировать существующие елементы и изменить их управляющие
свойства (для обновления изменений нажмите 'Apply'). Для добавления
нового управляющего елемента, выбирете требуемый класс из комбо-бокса в
нижней правой стороне и пустой объект управления данного класса будет
помещен на панель для дальнейшего редактирования. Для сохранения
текущего форматирования в ассоциированном ресурсном файле нажмите кнопку
'Save' (удостовертесь что ресурсный файл не защищен от записи).
Рисование и Поверхности
Используя схемы и свойства управления вы можете менять основное
расположение и форматировние существующих средств управления, но это это
не позволяет создавать полностью новые элементы. Для изменения места
положения панели требуется переопределить две виртуальные функции Panel::Paint() и Panel::PaintBackground().
В этих функциях вы можете использовать рисующие функции
предоставляющиеся интерфейсом ISurface для размещения линий,
прямоугольников, текста, рисунков и т.д. Во время обновления экрана,
VGUI вначале вызывает PaintBackground() затем Paint()
для каждой панели и ее дочерних панелей рекурсивно. Координаты
отрисовки отностительны к той панели в которой оно производится. Ниже
приводится пример кода который рисует красный прямоугольник в верхнем
левом углу:
void MyPanel::Paint(void)
{
BaseClass::Paint();
surface()->DrawSetColor( 255, 0, 0, 255 ); //RGBA
surface()->DrawFilledRect( 0, 0, 20, 20 ); //x0,y0,x1,y1
}
Для отрисовки текста на поверхности панели сначала понадобится
установить используемый шрифт. Шрифти имеют названия похожие на
"DefaultSmall" и свойства на подобии True-Type-шрифтов, размер и толщина
определяются в ресурсном файле схемы. Дескриптор шрифта может быть
возвращен вызовом GetFont("Name") текущей
схемы панели, затем поверхность может быть указана к использованию в
качестве текущего шрифта. После установки цвета и положения для
следующего текстового вывода, сам текст должен быть передан DrawPrintText(...)
как строка в расширенной кодировке (Unicode) . Этот текст не будет
выведен так как есть и никак не локализируется, поэтому
последовательности локализации должны производиться вручную.
void MyPanel::Paint(void)
{
wchar_t *pText = L"Hello world!"; // wide char text
// get the right font handle for this scheme
vgui::IScheme *pScheme = vgui::scheme()->GetIScheme(GetScheme());
vgui::HFont hFont = pScheme->GetFont( "DefaultSmall" );
surface()->DrawSetTextFont( hFont ); // set the font
surface()->DrawSetTextColor( 255, 0, 0, 255 ); // full red
surface()->DrawSetTextPos( 10, 10 ); // x,y position
surface()->DrawPrintText( pText, wcslen(pText) ); // print text
}
Для отрисовки текстуры или картинки, VGUI требуется предварительно
загрузить текстуру с диска (в конструкторе панели) и сгенерировать a
соответствующий текстурный ID. Этот ID затем используется как ссылка
когда отрисовывается тестура. Ниже приводится пример загрузки и
отрисовки текстуры "resourcesvguimylogo.vmt":
MyPanel::MyPanel(void)
{
m_nTextureID = vgui::surface()->CreateNewTextureID();
gui::surface()->DrawSetTextureFile( m_nTextureID, "vgui/mylogo" ,
true, false);
}
void MyPanel::Paint(void)
{
vgui::surface()->DrawSetTexture( m_nTextureId );
vgui::surface()->DrawTexturedRect( 0, 0, 100, 100 );
}
Локализация
Текстовые элементы VGUI поддерживают автоматическую локализацию для
предпочтительного для пользователя языка. Предположим, если текстовый
ярлык содержит "Hello world!" значит мы можем вывести этот текст
напрямую функцией SetText( "Hello World.").
Но если пользователь изменит на другой язык кроме английского этот
текст всеравно отсанется текстом английского языка. Поэтому всегда нужно
использовать локализированные последовательности чтоб сообщить VGUI о
переводе последовательности в родной текст пользователя, в этом примере SetText( "#MyMod_HelloWorld"). Последовательность является строкой начинающейся с знака весовой единицы '#' как управляющего символа для того чтобы сообщить VGUI что это не просто обычный текст.
VGUI хранит глобальную таблицу трансляции для связывания
последовательностей с текстовыми представлениями. Эти таблицы трансляции
загружаются из ресурсных файлов, где каждый из файлов содержит
специальную копию для каждого поддерживаемого языка (например resourcehl2_english.txt, resourcehl2_german.txt). Новое описание последовательности для вашей игры может выглядеть следующим образом:
В mymod_english.txt:
"MyMod_HelloWorld" "Hello world."
"[english] MyMod_HelloWorld " "Hello world."
В mymod_german.txt:
"MyMod_HelloWorld" "Hallo Welt."
"[english] MyMod_HelloWorld " "Hello world."
Если папка вашего мода называется "mymod" движок Source автоматически загружает корректный файл трансляции (/resource/gamedir_language.txt). Вы также загружать дополнительные файлы трансляции используя функцию AddFile(...) интерфейса ILocalize.
Вы также можете использовать интерфейс ILocalize для ручного перевода последовательности в текущий язык пользователя, например. vgui::localize()->Find("#MyMod_HelloWorld"). Эта функция возвращает перевод в виде 16-битной расширенной строки (Unicode).
VGUI использует Unicode для всех текстовых представлений для
поддержки языков которые используют больше чем 255 ASCII символов такие
как Китайский или Русский. Языковые ресурсные файлы кодируются в UTF-8,
что позволяет их довольно легко редактировать так как 7-Bit ASCII
является валидным подмножество UTF-8. Для перевода строк из ANSI ASCII в
Unicode во время выполнения программы можно использовать функции
интерфейса ILocalize: ConvertANSIToUnicode(…) и ConvertUnicodeToANSI(…). Также очень важной функцией является ConstructString(…) которая в основном похожа на sprintf(…) для Unicode строк.