Shambler team
Статьи по программированию под движком Gold Source и работе с SDK игры Half-Life

VGUI — концепция интерфейса и структура его компонентов


Оглавление:
1) Введение
2) Первое меню
3) Шрифты и вывод текста
4) Работа с изображениями
5) Загрузка текста из внешних файлов
6) Элементы управления
    - Фон и граница панели
    - Текстовые поля
    - Кнопки
    - Картинки
7) Обработка сигналов мыши и клавиатуры
8) Заключение

Введение

Обычно люди, не очень хорошо знакомые со VGUI (Versatile graphic user interface — изменяемый графический пользовательский интерфейс), при попытке что-нибудь в нём сконструировать или просто попытаться понять, как эта махина работает, испытывают мягко говоря неслабое чувство растерянности. Ещё бы, ведь стоит только залезть в эту кашу, творяющуюся в коде любой более-менее приличной менюшки, так тут же запутаешься во всех этих панелях, сигналах, хандлерах, и ещё бог знает в чём. И это притом, что от Вальве вообще нет никакой документации по VGUI. Конечно, всегда можно пойти на ХЛпрограмминг, или ещё куда-нибудь, и там скачать несколько руководств страниц по пять, и что-нибудь попытаться из них выжать. Но руководства, начинающиеся словами "Откройте файл TeamFortressViewport.h, и на 32-ой линии добавьте то-то и то-то", хоть и покажут вам, как скопировать TeamMenu и переименовать его в MyFirstVguiMenu, но не дадут каких-либо знаний о том, как оно всё работает. Поэтому я сначала лучше попытаюсь обрисовать целостную картину принципов работы VGUI, а потом мы уже перейдём к рисованиям своих панелек.

Итак, что может быть целостнее, чем взгляд с высоты птичьего полёта? Давайте посмотрим что ли на сводную таблицу всех классов VGUI с целью выяснить, кто тут главный и вообще.

Квадратиками я обозначил классы, которые определены в самой клиентской библиотеке, и исходники которых можно посмотреть. Как правило, это либо какие-то специфичные VGUI-панели, либо заточенные под них элементы управления. Классы, которые без квадратиков — это те инструменты, которые входят в комплект библиотеки VGUI. Большинство из них многофункциональны, и их можно использовать либо в своих панелях, либо в качестве основы для создания своих собственных элементов управления. Кстати, их нельзя увидеть в окне ClassView — их описания расположены в файлах из папки utils\vgui\include.

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


Конечно, чтобы создавать свои VGUI-панели, во всей этой конструкции разбираться вовсе не обязательно. К примеру, можно особо не брать в расчёт классы клиентской библиотеки (те, что с квадратиками) — ну кроме TeamFotressViewport, конечно, но его мы потом рассмотрим подробнее. Насчет классов из библиотеки VGUI — я расскажу только о самых часто используемых, таких как кнопки, поля с текстом, картинки, и т.п. Ведь после того, как вы более-менее здесь сориентируетесь, вам не составит никакого труда самостоятельно пройтись по остальным классам, и посмотреть, чего они предлагают интересного.

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

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

Панели всегда ограничены квадратной областью, внутри которой они и подчиненные им элементы могут выводить графику и получать сигналы от мыши. Ну в принципе, это понятно интуитивно — если кнопка, к примеру, вылезла за пределы панели, которой она подчинена, то она обрежется по её границе. Граница панели не обязательно должна быть видимой, равно как и её фон.

Вся графика, которую может выводить панель, делится на четыре категории — прямоугольник, рамка, текст, и растровая картинка. Линии можно изобразить с помощью сжатого прямоугольника. (Кстати, хоть и рисование прямоугольной области во VGUI похоже на FillRGBA, но у них есть одно важное отличие — во VGUI можно нарисовать тёмную область, в то время как FillRGBA принимает их за прозрачные).

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

На каждый класс, предоставляемый библиотекой VGUI, в папке utils\vgui\include существует по одному заголовочному файлу, что, кстати, довольно удобно — имена файлов соответствуют именам классов с приставкой "VGUI_". Следуя этой логике, нтересующим нас файлом является "VGUI_Panel.h". Ну что, открыли? Так-с.. Да, знаю, большой он, этот класс. Ну ничего, нам целиком не нужно, мы только по основному пройдемся.

Итак, первое, что мы видим в классе Panel — это конструктор:

Panel(int x,int y,int wide,int tall);

Хоть и напрямую объекты класса Panel почти никогда не создаются (в основном от него все наследуются), однако его конструктор играет очень важную роль — в него передаются координаты верхнего левого угла панели, её ширина и высота. Каждый класс, который наследуется от Panel, обязан из своего конструктора вызвать конструктор своего родителя, передав желаемые размеры своей панели (Как уже говорилось выше, в этой области панель может выводить графику и получать сведения об активности мыши). Исходников библиотеки VGUI нету ни у кого, кроме Вальвы, однако можно предположить, что именно через конструктор класса Panel происходит связь созданного объекта со всей остальной системой VGUI.

Дальше расположены всякие функции для изменения положения панели на экране и её размеров:

void setPos(int x,int y);
void getPos(int& x,int& y);
void setSize(int wide,int tall);
void getSize(int& wide,int& tall);
void setBounds(int x,int y,int wide,int tall);
void getBounds(int& x,int& y,int& wide,int& tall);
int getWide();

int getTall();

С ними и так всё понятно. А вот на этой стоит задержаться -

void setVisible(bool state);

Многие из VGUI-панелей создаются при загрузке игры и существуют на всем её протяжении (например, ScorePanel — таблица очков). Такие меню не удаляются во время игры, а лишь "скрываются". Вот как раз для включения и выключения отображения такого рода панелей и существует эта функция.

Следующей, безусловной интересной для нас функцией, является

void setParent(Panel* newParent);

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

В классе Panel ещё две функции, связывающие панели друг с другом — addChild и removeChild. Первая делает то же самое, что и setParent, но она вызывается у главной панели, и в неё передается указатель на подчиненную. Вторая, соответственно, удаляет подчиненную панель — при этом вызывается деструктор той панели — вобщем, всё как положено.

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

void addInputSignal(InputSignal* s);

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

Кстати, о рисовании — а вот, собственно, и оно — видите группу функций, начинающихся на draw? Это и есть наш "инструментарий художника". А чуть ниже есть ещё такие две функции:

void paintBackground();
void paint();

Они предназначены для того, чтобы наследуемый класс при необходимости переопределил их у себя, и из них производил все операции рисования. Сначала вызывается paintBackground, а потом paint. Функции рисования подчиненных панелей вызываются автоматически, после того, как будут вызваны функции главной.

Ну что-ж, пора бы нам уже действительно что-нибудь нарисовать. Однако кроме создания класса нашей панели, нам также надо где-то разместить создание объекта этого класса. Вот тут, пожалуй, стоит немного поговорить о классе TeamFortressViewport.

Если посмотреть по таблице классов, то видно, что TeamFortressViewport — это тоже панель. Существует только один объект этого класса, к которому можно получить доступ через глобальный указатель gViewPort. Основное назначение панели TeamFortressViewport — это быть parent'ом для всех остальных панелей. Таким образом, каждая созданная панель кому-то подчинена — панели меню подчинены TeamFortressViewport'у, а им подчинены их элементы управления — кнопки, ползунки, и т.д. Может возникнуть такой вопрос — а кому подчинен TeamFortressViewport? Ответ на этот вопрос находится там же, где и создание самого объекта gViewPort (в функции VGui_Startup) — он подчинен некой панели, которая предоставляется движком. Подчинена ли кому-нибудь она — это уже вопрос из области философии :).

В обязанности класса TeamFortressViewport также входит создание и инициализация остальных панелей меню. Помимо этого, он является свалкой для разнообразного рода вещей, тем или иным боком относящимся к VGUI. Например, он принимает мессаджи от сервера, обрабатывает нажатия кнопок на клавиатуре, показывает и убирает мышиный курсор, и т.д.

Что-ж, теперь у нас достаточно теоретических сведений для того, чтобы состряпать наше первое самопальное меню.



Первое меню

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

Но прежде чем мы начнем что-либо рисовать, надо немного поговорить о координатах во VGUI. У VGUI-менюшек есть плюс в том, что в разных разрешениях они выглядят более-менее одинаково. О размере шрифта нам, как правило, заботиться не приходится — VGUI автоматически загружает шрифты нужного размера для каждого разрешения. Но чтобы размеры наших рамочек, отступов, кнопок, и т.д. всегда оставались одинаковыми относительно размеров экрана, надо учесть одно простое правило. Представьте, что вы рисуете в разрешении экрана 640х480, и все константные значения координат пропускайте через макросы XRES и YRES — первый для горизонтальных координат, а второй, соответственно, для вертикальных. Эти макросы автоматически будут преобразовывать координаты так, чтобы они соответствовали текущему разрешению. К примеру, точка XRES(320), YRES(240) в любом разрешении будет центром экрана.

Где размещать класс панели — дело сугубо вашевское. Обычно для него создают новый cpp-файл, где пишут тело функций класса, а объявление выносят в заголовок. Впрочем, если хотите, то можете разместить всё в одном лишь заголовке.

