Документация VGUI

From Valve Developer Community
Jump to: navigation, search
English (en)español (es)polski (pl)русский (ru)中文 (zh)
... Icon-Important.png

VGUI - это собственный графический интерфейс пользователя Valve. Все приложения на Source и Steam используют VGUI для рисования окон, диалогов и меню. Это также обрабатывает локализацию: отображение текста на предпочитаемом пользователем языке.

  • Архитектура объекта является иерархической, и все реализованные элементы являются производными от базовых классов VGUI.
  • Система ввода с клавиатуры/мыши основана на событиях и очень похожа на другие системы с графическим интерфейсом.
  • Реализации для наиболее распространённых элементов графического интерфейса, таких как кнопки, текстовые поля или изображения, предоставляются библиотекой элементов управления VGUI (vgui_controls.lib).
  • Заголовки базового интерфейса расположены в \public\vgui\
  • Элементы управления определены в \public\vgui_controls\.

Как автор мода вы, скорее всего, будете использовать VGUI в проекте client.dll для отображения меню, элементов HUD или для отображения в игре (на оружии или компьютерных терминалах и т.д.).

Панели и иерархия

Базовый класс, из которого происходят все элементы VGUI: vgui::Panel, который определяет область на вашем экране, который имеет определенный размер и положение, может рисовать себя и обрабатывать входные события. Диалоговые окна, текстовые поля и кнопки являются панелями VGUI в иерархических отношениях родитель-потомок. Самая первая панель - это корневая панель, предоставляемая движком Source. Корневая панель клиента покрывает весь экран, но ничего не показывает. Даже если вы можете использовать корневую панель клиента, большинство панелей клиента используют общую панель BaseViewport в качестве родительской ( g_pClientMode->GetViewport() ).

На этой диаграмме показана иерархия панелей VGUI в Counter-Strike:

Иерархия клиентских панелей VGUI

Вы можете проверить иерархию панелей вашего мода с помощью Инструмент иерархии VGUI. Откройте его, отправив в консоль разработчика команду vgui_drawtree 1.

Инструмент иерархии VGUI

Названия панелей имеют цветовую кодировку:

  • Видимое
  • Скрытое
  • Всплывающая панель (Frame)
  • Имеет фокус

И следующие опции доступны как флажки:

Показать видимое
Список всех видимых панелей
Показать скрытое
Список всех скрытых панелей
Только всплывающие
Список только всплывающих панелей (Frames)
Выделить мышью
При наведении курсора мыши на панели выделяется цветная рамка. Дерево панели будет развернуто, чтобы показать текущую панель.
Заморозить
Блокирует текущий вид дерева
Показать адреса
Показывает адрес памяти панели
Показать Альфа
Показывает Альфа значение панели, 0 = полупрозрачный, 255 = непрозрачный
В порядке рендеринга
Сортировка панелей в дереве по порядку рендеринга

Класс Panel

VGUI класс Panel является базовым классом для более чем 50 различных элементовв управления, они определены в \public\vgui_controls\. Заголовочные файлы обычно предоставляют хорошую документацию о возможностях и поведении элементов. Следующая диаграмма показывает иерархию классов для некоторых самых часто используемых элементов:

Panel
EditablePanel Label TextEntry RichText ImagePanel
Frame Button ComboBox
MessageBox ToggleButton

Поскольку все 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.res");
pFrame->SetSize( 100, 100 );
pFrame->SetTitle("My First Frame", true );
pFrame->Activate();	// установить видимость, переместить на передний план, запросить фокус

Если изменяются размеры фрейма, происходит вызов PerformLayout() так что фрейм может реорганизовать расположение элементов для более удобного размещения. Текущее положение и размер фрейма также можно зафиксировать с помощью SetMoveable(bool state) и SetSizeable(bool state).

Сообщения о событиях

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

Имя и содержание сообщения указывается объект KeyValues (\public\KeyValues.h). Класс KeyValues имеет очень общую и гибкую структуру для хранения записей данных содержащих строки, целые и вещественные числа. Объект KeyValues содержит имя и набор записей с данными. Каждая запись имеет уникальное ключевое имя и соответствующее значение. С помощью KeyValues также создаются иерархические структуры, использующие вложенные ключи, но в данном случае, большинсво VGUI сообщений являются двухмерными записями. KeyValues не имеет определений данных/типов или чего-то подобного, поэтому можно добавлять или удалять записи любого типа как вам нравиться. Таким образом, отправитель и получатель должны знать внутреннюю организацию (например имена ключей, типы и их значения) объекта сообщений KeyValues для успешной коммуникации. Ниже приводится общий пример использования KeyValues:

// создаем новый объект KeyValues называемый "MyName"
KeyValues *data = new KeyValues("MyName"); 

// добавляет записи данных
data->SetInt( "anInteger", 42 );
data->SetString( "aString", "Marvin" );

// читает записи данных
int x = data->GetInt("anInteger");
Con_Printf( data->GetString("aString") );

// удаляет повторно объект KeyValues, освобождает данные записей
data->deleteThis();

Для отправки сообщения можно вызвать функцию PostMessage(…) класса Panel или непосредственно ivgui()-> PostMessage(…). Имя объекта KeyValues является также именем сообщения для дальнейшей отправки. VGUI вызывает функцию OnMessage(…) панели-получаетля, которая отправляет сообщение предыдущему определенному дескриптору сообщения. Панель может зарегистрировть новые дескрипторы сообщений с помощью одного из MESSAGE_FUNC_* макросов, который добавляет дескриптор функции в список сообщений. Нельзя переопределять функцию OnMessage(…) для обработки новых сообщений, для этого всегда используется макрос. Пожалуйста заметьте когда вы используете макрос MESSAGE_FUNC_* только указанные сообщения вызывают вашу функцию. Если вы хотите перехватить сообщение newLine (ENTER) из элемента txtEntry вы будете использовать MESSAGE_FUNC_PARAMS( NewLineMessage, "TextNewLine",data). NewLineMessage - это вызов функции, "TextNewLine" означает все сообщения у которых имя "TextNewLine" будут перенаправлены и конечные данные это ввод будут переданы вашей функции. В этом случае NewLineMessage будет выглядеть как void MyParentPanel::NewLineMessage (KeyValues *data).

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 );
}

Чтобы перехватить клавишу enter из элемента txtEntry используйте это.

class MyParentPanel  : public vgui::Panel
{
	...
private:
	MESSAGE_FUNC_PARAMS( OnMyMessage, "NewLineMessage", data );
}
MyParentPanel::MyParentPanel()
{
	...
	m_pChildPanel = new vgui::TextEntry( this, "txtEntry" );
	m_pChildPanel->AddActionSignalTarget( this );
	m_pChildPanel->SendNewLine(true); // с типом txtEntry вы должны установить это чтобы передавать клавишу enter как сообщение
}

void MyParentPanel::NewLineMessage (KeyValues *data)
{
	// когда txtEntry box отправляет сигнал действия это только устанавливает имя
	// поэто лучше проверить сфокусировано ли указанная txtEntry 
	// Text Entry сфокусирована?
	if (m_pChildPanel->HasFocus())
	{
	// Мы перехватили сообщение, теперь мы можем его обработать. Мы просто переотправляем в функцию OnCommand
	// Отправляем функцию нашему обработчику команды
		this->OnCommand("Submit");
	}// End HasFocus()
}

// Dawid Joubert 2006-02-11

Часто используемым сигналом является сообщение "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 );
}

Схемы

VGUI схемы описывают внешний вид панелей указывая используемые цвета, шрифты и иконки элементов управления. Схемы описываются в ресурсных файлах, например hl2\resource\clientscheme.res. Новые панели наследуют по умолчанию схему и установки их родителя. Менеджер схем VGUI может загружать новые схемы с помощью LoadSchemeFromFile(char *fileName, char *tag) которая возвращает дескриптор HScheme. Для использования загруженной схемы VGUI вызывает функцию панели SetScheme(HScheme scheme).

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

Наиболее удобный способ описать формат элементов панелей - это описать все елементы в внешнем ресурсном файле на подобии hl2\resource\UI\classmenu.res (текстовый файл содержащий записи KeyValues). Когда создается родительская панель, такой файл загружается и выполняется с помощью LoadControlSettings(char *dialogResourceName). Однако, эта фуекция только определена для EditablePanel (или для наследников EditablePanel таких как Frame). В этом ресурсном файле каждый управляющий элемент описан в отдельной секции. Типичное описание управления описано так:

"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