#ifndef _MYPANEL_H
#define _MYPANEL_H
using namespace vgui;

class CMyPanel : public Panel
{
public:
    CMyPanel(); // конструктор
    virtual void paint(); // функция отрисовки
};

#endif

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

#include "hud.h"
#include "cl_util.h"
#include "vgui_TeamFortressViewport.h"
#include "vgui_MyPanel.h"

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

Как вы помните, конструктор любой панели обязан вызывать констуктор класса Panel. Выглядеть конструктор нашей панели будет так:

CMyPanel::CMyPanel() : Panel(XRES(100), YRES(100), XRES(200), YRES(150))
{
    setPaintBackgroundEnabled(false); // отключить вызов paintBackground
}

Здесь верхний левый угол нашей панели будет располагаться в точке {100, 100}, и она будет иметь ширину и высоту, равные 200 и 150 пикселей соответственно.

setPaintBackgroundEnabled — это функция из класса Panel, которая позволяет включать и выключать вызов фукнции paintBackground(). Хоть и в нашем классе её нет, но отключить её вызов всё равно надо, так как у класса Panel есть версия этой функции по умолчанию, которая просто закрашивает всю панель определенным цветом. (Этот цвет можно задать вызовом функции setBgColor). Это иногда может быть удобным, но здесь мы условились, что нарисуем всё сами :). Кстати, есть ещё и похожая функция setPaintEnabled, которой можно отключать вызов paint().

Итак, давайте рисовать. Для пробы пера созданим элементарную функцию paint, которая просто закрасит панель тёмно-синим цветом.

void CMyPanel::paint()
{
    drawSetColor(40, 40, 200, 100); // r, g, b, alpha
    drawFilledRect(0, 0, getWide(), getTall());
}

Как видите, для рисования прямоугольника нужно вызвать две функции класса Panel — установка цвета, и собственно, рисование прямоугольника. Функции getWide и getTall тоже являются частью класса Panel, и возвращают, как вы уже наверное догадались, ширину и высоту панели. Кстати, имейте ввиду, что при рисовании прямоугольников во VGUI, третьим и четвертым параметром вы задаете не высоту и шириру прямоугольника, как это было в FIllRGBA, а координаты правой нижней точки. То есть, команда drawFilledRect(50, 50, 70, 70) нарисует квадрат с длиной стороны 20 пикселей. Также не стоит забывать, что система координат для рисования относительна положения текущей панели. И ещё один момент — чем меньше альфа, тем меньше прозрачность, а не наоборот.

Собственно, класс панели уже готов — осталось только подключить заголовок куда надо и создать объект класса.

В первую очередь, где нибудь в классе TeamFortressViewport надо создать переменную-указатель, которая будет указывать на наш объект, и через которую к нему можно будет обратиться. Откройте описание класса (во vgui_TeamFortressViewport.h), и там в конце найдите коммент, гласящий "VGUI Menus". Вот после него и добавляем:

    CMyPanel *m_pMyPanel;

Чтобы компилятор не кидался в нас "undeclared identifier"ом, надо в начале этого файла добавить "короткое" определение нашего класса. Найдите там ряд таких определений для уже существующих панелей, который выглядит примерно так:

...
class Cursor;
class ScorePanel;
class SpectatorPanel;
class CCommandMenu;
class CommandLabel;
... и т.д.

и втисните туда упоминание нашего класса:

class CMyPanel;

Отлично, теперь шуруем в конструктор класса TeamFortressViewport, где мы, собственно, и создадим объект нашей панели. Пролистайте куда-нибудь в конец конструктора, где будут вызовы всяких "CreateTeamMenu", "CreateClassMenu", и там добавьте:

    m_pMyPanel = new CMyPanel();
    m_pMyPanel->setParent( this ); // подчинен TeamFortressViewport'у
    m_pMyPanel->setVisible( true ); // сразу включить

Чтобы компилятор знал, что это за класс такой, надо где-нибудь в начале этого файла (vgui_TeamFortressViewport.cpp) добавить включение нашего заголовка. Желательно добавить его после всех остальных.

#include "vgui_MyPanel.h"

Ура, наше первое меню создано!

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

У вас, наверное, возникает вполне закономерное желание как-нибудь разнообразить эту панель. Можно с помощью рамочки сделать бордюр, и добавить ещё каких-нибудь прямоугольников. А можно и написать туда какой-нибудь текст. Рисование текста — не очень сложная задача, которая, однако, требует понимания того, как VGUI работает со шрифтами. Поэтому давайте пока что отложим в сторону рисование, и попробуем разобраться в шрифтах.



Шрифты и вывод текста

Если внимательно присмотреться к таблице классов VGUI, то можно где-то в начале обнаружить такой неприметный с виду класс Font. Этот класс обеспечивает загрузку и хранение шрифта. Загружать шрифт он может либо из tga-файла (по типу тех, что находятся в папке gfx\vgui\fonts), либо из набора виндовских шрифтов, установленных в системе. Во втором случае, для шрифта можно указать произвольную высоту и ширину символов, а также специальные параметры, типа "наклонный" и "перечеркнутый".

Одного взгляда на объявление конструктора класса Font достаточно, чтобы понять, что это довольно простая для использования система — вы передаете нужные параметры, и он делает всю работу. Однако нам, скорее всего, не придется самостоятельно загружать шрифты — в клиентской библиотеке есть некий класс по имени CSchemeManager, который может сделать эту работу за нас, а также проследить за корректным удалением шрифта. Подобно классам CHud и TeamFortressViewport, класс CSchemeManager тоже является синглетоном — всегда существует только один его экземпляр, который можно получить через gViewPort->GetSchemeManager().

Откуда CSchemeManager знает, какие шрифты надо загружать? Ответ на этот вопрос находится в файликах *_textscheme.txt, расположеных в папке valve или в папках других модов. Для каждого разрешения существует такой текстовый файлик, который описывает все существующие текстовые схемы и соответствующие им шрифты. Тут надо подробнее остановиться на разделении понятий "текстовая схема" и "шрифт". Каждой текстовой схеме проставлен в соответствие свой шрифт, но помимо шрифта она также может хранить набор настроек цвета для этой схемы, и набор курсоров мыши.

Формат файлов схем довольно простой. Сначала идет имя описываемой схемы:

SchemeName = "Basic Text"

Затем можно указать какой-нибудь шрифт, установленный в системе. Он, и его параметры, будут использованы только в том случае, если SchemeManager не найдет соответствующего названию этой схемы tga-файла в папке gfx\vgui\fonts.

FontName = "Arial"
FontSize = 17 // высота букв
FontWeight = 0 // толщина линий (значение 700 аналогично параметру bold)

Затем по желанию можно указать дополнительные параметры цвета (конечно, только если код, выводящий этот текст на экран, их использует)

FgColor = "255 170 0 255"
FgColorArmed = "255 255 255 255" // при наведении курсора
FgColorMousedown = "255 255 255 255" // при нажатии мыши

Теперь давайте посмотрим на это дело изнутри. Если вас интересует рутина парсинга файлов со схемами, и загрузка шрифтов, то можете заглянуть в конструктор класса CSchemeManager — оно всё находится там. Кстати, если вы всё-таки надумаете отказаться от услуг менеджера схем, и решите самостоятельно загружать шрифты, то учтите, что в отличии от панелей, удаление которых происходит автоматически, объекты шрифтов надо удалять вручную.

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

// получаем указатель на менеджер схем
CSchemeManager *pSchemes = gViewPort->GetSchemeManager();
// получаем номер схемы с заданным именем
SchemeHandle_t hTitleScheme = pSchemes->getSchemeHandle( "Title Font" );
// получаем шрифт по номеру
Font *pTitleFont = pSchemes->getFont( hTitleScheme );

Вуаля! pTitleFont теперь указывает на нужный нам шрифт, и мы с ним можем делать что угодно. Мда, кстати, а что нам с ним делать-то?... Весьма своевременный вопрос — ведь теперь мы всё знаем о том, как загружать шрифты, и настало время разобраться, как всё-таки вывести на экран текст.

В поисках функций рисования текста, идём в уже знакомый нам "набор художника" — а именно, копаемся в наборе draw-функций класса Panel. И находим аж целую кучу функций для вывода текста:

void drawSetTextFont(Font* font); // установить шрифт
void drawSetTextColor(int r,int g,int b,int a); // установить цвет
void drawSetTextPos(int x,int y); // установить координаты
void drawPrintText(const char* str,int strlen); // напечатать строку
void drawPrintChar(char ch); // напечатать символ

Ну как тут не разгуляться :) Срочно бежим в наш класс CMyPanel, и переписываем функцию paint так, чтобы испытать новые игрушки. Заодно побалуемся прямоугольниками и рамочками. Лично у меня получилась вот такая вот скромная панелька:


А вот и код её функции рисования:

void CMyPanel::paint()
{
// Получаем указатель на шрифт, которым мы будем выводить текст
    CSchemeManager *pSchemes = gViewPort->GetSchemeManager();
    SchemeHandle_t hTitleScheme = pSchemes->getSchemeHandle( "Title Font" );
    Font *pTitleFont = pSchemes->getFont( hTitleScheme );

// Рисуем верхнюю заливку
// Обратите внимание, что её размер основан на высоте букв шрифта!
    drawSetColor(20, 70, 30, 100);
    drawFilledRect(0, 0, getWide(), pTitleFont->getTall() + YRES(4));

// Рисуем заливку основного пространства
    drawSetColor(40, 150, 60, 150);
    drawFilledRect(0, pTitleFont->getTall() + YRES(4), getWide(), getTall() - YRES(10));

// Рисуем нижнюю заливку.
    drawSetColor(20, 70, 30, 100);
    drawFilledRect(0, getTall() - YRES(10), getWide(), getTall());

// Выводим текст
    drawSetTextFont(pTitleFont);
    drawSetTextColor(250, 250, 250, 40);
    drawSetTextPos(XRES(10), YRES(2));
    drawPrintText("bla-bla-bla", 11); // 11 - длина строки

// Рисуем рамку вокруг панели
    drawSetColor(0, 0, 0, 70);
    drawOutlinedRect(0, 0, getWide(), getTall());
}

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

Еще одной новенькой функцией здесь является drawOutlinedRect — как вы, наверное, догадались, это рисование рамки.

Конечно, вам не придется таким макаром рисовать все кнопки и прочие элементы вашего меню — во VGUI уже есть множество готовых элементов, которые сами себя отрисуют, как положено. Но тем не менее, иметь представление о рисовании "вручную" всё равно нужно — иногда это даже может оказаться более простым решением, чем копанием в уже готовых компонентах.



Работа с изображениями

Работа с картинками чем-то напоминает работу со шрифтами — существует класс Bitmap, который выполняет функции загрузки и хранения изображения. Как правило, вместо класса Bitmap используется его потомок BitmapTGA, умеющий загружать TGA-файлы. Объекты этих классов, как и шрифты, необходимо удалять. Но если для шрифтов эту работу делал CSchemeManager, то тут такого механизма нет, и поэтому нам придется в классе нашей панели создать деструктор, из которого мы будем производить удаление загруженных картинок.

У класса Font, как мы помним, был довольно удобный конструктор, в котором сразу можно было указать имя файла. Если вглянуть на конструктор класса BitmapTGA, то мы увидим, что он принимает довольно странный аргумент:

BitmapTGA(InputStream* is,bool invertAlpha);

InputStream — это очередной замут вальве, выливающийся в то, что мы ещё и должны самостоятельно открывать файл с картинкой. Впрочем, у вальве на каждый свой замут есть функции, его упрощающие :). Для упрощенной загрузки изображений предусмотрены две функции, который можно найти в файле vgui_loadtga.cpp:

BitmapTGA* vgui_LoadTGA(char const *pFilename)
BitmapTGA* vgui_LoadTGANoInvertAlpha(char const *pFilename)

Они обе должным образом создают объект BitmapTGA, и возвращают на него указатель. Разница между ними только в том, что первая задает параметр invertAlpha равным true, а вторая — false. Как вы, наверное, знаете, TGA-картинки располагают альфа-каналом, что позволяет каждой точке изображения иметь индивидуальный уровень прозрачности. Чтобы создавать такие изображения в Photoshop, надо либо воспользоваться стирательной резинкой, и в нужных местах "проскрябать" рисунок, чтобы скозь него было видно клетчатый фон, либо выделить нужные области и нажать delete. Так вот при загрузки изображения функцией vgui_LoadTGANoInvertAlpha, альфа-канал инвертируется — всё, что было невидимым, становится видимым, и наоброт.

После загрузки изображения мы можем узнать его размер, используя функцию getSize. Чтобы установить его в нужные координаты, воспользуйтесь функцией setPos. При желании, можно также задать её цвет или прозрачность через функцию setColor.

Ну вот, как загрузить картинку, мы теперь знаем, осталось только узнать, как её нарисовать. У картинки есть функция

void doPaint(Panel* panel);

которая производит рисование изображения в указанную панель. Нам просто нужно вызвать эту функцию из paint, и в качестве панели для рисования указать this.


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

class CMyPanel : public Panel
{
public:
    CMyPanel(); // конструктор
    ~CMyPanel(); // деструктор
    virtual void paint(); // функция отрисовки

    BitmapTGA *m_pImage; // указатель на картинку
};

В конструктор добавляем следующие строки:

    m_pImage = vgui_LoadTGA("gfx/vgui/640_timer.tga");

    int imgx, imgy;
    m_pImage->getSize(imgx, imgy);
    m_pImage->setPos(getWide() - imgx - XRES(5), YRES(5));

Здесь я установил картинку в верхний правый угол панели с отступом в пять пикселей от края. Для этого мне понадобилось узнать её размер, и отнять его от ширины панели.

Чтобы отрисовать картинку, идём в функцию paint, и добавляем такую строку:

m_pImage->doPaint(this);

Осталось только её удалить — ведь мы не хотим, чтобы в памяти появлялись дыры. Создаем такую функцию деструктора:

CMyPanel::~CMyPanel()
{
    delete m_pImage;
}

Можете полюбоваться на результат:



Загрузка текста из внешних файлов

На первый взгляд может показаться, что эта тема не совсем относится ко VGUI, однако на самом деле тут без текстовых файлов просто и шагу ступить нельзя. Если имена кнопок и заголовков ещё можно прописать прямо в коде, то что вы будете делать с длинными текстами, которые игрок должен прокручивать, чтобы прочесть? Тут уже внешние текстовые файлы подойдут как нельзя кстати. Тем более, если вы — кодер в каком-нибудь проекте, то чем больше настроек будет вынесено из кода в текстовые файлы, тем удобнее будет остальным.

Существует два способа загрузить текст извне.
1) Непосредственно загрузить в память любой нужный текстовый файл, прочесть его, и выгрузить.
2) Использовать специальные функции-помощники класса CHudTextMessage, облегчающие чтение сообщений из файла titles.txt

Первый метод лучше всего подходит для больших кусков текста (например, описания характеристик персонажей в многопользовательских модах). Второй, использующий titles.txt — для коротких сообщений, не превышающих пары строчек, или например, для имен кнопок и заголовков панелей.


Для загрузки файлов первым способом движок предоставляет нам три функции:

byte* COM_LoadFile( char *path, int usehunk, int *pLength );
void  COM_FreeFile( void *buffer );
char* COM_ParseFile( char *data, char *token );

Функция COM_LoadFile загружает в память файл, и возвращает указатель на его начало. В переменную pLength она записывает длину файла, а переменная usehunk, я так понимаю, не используется — она всегда равна пяти.

Каждый загруженный таким макаром файл по окончании работы с ним должен быть выгружен из памяти с помощью функции COM_FreeFile. Ей надо передать указатель на начало файла, который вернула COM_LoadFile. Кстати говоря, файл должен быть выгружен не "когда-нибудь потом", а желательно в той же функции, где он и загружался. Не надо держать файлы загруженными — лучше скопируйте нужный текст в какой-нибудь буфер, и закройте файл.

Многие компоненты VGUI, такие как кнопки или тектовые поля, имеют собственный внутренний буфер, куда они копируют используемый ими текст. Тем самым, загрузка текста из файлов упрощается до неприличия — вы открываете текстовый файл, создаете нужный компонет, он копирует текст во внутренний буфер, и вы закрываете файл.

Вспомогательная функция COM_ParseFile нужна, чтобы облегчить вам чтение отдельных слов из файла. Это на тот случай, если ваш файл является каким-нибудь скриптом или файлом с настройками, и вам надо считать из него значения каких-либо переменных. Функция принимает два аргумента — указатель на положение в тексте, откуда она должна извлечь очередное слово, и указатель на буфер, в который она поместит это слово. Буфер желательно сделать с запасом — например, 1024 символа вполне подойдет. Указатель, возвращаемый COM_ParseFile можно подставить в следующий её вызов, и прочитать идущее следом слово, и так далее.

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

int iarray[10];
int iNumsLoaded = 0; // счетчик загруженных чисел
char token[1024];
char *pfile = gEngfuncs.COM_LoadFile( "numbers.txt", 5, NULL);
if (pfile)
{
    // если файл загружен
    char *ptext = pfile; // не трогаем pfile, он понадобится при закрытии файла
    for (iNumsLoaded = 0; iNumsLoaded < 10; iNumsLoaded++)
    {
        ptext = gEngfuncs.COM_ParseFile(ptext, token); // читаем следующее слово
        if (!strlen(token))
            break; // неожиданный конец файла

        iarray[iNumsLoaded] = atoi(token); // преобразуем в число и пишем в массив
    }
    gEngfuncs.COM_FreeFile(pfile); // выгружаем файл
}

Как видите, всё довольно просто.


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

char *LocaliseTextString( const char *msg, char *dst_buffer, int buffer_size )
char *BufferedLocaliseTextString( const char *msg )