Довольно простым является редактирование панели и расположения элементов в 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") текущей схемы панели, затем поверхность может быть указана к использованию в качестве текущего шрифта. После установки цвета и положения для следующего текстового вывода, сам текст должен быть передан как строка в расширенной кодировке (Unicode) в DrawPrintText(...). Этот текст не будет выведен так как есть и никак не локализируется, поэтому последовательности локализации должны производиться вручную.

void MyPanel::Paint(void)
{
	wchar_t *pText = L"Hello world!";	// wide char text

	// получаем правильный дескриптор для этой схемы
	vgui::IScheme *pScheme = vgui::scheme()->GetIScheme(GetScheme());
	vgui::HFont hFont = pScheme->GetFont( "DefaultSmall" );

	surface()->DrawSetTextFont( hFont ); // устанавливаем шрифт
	surface()->DrawSetTextColor( 255, 0, 0, 255 ); // красный
	surface()->DrawSetTextPos( 10, 10 ); // позиция x,y
	surface()->DrawPrintText( pText, wcslen(pText) ); // печатаем текст
}

Для отрисовки текстуры или картинки, VGUI требуется предварительно загрузить текстуру с диска (в конструкторе панели) и сгенерировать соответствующий текстурный ID. Этот ID затем используется как ссылка когда отрисовывается тестура. Ниже приводится пример загрузки и отрисовки текстуры "\materials\your_folder\mylogo.vmt":

MyPanel::MyPanel(void)
{
	//Do _not_ forget CreateNewTextureID(), it gave me a headache for a week
	m_nTextureID = vgui::surface()->CreateNewTextureID();
	vgui::surface()->DrawSetTextureFile( m_nTextureID, "materials/your_folder/mylogo" , true, false);
}

void MyPanel::Paint(void)
{
	vgui::surface()->DrawSetTexture( m_nTextureID );
	vgui::surface()->DrawTexturedRect( 0, 0, 100, 100 );
}

Пропорциональность

Значения кординат которые вы указываете в конфигурационном файле относительны разрешения 640x480, если SetProportional(true) вызван до загрузки файла. Эти значения будут масштабироваться к правильному значению без разницы какое разрешение может использоваться. Для примера, значение "tall" 120 будет всегда означать что панель занимает 1/4 высоты экрана, и значение "x" 320 означает что панель будет всегда по центру.

Тем неменее, иногда вам нужно высчитать кординаты для рисования и к сожалению отказаться от конфигурационных файлов. Проблема в том что значения которые вы указываете в коде относительны родного пользовательского расширения, которые моугут совпадать или нет с текущим игровым разрешением (поэтому surface()->GetScreenSize() бесполезно). Решение проблемы в использование scheme()->GetProportionalScaledValue(). Это позволяет вам указывать координаты в разрешение 640x480 и они будут пересчитаны к текущему разрешению.

Локализация

Текстовые элементы VGUI поддерживают автоматическую локализацию для предпочтительного для пользователя языка. Предположим, если текстовая метка содержит "Hello world!" значит мы можем вывести этот текст напрямую функцией SetText( "Hello World."). Но если пользователь изменит на другой язык кроме английского этот текст всеравно отсанется текстом английского языка. Поэтому всегда нужно использовать локализированные последовательности чтоб сообщить VGUI о переводе последовательности в родной язык пользователя, в этом примере SetText( "#MyMod_HelloWorld") Последовательность является строкой начинающейся с знака весовой единицы '#' как управляющего символа для того чтобы сообщить VGUI что это не просто обычный текст.

VGUI хранит глобальную таблицу перевода для связывания последовательностей с текстовым представлением. Эти таблицы перевода загружаются из ресурсных файлов, где каждый из файлов содержит специальную копию для каждого поддерживаемого языка (например \resource\hl2_english.txt, \resource\hl2_russian.txt). Новое описание последовательности для вашей игры может выглядеть следующим образом:

В mymod_english.txt:

	"MyMod_HelloWorld"			"Hello world."

В mymod_russian.txt:

	"MyMod_HelloWorld"			"Здраствуй мир."
	"[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 символов такие как Китайский или Русский. Языковые ресурсные файлы кодируются в Unicode. Для перевода строк между ANSI ASCII и Unicode во время выполнения программы можно использовать функции интерфейса ILocalize ConvertANSIToUnicode(…) and ConvertUnicodeToANSI(…). Также очень важной функцией является ConstructString(…) которая в основном похожа на sprintf(…) для Unicode строк.

Смотрите также

Уроки