Функции являются статическими, т.е. вам не нужен объект класса, чтобы их вызвать.
Первая функция получает указатель на строку, содержащую имена сообщений, и на буфер, куда она должна записать результаты. Вы также должны указать размер этого буфера, чтобы функция случайно его не превысила. Имена сообщений в строке должны начинаться с символа '#', чтобы функция отличала их от обычного текста. Например, если строкой с именем сообщения является "#T0A0TITLE", то это значит, что в соответствии записям в titles.txt, в результирующем буфере окажется текст "HAZARD COURSE".
Возьмем более сложную строку:

"Employes of the month: #CR1, #CR3, and #CR4"

В этом случае, все имена сообщений CR1, CR2, и CR3 будут заменены на соответствующие записи из файла, и в результате мы получим:

"Employes of the month: Ted Backman, Kelly Bailey, and Yahn Bernier"

Интересно, не правда ли? :)
Теперь разберемся со сторой функцией. Она является упрощенной версией первой — не в том смысле, что она делает меньше работы, а в том, что ей удобнее пользоваться. У неё внутри есть статический буфер размером в 1024 символа, куда она помещает извлеченные строки, и возвращает на него указатель. Конечно, вы должны обработать результат до следующего вызова этой функции, так как её внутренний буфер при этом будет перезаписан новой строкой.

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



Элементы управления

Ну что, соскучились уже по всяким штукам, которые можно подергать и потыкать мышкой? :) Да, действительно, пора бы уже нам сообразить что-нибудь, что будет отвечать на наши мышиные поползновения. Сразу скажу, что о всех элементах управления я рассказывать не буду, только лишь пройдусь вкратце по основным, таким как обычные и прокручиваемые поля с текстом, различные виды кнопок, и, конечно же, картинки.

Так как с этого момента мы начинаем рассматривать VGUI меню, реагирующие на мышь, то нам, соответственно, понадобится курсор. Курсором заведует функция UpdateCursorState из класса TeamFortressViewport. Она проверяет наличие какого-либо меню, требующего курсор, и если таковое находится, то устанавливает его. В противном случае — убирает.
Откройте эту фукнцию и в самом начале обратите внимание на эту длинную строчку:

if (m_pSpectatorPanel->m_menuVisible || m_pCurrentMenu || m_pTeamMenu->isVisible() || ... и т.д.)

Нам надо включить в это условие свое меню. Измените начало условия таким образом:

if ( m_pMyPanel->isVisible() || ... далее остальное )

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


Добавление элемента управления в класс вашего меню сильно напоминает добавление самого меню в класс TeamFortressViewport. Обычно в ваш класс добавляется переменная-указатель, через которую можно будет обратиться к этому элементу, а сам элемент создается в конструкторе вашей панели. Остается только связать его и вашу панель "родственными" связями через setParent, и готово — он сам будет рисоваться и отвечать на ввод пользователя.

Ну давайте для примера возьмем обычную кнопку. Из библиотеки VGUI за кнопки отвечает класс Button. Шаг номер один — идём в описание класса нашей панели (CMyPanel), и добавляем новый указатель. Теперь наш класс выглядит так:

class CMyPanel : public Panel
{
public:
    CMyPanel(); // конструктор
    virtual void paint(); // функция отрисовки

    Button *m_pButton; // указатель на элемент-кнопку
};

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

CMyPanel::CMyPanel() : Panel(XRES(100), YRES(100), XRES(200), YRES(150))
{
    m_pButton = new Button("Cool button", XRES(25), YRES(15));
    m_pButton->setParent(this); // установить подчинение этой панели

    setPaintBackgroundEnabled(false); // отключить вызов paintBackground
}

Вот и всё, кнопка создана. Посмотрите на неё — ну чем не cool button? :) (Как видите, я всё убрал из функции paint, и сделал простую тёмную заливку и черную рамку)

Пока что эта кнопка ничего не делает, и при нажатии только лишь весело подмигивает. Как научить кнопки запускать какие-нибудь штуки, я расскажу когда мы доберемся до обработчиков событий, а до тех пор просто поделаем кнопки-пустышки. Не то чтобы обработчики событий являлись чем-то сложным, просто всему свое время.


Фон и граница панели

Когда мы заботимся о внешнем виде своих собственных панелей, мы, конечно, можем рисовать фон и границу вручную, с помощью заливки и прямоугольника. Однако применение этого способа становится довольно затруднительным, если речь идёт о настройке вида каких-нибудь добавленных в вашу панель элементов управления. Есть парочка функций класса Panel, которые позволяют создать фон и бордюр для нужной панели, не прибегая к "инструментарию художника". О фоне я уже упоминал, когда говорил о версии по умолчанию функции paintBackground.

Для установки цвета фона панели надо просто вызвать функцию

void setBgColor(int r,int g,int b,int a);

Чтобы это сработало, вы не должны переопределять в вашем классе paintBackground, а также не должны отключать её вызов (то есть, команду setPaintBackgroundEnabled(false) из предыдущих примеров теперь использовать не нужно. Также стоит убрать код рисования фона и границы, который мы нагородили в paint).


Установить бордюр для панели не особо сложнее, чем цвет фона, однако тут есть разница в концепции. Загляните в таблицу классов, и найдите там Image, а затем наследующийся от него класс Border. У него есть ещё пять потомков, и объекты любого из них могут выполнять функции отрисовки бордюра для панели. Делается это таким образом — создается объект, а затем назначается панели через функцию setBorder. Вот парочка примеров:

    // создаст черную рамку толщиной в пиксель
    setBorder( new LineBorder );

    // создаст тёмно-красную рамку толщиной в три пикселя
    setBorder(new LineBorder(3, Color(100, 20, 20, 50)));

Не нужно беспокоиться об удалении объектов-бордюров — панель их удалит автоматически. У некоторых бордюров, как у LineBorder, есть конструкторы, которые позволяют задавать параметры границы. Чтобы использовать какой-либо бордюр, вам надо подключить заголовочный файл с именем "VGUI_имя-бордюра.h"


Текстовые поля

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

Я рассмотрю три основных компонента, позволяющих отображать текст. Это:
1) TextPanel — просто панель с текстом.
2) ScrollPanel — то же самое, только умеет прокручиваться мышью.
3) Label — "метка" — однострочное текстовое поле, предназначенное, как правило, для заголовков и подписей.


Как мы помним, добавление нового элемента в панель начинается с создания переменной-указателя под него. Добавьте в наш класс такую переменную:

TextPanel *m_pTextPanel;

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

Затем идём в конструктор панели, где мы и создадим сам объект. (Кнопку из предыдущего примера я убрал, чтобы не мешалась, а также немного увеличил панель)

#define STEPX XRES(10)
#define STEPY YRES(10)

CMyPanel::CMyPanel() : Panel(XRES(100), YRES(100), XRES(300), YRES(204))
{
    char *pfile = (char*)gEngfuncs.COM_LoadFile( "testtext.txt", 5, NULL );
    if (pfile)
    {
        m_pTextPanel = new TextPanel( pfile, STEPX, STEPY, getWide() - STEPX*2, getTall() - STEPY*2 );
        m_pTextPanel->setParent( this );
        m_pTextPanel->setPaintBackgroundEnabled(false); // отключаем рисование фона текста

        gEngfuncs.COM_FreeFile(pfile);
    }

    setBgColor(0, 0, 0, 200); // цвет панели
    setBorder(new LineBorder); // граница
}

Конструктор TextPanel принимает указатель на текст, который она будет выводить, а также стандартные для большинства панелей параметры — координаты и размеры. Здесь текстовая панель загружает текст из файла testtext.txt, расположенного в папке мода. Как видите, в этом нет ничего сложного, так как панель копирует текст в свой внутренний буфер, позволяя нам закрыть файл.

Текстовая панель имеет такую же функцию рисования фона (paintBackground), как и класс Panel, поэтому если бы я её не отключил, то она бы вдобавок нарисовала ещё одну заливку под текстом. В принципе, можно её не отключать, а с помощью функции setBgColor задать какой-нибудь красивый цвет — дело ваше.

Само собой, TextPanel предоставляет вам возможность настроить шрифт и цвет текста. Для этого используются функции setFont и setFgColor — посмотрите их формат в описании класса, там в принципе всё понятно.


В TextPanel показываемый текст просто обрезается по краям, не возволяя нам каким-либо образом увидеть его продолжение. Чтобы создать прокручиваемый текст, надо воспользоваться услугами класса ScrollPanel. Можно подумать, что ScrollPanel — это такой же TextPanel, который, разве что, умеет прокручиваться, но это не так. На самом деле, ScrollPanel — это лишь оболочка с полосками прокрутки, в которую вы должны сами вставить нужную панель. Представьте, какие это дает возможности — ведь можно не только засовывать туда панели с текстом, а также панели с заголовками и картинками.

ScrollPanel, помимо полосок прокрутки, имеет в своем составе ещё две панели — одна условно называется "ClientClip", и ограничивает квадратное окошко, сквозь которое мы видим информацию, а вторая называется просто "Client", и находится внутри ClientClip (т.е., подчинена ей), и она перемещается вместе с движением полосок прокрутки. Client уже, в свою очередь, имеет в подчинении нашу панель, которую мы добавляем в ScrollPanel.

Ну что, давайте сотворим прокручиваемое поле с текстом. Для этого, помимо объекта ScrollPanel, придется также создать TextPanel, и поставить его в подчинение клиентской панели ScrollPanel. Камнем преткновения в этой задаче может стать подборка нужного размера для текстовой панели. А точнее, нам нужно, чтобы размер текстовой панели зависил от реального количества текста в ней, а не был установлен наобум. Иначе у нас получится так, что не полосы прокрутки подстраиваются под текст, а текст подстраивается под отпущенную ему область :) А это, согласитесь, не совсем правильно. Вобщем, смотрите на примере, как создается панель прокрутки, а там дальше уже станет понятно.

Выкидываем всё из нашего конструктора, и используем следующий код (по желанию добавляя переменную m_pScrollPanel в наш класс):

#define STEPX XRES(10)
#define STEPY YRES(10)

CMyPanel::CMyPanel() : Panel(XRES(100), YRES(100), XRES(300), YRES(204))
{
    char *pfile = (char*)gEngfuncs.COM_LoadFile( "testtext.txt", 5, NULL );
    if (pfile)
    {
    // Создаем панель прокрутки и назначаем подчинение нашей панели.
        m_pScrollPanel = new ScrollPanel(STEPX, STEPY, getWide() - STEPX*2, getTall() - STEPY*2 );
        m_pScrollPanel->setParent(this);

    // Назначаем рамку для панели прокрутки
        m_pScrollPanel->setBorder( new LineBorder( Color(0,0,0,50) ) );
    // Указываем автоматическое включение полос прокрутки при необходимости
        m_pScrollPanel->setScrollBarAutoVisible(true, true);
        m_pScrollPanel->setScrollBarVisible(false, false);
    // Вызываем функцию расчета размера ClientClip
        m_pScrollPanel->validate();
        
    // Создаем текстовую панель, и назначаем подчинение клиентской панели ScrolPanel'а.
    // Размер по горизонтали - размер области ClientClip.
    // Размер по вертикали - любой.

        TextPanel *text = new TextPanel(pfile, 0, 0, m_pScrollPanel->getClientClip()->getWide(), 64 );
        text->setParent( m_pScrollPanel->getClient() );
        text->setPaintBackgroundEnabled(false);

    // получаем размеры площади, реально занимаемой текстом
        int iScrollSizeX, iScrollSizeY;
        text->getTextImage()->getTextSizeWrapped( iScrollSizeX, iScrollSizeY );
    // присваиваем эти размеры текстовой панели
        text->setSize( iScrollSizeX , iScrollSizeY );

    // Вызываем повторный расчет параметров ScrollPlanel.
        m_pScrollPanel->validate();

        gEngfuncs.COM_FreeFile(pfile);
    }

    setBgColor(0, 0, 0, 200); // цвет панели
    setBorder(new LineBorder); // граница
}

Ну давайте разбираться по порядку. Сначала мы создаем объект ScrollPanel и назначаем ему бордюр — ну с этим, в принципе, всё должно быть понятно. Далее идет вызов нескольких функций, специфичных для класса ScrollPanel. Вы можете сделать так, чтобы полосы прокрутки были видимы всегда, или видимы только когда текст выходит за пределы области прокрутки. Функция validate заставляет ScrollPanel расчитать площадь ClientClip для заданных размеров панели, а также установить нужные параметры для полосок. Как правило, её надо вызывать после обновления содержимого области прокрутки, или после изменения размеров ScrollPanel.

Следующим шагом у нас идет создание текстовой панели. Координаты ставим 0, 0 — нам не нужны всякие сюрпризы. Горизонтальный размер панели задаем таким, какую, по нашему мнению, ширину не должен превышать текст. Если мы не хотим, чтобы появлялись полосы горизонтальной прокрутки, то подставляем сюда ширину ClientClip — в таком случае, текст никогда не будет выползать за пределы окна по ширине, и не помещающиеся слова будут переноситься на следующую строку. Высоту текстовой панели можно поставить произольной — всё равно потом сделаем пересчет под реальный размер текста.

И последний шаг в создании панели с прокручиваемым текстом — это подгонка размера текстовой панели под реальную прощадь, необходимую тексту. TextPanel содержит внутри себя ещё один объект, с которым мы ранее не знакомились — TextImage. Добраться до него можно, вызвав функцию getTextImage(). Не сказать, что это дает нам много пользы, но вот в этом конкретном случае может пригодиться, потому что этот TextImage как раз и может высчитать площадь, занимаемую текстом. Надо только вызвать его функцию getTextSizeWrapped, и он вернет горизонтальный и вертикальный размеры. Ширина обязательно будет меньше ширины текстовой панели, которую мы указывали в её конструкторе, а вот высота уже полностью зависит от количества текста.

В завершении всего, нужно ещё раз вызвать validate для панели прокрутки, чтобы она всё правильно восприняла и просчитала.

Да, у меня тоже такое ощущение, что чего-то не хватает. Кнопочки у скроллбаров какие-то странные — без стрелочек. Конструкция ScrollPanel такова, что он содержит в подчинении два объекта ScrollBar, а каждый из них — по одному Slider'у и двум кнопкам. Мы можем без труда получить доступ ко всему этом хозяйству и настроить его внешний вид как нам заблагорассудится.

Вы, думаю, обратили внимание на то, что вид создаваемых нами кнопок и полос прокрутки не совсем похож на привычные VGUI-кнопки из зеленых рамочек, подсвечиваемых оранжевым фоном. Дело в том, что мы используем компоненты из набора библиотеки VGUI, а во всех модах, типа TeamFortress или Counter-Strike, используются специальные версии этих объектов, сделанные чисто под TeamFortress. Глянув в таблицу классов, вы можете легко обнаружить различные классы, созданные на базе VGUIшных. Например, у Button есть продолжения рода в виде CommandButton или SpectToggleButton, а ScrollPanel и Slider имеют TeamFortress-версии под названиями CTFScrollPanel и CTFSlider. Можете проэкспериментировать с вышеприведенным кодом, и заместо ScrollPanel подставить CTFScrollPanel. Появится зеленая полоска прокрутки, правда без кнопок, так как необходимые для них TGA-файлы со стрелочками отсутствуют.

Так на чём я остановился? Ах да, мы хотели сделать стрелочки. Версия ScrollPanel по умолчанию хоть и создаёт кнопки, но не назначает им изображения, преполагая, что это сделаем мы сами. К счастью, там в папке валяются какие-то TGA-файлики со стрелочками, коотрые вполне можно приспособить. Для этого нужно после создания ScrollPanel добавить только две строчки:

m_pScrollPanel->getVerticalScrollBar()->getButton(0)->setImage(vgui_LoadTGA("gfx/vgui/arrowup.tga"));
m_pScrollPanel->getVerticalScrollBar()->getButton(1)->setImage(vgui_LoadTGA("gfx/vgui/arrowdown.tga"));

Вы, наверное, спросите — а разве нам не надо удалить объект BitmapTGA, который создаёт функция vgui_LoadTGA? Я в таком случае отвечу — об этом позаботится сама кнопка. Дело в том, что если вы назначаете картинку какому-либо объекту через функцию setImage (которая существует в классе Label и, соответственно, у всех его потомков), то с этого момента он берёт на себя обязанности по её удалению. По сути, он делает то же самое, что мы изучали в главе "Работа с изображениями" — вызывает для неё doPaint и удаляет в деструкторе.

Я бы порекомендовал вам изучить код классов CTFScrollPanel и CTFSlider — это даст весьма наглядные примеры того, как можно создать уникальные поля прокрутки, выглядящие по-своему.


Заканичивая разговор о прокручивамых панелях, я хочу сделать небольшой комментарий относительно вышеприведенного кода. Если вдруг нужного файла не окажется на диске, то текстовая панель вовсе не появится. Может где-то такое поведение будет вполне подходящим, но иногда лучше сделать так, чтобы в панель подставлялся "текст по умолчанию" (например, что-нибудь вроде "Can't load description file"). Вот небольшой кусок кода — думаю, прочитав его, вы поймете основную идею.

char *pfile = NULL;
char *ptext = "Can't load description file";
pfile = gEngfuncs.COM_LoadFile( "testtext.txt", 5, NULL );
if (pfile)
    ptext = pfile;
...
... Далее загружаем текстовую панель, используя текст по указателю ptext
...
if (pfile)
    gEngfuncs.COM_FreeFile(pfile);

Можно, конечно, сделать и более сложную систему, которая в случае отсутствия файла не будет создавать панель, а заменит её чем-нибудь.. Ну, вобщем, на ваше усмотрение.



У нас остался ещё один нерассмотренный элемент для работы с текстом — это Label (метка). Обычно Label — это однострочный тест, использующийся для создания заголовков и подписей. К слову, кнопки наследуются от Label'а, так как они тоже, по сути, состоят из одной строчки текста, только с более навороченным кодом рисования.

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



// Получаем шрифт для заголовков
    CSchemeManager *pSchemes = gViewPort->GetSchemeManager();
    SchemeHandle_t hTitleScheme = pSchemes->getSchemeHandle( "Title Font" );
    Font *pTitleFont = pSchemes->getFont( hTitleScheme );
    int labelsize = pTitleFont->getTall()+YRES(4); // вычисляем высоту метки

// Создаем метку
// Её высота зависит от высоты букв в шрифте
    Label *plabel = new Label("", STEPX, STEPY, getWide() - STEPX*2, labelsize)
    plabel->setParent(this);
    plabel->setFont(pTitleFont);
    plabel->setBgColor(0, 0, 0, 190); // цвет фона
    plabel->setFgColor(255, 255, 255, 0); // цвет текста
    plabel->setContentAlignment( Label::a_center ); // выравнивание по центру
    plabel->setText( "Muddasheep: Halfquake" );

Также стоит немного поправить координаты панели с прокручиваемым текстом, чтобы они не наползали друг на друга, и мы получим результат, как на вышепримеденной иллюстрации.


Кнопки

Когда мы начинали разговор об элементах управления, я уже показывал, как создать базовую кнопку. Поэтому я не буду повторяться, а лучше вместо этого наконец расскажу о том, как заставить кнопку реагировать на нажатия.

Во многих системах для обеспечения обратной связи используется механизм callback'ов — т.е. вы объекту передаёте указатель на функцию, которую он должен вызвать при наступлении определённого события. Во VGUI же интенсивно использутся объектно-ориентированные конструкции — вы передаете указатель на другой объект, виртуальная функция которого будет вызвана при наступлении события.

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

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

class VGUIAPI ActionSignal
{
public:
    virtual void actionPerformed(Panel* panel)=0;
};

Собственно, при создании нового обработчика событий, нашей задачей всего лишь является переопределение функции actionPerformed. Ну и, конечно, при необходимости также надо будет добавить какие-нибудь переменные и, для большего комфорта, конструктор. Указатель, передаваемый в actionPerformed — это указатель на саму кнопку.

Давайте напишем простенький обработчик события (хотя они все, в большинстве случаев, простенькие), который будет закрывать наше окно.

class CMyHandler : public ActionSignal
{
private:
    Panel* pOwner;

public:
    CMyHandler(Panel* owner)
    {
        pOwner = owner;
    }

    void actionPerformed(Panel* panel)
    {
        pOwner->setVisible(false);
        gViewPort->UpdateCursorState();
    }
};

В классе этого обработчика есть одна переменная — pOwner. Она хранит указатель на панель-родитель, который мы передаем в конструкторе при создании обработчика. Функция actionPerformed скрывает эту панель, и заставляет TeamFortressViewport обновить статус курсора мыши. (Кстати, не забудьте, что скрыть панель, и удалить панель — это разные вещи).

Теперь в наше предыдущее окно добавим саму кнопку:

    int butx, buty;
    m_pButton = new Button(" Okay ", 0, 0);
    m_pButton->setParent(this);
    m_pButton->getSize(butx, buty);
    butx = getWide() - butx - STEPX;
    buty = getTall() - buty - STEPY;
    m_pButton->setPos(butx, buty);
    m_pButton->addActionSignal(new CMyHandler(this));

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

У базовой VGUIшной кнопки есть одно преимущество — она сама может высчитать свой размер на основании площади, занимаемой текстом. Здесь я хочу поместить эту кнопку в нижний правый угол панели, поэтому после того, как я создал кнопку, мне нужно узнать её получившиеся размеры. Итоговые координаты кнопки получаются по формуле: coord = panelSize - buttonSize - offset, где offset — это отступ от края панели.

Конечно, нам опять же придется поправить размер панели прокрутки. У меня она имеет такой код создания:

... new ScrollPanel( STEPX, labelsize + STEPY*2, getWide() - STEPX*2, buty - (labelsize + STEPY*3));

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

Как всегда, привожу кадр. Наверное Муддашип вас уже заколебал, поэтому я сменил пластинку :)

При нажатии на кнопку, как и задумано, панель со свистом закрывается, курсор исчезает, и мы можем бегать как обычно. Чтобы показать нашу панель опять, надо откуда-нибудь вызвать gViewPort->m_pMyPanel->setVisible(true), а затем UpdateCursorState. Откуда это вызывать — это, конечно, ваше дело. Можете создать мессадж или эвент, и при его приеме показывать панель (создание мессаджей и эвентов в этой статье не рассматривается)

В таблице классов напротив ActionSignal я поставил троеточие — это значит, что у него очень много потомков (около двадцати). Среди них есть и закрывающие меню (правда не такое, как у нас), и выполняющие консольную команду, и переключающие значения cvar'ов (console variables), и т.д. Если вам нужны примеры, то просто наберите в поиске MSVC строку "public ActionSignal".

Есть один трюк с обработчиками событий, о котором я должен рассказать. Иногда делают так, что сама главная панель и является обработчиком события. Здесь мы сталкиваемся с таким понятием, как множественное наследование — то есть, класс нашей панели одновременно наследуется и от Panel, и от ActionSignal, и, соответственно, среди прочих функций содержит и actionPerformed. Выглядит это примерно так:

class CMyPanel : public Panel : public ActionSignal
{
public:
    CMyPanel();
    ~CMyPanel();
    void paint();
    void actionPerformed(Panel* panel);

    Button *m_pButton;
    ScrollPanel *m_pScrollPanel;
};

Соотетственно, добавлять обработчик события кнопке мы будем уже так:

    m_pButton->addActionSignal( this );

Функция actionPerformed будет делать то же самое, только теперь смотреться будет чуть попроще:

    void CMyPanel :: actionPerformed(Panel* panel)
    {
        setVisible(false);
        gViewPort->UpdateCursorState();
    }

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



Стандартная бело-оранжевая кнопка, конечно, подходит для того, чтобы на ней потренироваться, но, в конце-концов, она является лишь исходником, который подразумевает, что на его основе будет создана нормальная кнопка. В таком виде её нельзя использовать по нескольким причинам — во-первых, её цветовая гамма не поддается настройке (можно только поменять цвет текста через setFgColor, но это уже свойство не кнопки, а класса Label), во-вторых, подсветка нажатия выглядит чисто схематично, ну и в-третьих, у неё есть небольшой баг с подсветкой — если зажать мышь, увести её с кнопки и отпустить, то подсветка останется. В принципе, нам никто не мешает воспользоваться TeamFortress'овской кнопкой CommandButton — это привычная кнопка с рамкой и подсвечиванием при наведении. У неё есть ещё около пятнадцати потомков, из которых, правда, вряд ли что-нибудь будет для вас полезным — они, в основном, заточены под TeamFortress (например, некоторые видимы только для одной команды и т.п.). Я их не стал рисовать в таблице классов, а просто поставил троеточие.

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

Сейчас я для примера покажу, как можно просто переделать внешний вид кнопки. Как вы знаете, класс кнопки наследуется прямо от класса Label, который в функции paint отрисовывает текст. Кнопка же свой фон отрисовывает в paintBackground, что очень удобно — мы можем, не трогая рисование текста, переделать рисование самой кнопки.

class CMyButton : public Button
{
public:
    // пустой конструктор
    CMyButton(const char* text, int x, int y) : Button(text, x, y) {};
    virtual void paintBackground()
    {
        int r, g, b, a;
        getBgColor(r, g, b, a); // получаем заданный цвет фона
        drawSetColor(r, g, b, 0);
        drawFilledRect(0, 0, getWide(), getTall()); // закрашиваем фон

        // кнопка имитирует выпуклость в обычном состоянии, и
        // утопленность при нажатии.

        if (isSelected())
        {
            drawSetColor(255, 255, 255, 70);
            drawOutlinedRect(0, 0, getWide()-1, getTall()-1);

            drawSetColor(0, 0, 0, 70);
            drawOutlinedRect(1, 1, getWide(), getTall());
        }
        else
        {
            drawSetColor(0, 0, 0, 70);
            drawOutlinedRect(0, 0, getWide()-1, getTall()-1);

            drawSetColor(255, 255, 255, 70);
            drawOutlinedRect(1, 1, getWide(), getTall());
        }

        // ограничивающая рамка.
        drawSetColor(r/2, g/2, b/2, 0);
        drawOutlinedRect(0, 0, getWide(), getTall());
    }
};

Ну и при создании не забываем указывать цвет:

    m_pButton->setBgColor(30, 100, 30, 0); // цвет фона
    m_pButton->setFgColor(255, 255, 255, 50); // цвет текста

Вот такая у нас получится кнопочка, которая, к тому же, имитирует утапливаемость при нажатии, как обычная кнопка windows. Согласитесь, уже какой-то прогресс, по сравнению с базовой кнопкой :).


Во VGUI существует ещё два вида кнопок: радиокнопки и флажки (оба наследуются от ToggleButton, предком которого является уже знакомый нам Button). Радиокнопки — это такие кружочки с подписями, как правило, объединяемые в группы, из которых вы можете выбрать только одну. Флажки — это квадратики либо с крестиками либо с галочками. В стандартной библиотеке VGUI за эти элементы отвечают классы RadioButton и CheckButton соответственно. Если глянуть их описания, то вы увидите, что там ничего нет, кроме функции рисования и пары конструкторов, которые имеют такой же вид, как и конструкторы класса Button.

У радиокнопок есть одна специфика — один должны быть разбиты по группам, иначе в них нет никакого смысла. Существует класс под названием ButtonGroup, который является логическим контейнером для группы радиокнопок. Я его назвал логическим потому что ButtonGroup не является панелью, и никак не отрисовывается — т.е. если вы хотите рамку вокруг группы радиокнопок, то придется её рисовать самим. Из предыдущего вытекает, что объекты ButtonGroup не удаляются автоматически, и об их удалении надо позаботиться нам. Привязать кнопку к определенной группе довольно просто — надо лишь вызвать её функцию setButtonGroup и в качестве аргумента передать указатель на объект ButtonGroup.

У всех кнопок есть статус selected, который можно узнать функцией isSelected. Разница только в принципе, по которому кнопки этот статус себе устанавливают. Обычные кнопки находятся в этом статусе, когда пользователь её зажимает мышью. Флажки переключают этот статус от щелчков (галочка в квадратике то загорается, то исчезает). Из всех радиокнопок в группе только одна из них имеет статус selected включенным — та, которую пользователь последней ткнул мышью.

События от радиокнопок и флажков обрабатываются так же, как и от обычных кнопок — с помощью ActionSignal.

Ну что-ж, давайте закрепим материал примером. Наверное, вы все знаете про консольные команды "r_speeds" и "r_drawentities". Я сделал небольшую панельку, которая позволяет с помощью флажка и радиокнопок вызывать эти команды. Сначала посмотрите на кадр, а далее я приведу код этой панели.


// описание класса панели
class CMyPanel : public Panel
{
public:
    CMyMenu();
    ~CMyMenu();
    virtual void paint();

    ButtonGroup *m_pButtonGroup;
    int m_iBorderSizeX;
    int m_iBorderSizeY;
    int m_iBorderPosY;
};

// обработчик события радиокнопок
class CEntDrawActionSignal : public ActionSignal
{
private:
    int m_iMode; // сохраняем режим r_drawentities этого обработчика

public:
    CEntDrawActionSignal(int mode)
    {
        m_iMode = mode;
    }

    void actionPerformed(Panel* panel)
    {
        char sz[64];
        sprintf(sz, "r_drawentities %d\n", m_iMode);
        ClientCmd(sz);
    }
};

// обработчик события флажка
class RSpeedsToggleSignal : public ActionSignal
{
public:
    void actionPerformed(Panel* panel)
    {
        Button* pbut = (Button*)panel;
        if (pbut->isSelected())
            ClientCmd("r_speeds 1");
        else
            ClientCmd("r_speeds 0");
    }
};

#define TITLE_STEP XRES(5)

// конструктор панели
CMyPanel::CMyPanel() : Panel(XRES(100), YRES(100), XRES(400), YRES(250))
{
    // массив с именами кнопок
    static char* radioButtons[] = {
        "Don't draw entities",
        "Draw normal",
        "Show bones",
        "Colored hitboxes",
        "Transparent hitboxes",
        NULL };

    // создаем заголовок рамки
    Label *plabel = new Label("Draw entites mode:", STEPX + TITLE_STEP, STEPY);
    plabel->setParent(this);
    plabel->setBgColor(0, 0, 0, 0);
    plabel->setFgColor(255, 255, 255, 0);

    int sizeX, sizeY;
    plabel->getSize(sizeX, sizeY);
    m_iBorderPosY = STEPY + sizeY/2;

    m_pButtonGroup = new ButtonGroup; // создаем группу кнопок

    int buttonNumber = 0; // режим r_drawentities для создаваемой кнопки
    int butX = STEPX*2; // координаты создаваемой кнопки
    int butY = STEPY*2 + sizeY;
    m_iBorderSizeX = 0; // по ходу дела подсчитываем нужный размер рамки
    m_iBorderSizeY = 0;
    Button *pbut;
    // читаем массив с именами кнопок пока он не кончится, и создаем их
    while (radioButtons[buttonNumber])
    {
        pbut = new RadioButton(radioButtons[buttonNumber], butX, butY);
        pbut->setParent(this);
        pbut->setButtonGroup(m_pButtonGroup);
        pbut->addActionSignal(new CEntDrawActionSignal(buttonNumber));
        pbut->setPaintBackgroundEnabled(false);
        pbut->setFgColor(255, 255, 255, 0);
        pbut->getSize(sizeX, sizeY);
        butY += sizeY;
        buttonNumber++;
        if (m_iBorderSizeX < sizeX) m_iBorderSizeX = sizeX;
    }

    m_iBorderSizeX += STEPX*2;
    m_iBorderSizeY += butY - m_iBorderPosY + STEPY;

    pbut = new CheckButton("Show r_speeds", STEPX, m_iBorderPosY + m_iBorderSizeY + STEPY);
    pbut->setParent(this);
    pbut->addActionSignal(new RSpeedsToggleSignal);
    pbut->setPaintBackgroundEnabled(false);
    pbut->setFgColor(255, 255, 255, 0);
    pbut->getSize(sizeX, sizeY);

    setSize(m_iBorderSizeX + STEPX*2, m_iBorderPosY + m_iBorderSizeY + sizeY + STEPY*2);
    setBgColor(0, 0, 0, 120);
    setBorder(new LineBorder);
}

void CMyPanel::paint()
{
    // рисуем рамку вокруг радиокнопок
    drawSetColor(0, 0, 0, 0);
    drawOutlinedRect(STEPX, m_iBorderPosY, STEPX + m_iBorderSizeX, m_iBorderPosY + m_iBorderSizeY);
}

CMyPanel::~CMyPanel()
{
    // удаляем группу кнопок
    delete m_pButtonGroup;
}

Как и в предыдущих примерах, панель автоматически высчитывает свои размеры, основываясь на размере и количестве кнопок. Правда с ними есть одна проблема — я уж не знаю, откуда VGUI берёт эти галочки и круглешки, но они вообще не меняют своего размера при переходе в разные разрешения. В 640х480 смотрится довольно крупновато по сравнению с текстом, а представляю, что будет в 400х300... Для флажков существует альтернатива — SpectToggleButton, который использует tga-картинки. Правда его обработка событий настроена таким образом, чтобы можно было менять значение какого-либо cvar'а. Несмотря на это, я всё равно рекомендую ознакомиться с его кодом — может быть вы, основываясь на нем, напишите класс своего флажка.


Картинки

Ранее мы уже рассматривали класс BitmapTGA и учились им пользоваться для "ручного" вывода картинки из функции paint. Но во VGUI существует также парочка элементов, которые могут облегчить этот процесс, и снять с нас часть забот по рисованию и удалению картинки. Это уже знакомый нам Label и пока ещё не очень знакомый ImagePanel.

Помимо создания текстовых подписей, Label также позволяет выводить картинки. Нужно только создать объект BitmapTGA (например, с помощью функции-помощника vgui_LoadTGA), и назначить его метке через функцию setImage. После этого мы можем о нем забыть — метка сама будет рисовать и удалять картинку.
Пример:

    Label *plab = new Label("", STEPX, STEPY);
    plab->setParent(this);
    plab->setImage(vgui_LoadTGA("gfx/vgui/640_banner.tga"));
    plab->setPaintBackgroundEnabled(false); // отключить фон для метки

Всего лишь четыре строчки кода, и картинка в верхнем левом углу готова. Для самых-пресамых ленивых есть класс CImageLabel. Он наследуется от Label, и делает всё то же самое, только у него более удобный конструктор. С ним вообще получается две строчки:

    CImageLabel *plab = new CImageLabel("banner", STEPX, STEPY);
    plab->setParent(this);

Для CImageLabel требуется указать только имя файла картинки — он сам проставляет путь "gfx/vgui" и нужную приставку (320 или 640) в зависимости от разрешения.


ImagePanel делает чуть меньше работы, чем Label — он рисует картинку, но не удаляет её. По мне так — он удобен для того, чтобы делать переключающиеся картинки. Вот предположим, вы делаете класс своего флажка. Вы загружаете два BitmapTGA, и создаете один ImagePanel, которому названачаете один из этих TGA'шек. При переключении режима кнопки вы просто функцией setImage назначаете ImagePanel'у то одну, то другую картинки. Естественно, что объекты BitmapTGA потом надо будет удалить операцией delete.



Обработка сигналов мыши и клавиатуры

Когда вы изучали класс Panel, вы, наверное, увидели там такой список функций:

virtual void internalCursorMoved(int x,int y);
virtual void internalCursorEntered();
virtual void internalCursorExited();
virtual void internalMousePressed(MouseCode code);
virtual void internalMouseDoublePressed(MouseCode code);
virtual void internalMouseReleased(MouseCode code);
virtual void internalMouseWheeled(int delta);

Как можно догадаться из их названия, они вызываются при наступлении соответствующих событий — при движении курсора, при входе его в область панели и выходе из неё, при нажатии кнопки мыши, и так далее. От вас только требуется переопределить их к классе вашей панели, и написать соответствующий код реакции на событие. Например, обычные кнопки при нажатии мыши устанавливают себе статус selected, а при отжатии — снимают его, и срабатывают. Кстати, к разговору о кнопках — помните, я говорил, что у обычной кнопки есть такая ошибка — если зажать мышь на её территории, а отпустить где-то в другом месте, то подсветка всё равно останется. Это исправляется простой обработкой события internalCursorExited(). К примеру, идём в класс нашей кнопочки CMyButton, и добавляем туда:

    void internalCursorExited()
    {
        setSelected(false);
    }

Кстати говоря, следует помнить, что каждое событие мыши получает только один объект. Например, если у вас есть панель, и внутри кнопка, то при щелчке мышью по пустому пространству панели сигнал MousePressed будет получать панель, а при щелчке по кнопке — только кнопка.


Переопределять функции — это, конечно, хорошо, но не всегда удобно. Вот представим стандартную для работы в среде windows ситуацию — у нас есть много различных объектов, типа кнопок, панелей, картинок, и так далее, а внизу расположена строка состояния, в которой при наведении мышью на какой-либо из вышеперечисленных объектов появляется краткое его описание. Следуя логике переопределения фунций, мы должны создать специальные версии кнопок, текстовых полей, и картинок, которые при получении события CursorEntered будут забивать соответствующую строку в определенный Label. Мутор какой-то, скажете вы. Здесь на помощь приходит ещё один обработчик события — InputSignal, который используется столь же часто, сколько и ActionSignal. Использование InputSignal позволяет назначить обоработку мышиных событий произвольному объекту, независимо от того, какого он класса (лишь бы он был родом от Panel'а). Описание класса InputSignal содержит всё те же вышеперечисленные функции, только без приставки "internal". Привязка обработчика к объекту осуществляется через функцию addInputSignal.

Давайте создадим небольшую панельку с тремя кнопками и неким подобием строки состояния, в которой будут писаться подписи к этим кнопкам. Обработчик будет помнить, какой он должен вывести текст, и в какую метку. Обычно, кстати, Input-обработчики наследуют не от самого InputSignal, а от CDefaultInputSignal — там все функции проставлены пустыми по умолчанию.

Сами кнопки в этом примере не будут делать ничего — да, собственно, нам это и не надо.



// обработчик события
class CShowDescriptionSignal : public CDefaultInputSignal
{
private:
    const char* ptext; // указатель на текст комментария
    Label* plabel; // указатель на метку

public:
    CShowDescriptionSignal(const char* text, Label* label)
    {
        ptext = text;
        plabel = label;
    }

    void cursorEntered(Panel* panel)
    {
        plabel->setText(ptext);
    }
};

Код класса. Я не буду разделять описание и функции, чтобы было покороче.

class CShutDownPanel : public Panel
{
private:
    Label* m_pLabel; // указатель на метку

public:
    // конструктор
    CShutDownPanel() : Panel(XRES(100), YRES(100), XRES(300), YRES(150))
    {
        m_pLabel = new Label("", 0, 0);
        m_pLabel->setParent(this);
        m_pLabel->setPaintBackgroundEnabled(false);
        m_pLabel->setFgColor(255, 255, 255, 0);
        m_pLabel->setPos(STEPX, getTall() - STEPY - m_pLabel->getTall());

        int posY = STEPY;
        CMyButton* pbut = new CMyButton("Explode monitor", STEPX, posY);
        pbut->setParent(this);
        pbut->addInputSignal(new CShowDescriptionSignal("explodes your monitor", m_pLabel));
        pbut->setBgColor(20, 100, 40, 0);
        pbut->setFgColor(255, 255, 255, 0);
        posY += pbut->getTall() + YRES(5);

        pbut = new CMyButton("Format C", STEPX, posY);
        pbut->setParent(this);
        pbut->addInputSignal(new CShowDescriptionSignal("formats you C drive", m_pLabel));
        pbut->setBgColor(20, 100, 40, 0);
        pbut->setFgColor(255, 255, 255, 0);
        posY += pbut->getTall() + YRES(5);

        pbut = new CMyButton("Shut down", STEPX, posY);
        pbut->setParent(this);
        pbut->addInputSignal(new CShowDescriptionSignal("just shuts down the system", m_pLabel));
        pbut->setBgColor(20, 100, 40, 0);
        pbut->setFgColor(255, 255, 255, 0);
        posY += pbut->getTall() + YRES(5);

        setBgColor(0, 0, 0, 120);
        setBorder(new LineBorder);
    }

    // рисуем полоску, имитирующую строку состояния.
    virtual void paint()
    {
        int x, y;
        m_pLabel->getPos(x, y);

        drawSetColor(255, 255, 255, 200);
        drawFilledRect(0, y, getWide(), y + m_pLabel->getTall());
    }

    // убираем текст комментария, если курсор на самой панели, а не на кнопке
    virtual void internalCursorEntered()
    {
        m_pLabel->setText("");
    }
};

Конечно, код создания трех кнопок подряд выглядит несколько неуклюже. Вообще удобнее было бы, как в предыдущем примере, сделать массив с их именами и подписями, а потом в цикле его читать, пока не наткнемся на NULL. Да и кнопки разного размера не очень выглядят.. Ну для демо менюшки, я думаю, простительно.

Обратите внимание на такой важный момент в конструкции нашего обработчика — он не содержит никакого внутреннего буфера для текста, а хранит только указатель на него. Это значит, что его можно использовать только с константными строками. Если вы захотите загружать подписи из titles.txt или ещё откуда-нибудь, то придется встраивать в класс обработчика массивы для хранения строк, типа char szDesc[128]. (Впрочем, работа со строками в языке си не входит в компетенцию этой статьи).

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



Что-то мы всё о мыши, да о мыши. Наверное, если вы захотите сделать меню выбора чего-либо, то неплохо будет также предусмотреть использование хоткеев (много ли людей тыкают мышью во VGUI-меню в CounterStrike?). Обработка нажатия кнопок на клавиатуре чуток посложнее, чем обработка мыши. Возможность создания реакции на нажатия кнопок клавиатуры не является частью VGUI, и поэтому класс TeamFortressViewport имеет связь с Input-системой, откуда он уже передает сигналы в соответствующие меню.

Откройте класс TeamFortressViewport, и найдите функцию KeyInput. Туда попадают все нажимаемые на клавиатуре кнопки. Если эта функция возвращает 1, то кнопка дальше обрабатывается движком. Если KeyInput возвращает 0, то он перехватывает кнопку. Помните, как мы добавляли проверку нашей панели в UpdateCursorState? Здесь мы будем заниматься чем-то подобным. Есди представить, что m_pMyPanel — это указатель на нашу панель, а ProcessKey(int down, int keynum) — это её функция для обработки нажимаемых кнопок, то тогда надо добавить что-то вроде этого:

if (m_pMyPanel->isVisible)
    return m_pMyPanel->ProcessKey(down, keynum);

Если наша панель включена, то тогда судьба кнопки зависит от того, что решит её фукнция ProcessKey. Если мы получаем какую-то интересующую нас кнопку (предположим, меню предлагает выбрать варианты от 1 до 9), то тогда обрабатываем её и возвращаем ноль, чтобы на эту кнопку кроме нас больше никто не среагировал (скажем, чтобы вдруг не открылось меню выбора оружия). Если кнопка нас не интересует, то возвращаем 1. Блокировать абсолютно все кнопки не стоит — а то пользователь даже не сможет выйти в меню или вызвать консоль.

Коды специальных кнопок, типа alt, f12 и т.п. прописаны в файле engine\keydefs.h. Обычные текстовые кнопки идут как строчные символы ascii. Переменная down позволяет отличать нажатия от отжатий — при нажатии она равна еденице.

Я думаю, обработка кнопок клавиатуры — не такая уж и сложная штука, чтобы городить очередной пример. В крайнем случае, посмотрите код той же KeyInput.



Заключение

Подводя итоги, хочется озвучить мысль о том, что VGUI — это далеко не зеленые рамочки с оранжевой заливкой, каким его обычно все привыкли видеть, и даже не меню покупки оружия в Counter-Strike. VGUI — это система для создания двухмерного пользовательского интерфейса, ни больше, ни меньше. Про VGUI нельзя сказать, что он красивый или некрасивый, соответствует атмосфере игры, или нет. Точно так же, как это нельзя сказать, к примеру, про MFC. Каким вы интерфейс нарисуете, таким он и будет.

В статье были рассмотрены не все вещи, которые изначально планировались — например, вне поля зрения остались поля ввода текста (TextEntry), а также мы не поговорили о каскадных меню (наподобие меню кнопки start в windows), которые, можно, кстати, создавать через текстовые файлы... Но, впрочем, я надеюсь, что это руководство дало вам базу, достаточную для самостоятельного освоения всего, что осталось во VGUI. Попробуйте обратиться за примерами к уже существующим меню из TeamFortress — вряд ли там сейчас найдется что-нибудь, что окажется непонятным. Ну и конечно же, экспериментируйте и испытывайте — ведь ощущения уверенности от того, как вы сами убедитесь в работоспособности своего кода, не даст ни одно руководство.

До новых встреч!



С уважением,
BUzer
февраль 2005
2005 Shambler Team
  Counter.CO.KZ -     !
Hosted by uCoz