Manual de VGUI
You can help by finishing the translation.
Also, please make sure the article tries to comply with the alternate languages guide.Contents
Descripción general
La biblioteca VGUI2 (vgui2.dll) es propiedad de una interfaz gráfica de usuario (GUI) proporcionada por el motor Source. Todas las aplicaciones Source & Steam usan VGUI para presentar ventanas, diálogos o menus. La arquitectura de objetos es jerarquica y todos los elementos implementados son controlados desde las clases base del VGUI. El sistema de entradas del teclado y el ratón es conducido por eventos y bastante similar a otras librerias GUI (como Java Swing o Formularios .NET). Las implementaciones para los elementos GUI más comunes como botones, campos de texto o imagenes, son proporcionadas por la libreria de controles VGUI (vgui_controls.lib
). Además de dibujar elementos GUI y procesar entradas, VGUI tanbién controla la localización para la presentación de testo en el lenguaje preferido.
Todos los VGUI base de las cabeceras de la interfaz se encuentran en \public\vgui\
, los elementos de control son definidos en \public\vgui_controls\
.
Como autor de mods lo más probable es que uses VGUI en el proyecto client.dll para mostrar los menús, elementos HUD o para presentaciones dentro del juego (en weapons o ordendaores, etc). La clase básica de la que derivan todos los elementos VGUI es vgui::Panel
, que define un area en tu pantalla que tiene un tamaño específico y una posición, puede dibujarse a si misma y procesar eventos de entrada. Las ventanas de dialogo, campos de texto y botones son todos paneles VGUI en relación jerarquica padre-hijo. El primer panel es el panel raiz y es proporcionado por el motor Source. El panel raiz del cliente cubre la pantalla entera, pero no muestra nada. Incluso aunque pudieras usar el panel raiz del cliente, la mayoria de paneles de cliente usan el panel compartido BaseViewport
como padre ( g_pClientMode->GetViewport()
).
Este diagrama muestra una parte de la jerarquía del panel del cliente. Una flecha del panel a al panel b significa que a es padre de b .
Para navegar a través de la jerarquía del panel VGUI durante el tiempo de ejecución, puedes abrir la herramienta VGUI Hierarchy ejecutanto el comando vgui_drawtree 1
en la console de desarrollo. Todos los paneles están listados en un TreeView expandible.
Los nombre de los paneles están coloreados:
blanco | El Panel es visible |
gris | El Panel esta oculto |
amarillo | Panel Popup (Frame) |
verde | El Panel tiene el foco |
Las siguientes opciones están disponibles:
Show Visible | Lista todos los paneles visibles. |
Show Hidden | Lista todos los paneles ocultos. |
Popups only | Lista solo los paneles Popup (Frames) |
Highlight Mouse Over | Los paneles están destacados con un borde coloreado cuando mueves el ratón sobre el. El árbol de paneles se expanderá para mostrar el panel actual. |
Freeze | Bloquea el TreeView actual. |
Show Addresses | Muestra las direcciones de memoria de los paneles. |
Show Alpha | Muestra el valor alpha del panel, 0 = translucido, 255 = opaco |
In Render Order | Muestra un árbol de paneles en orden de presentación. |
La Clase Panel
La clase VGUI Panel es la clase base para, alrededor de 50 elementos de control diferentes, que son definidor en \public\vgui_controls\
. Los archivos de cabecera normalmente proporcionan una buena documentación sobre las características y comportamientos del control. El siguiente diagrama muestra la jerarquía de clase para algunos elementos comunmente utilizados:
Panel | ||||
---|---|---|---|---|
EditablePanel | Label | TextEntry | RichText | ImagePanel |
Frame | Button | ComboBox | ||
MessageBox | ToggleButton |
Desde que todas las clases VGUI derivan de la clase Panel, deberemos prestar una mayor atención a las funciones y propiedades esenciales de sus miembros.
El constructor más habitualmente utilizado para crear un panel es Panel(Panel *parent, const char *panelName)
, desde que cada panel necesita un panel-padre, este pertenece a un único nombre en su nivel jerarquico. Una vez un panel es creado, puedes hacerlo visible o ocultarlo de nuevo con SetVisible(bool state)
.
VGUI mantiene un identificador para cada nuevo grupo, que se utiliza para tratar globalmente un panel sin pasar punteros. Muchos eventos de funciones de mensajería usan ese identificador VPANEL
para reconocer paneles fuente y objeto. Para conseguir el identificador único VPANEL
de un panel, llama a GetVPanel()
.
Para establecer la posición y el tamaño de un panel usa las funciones miembro SetPos(int x,int y)
y SetSize(int wide,int tall)
. EL tamaño siempre en pixeles y no se escala automáticamente con aumento de resoluciones. La posición es siempre relativa al panel - padre dado, donde (0,0) es la esquina superior izquierda.
Para hacer una iteración a través de la jerarquía de paneles usando un determinado panel como punto de inicio, debes conseguir el panel-padre llamando GetParent()
o GetVParent()
para tomar el identificadr correspondiente VPANEL
. Por supuesto, un panel puede tener solo un panel-padre, pero múltiples paneles-hijo (sub panels, botones, elementos de lista, etc). GetChildCount()
devolverá el número total de hijos, pudiendo ser cada uno consultado con GetChild(int index)
.
Si un panel debe reaccionar a eventos de entrada como una tecla presionada o un click del ratón, las funciones virtuales OnEvent()
como OnMousePressed(MouseCode code)
o OnKeyCodePressed(KeyCode code)
deben ser evitadas. Los eventos de entrada de ratón y teclado pueden ser activados / desactivados para un panel, usando SetMouseInputEnabled(bool state)
o SetKeyBoardInputEnabled(bool state)
.
Otra útil función virtual a evitar es OnThink()
que es llamada cada vez que el panel es repintado. En ocasiones, esto es demasiado y querrás actualizar o chequear el estado del panel menos a menudo. Entonces puedes conectar con OnTick()
llamada frecuentemente en un intervalo de tiempo ajustable. Pero debe ser dicho que VGUI debería llamar OnTick()
para un panel dado y cuan a menudo, esto se hace con ivgui()->AddTickSignal(VPANEL panel, int intervalMilliseconds)
.
Windows & popups
Un Panel
como es descrito arriba es siempre un 100% del area de su panel contenedor, su posición está siempre en una posición fija relativa y solo puede ser cambiada por código. También se dibuja a si mismo solo dentro de la región contenedora, si mueves un elemento VGUI fuera de la región que debe contenerlo, será recortado. Esto está bien para añadir elementos de control, imagenes o campos de texto a un panel, pero no permitiría ventanas pop-up independientes o cajas de dialogo que el usuario pueda mover, redimensionar o minimizar. Por tanto, los paneles VGUI tienen la función MakePopup()
que despareja un panel de su contenedor renderizando y haciendo una nueva ventana independiente. sin embargo sigue perteneciendo a un panel contenedor y se hace invisible cuando el contenedor lo hace.
En la mayoría de casos no llamarás a la función MakePopup()
por ti mismo pero usa la clase Frame
en su lugar. La clase Frame
encapsula todas las caracteristicas comunes de las ventanas GUI como la barra de titulo, el sistema de menús (cerrar, minimizar y maximizar), arrastrar, redimensionar, focalizar y controlar el diseño.
Aqui hay un pequeño ejemplo de como abrir un frame y activarlo. Hágase notar que el frame se borrará a si mismo una vez el usuario cierre el Frame
o la función Close()
sea llamada (solo asegurate de que los objetos sean limpiados en el destructor).
Frame *pFrame = new Frame( g_pClientMode->GetViewport(), "MyFrame" ); pFrame->SetScheme("ClientScheme.res"); pFrame->SetSize( 100, 100 ); pFrame->SetTitle("My First Frame", true ); pFrame->Activate(); // set visible, move to front, request focus
Si un Frame es redimensionado, la función virtual PerformLayout()
es llamada de manera que el frame pueda reorganizar sus elementos para formarlos mejor para el nuevo tamaño. La posición y el tamaño actual del Frame pueden ser bloqueadas también con SetMoveable(bool state)
y SetSizeable(bool state)
.
Evento de mensajes
Los paneles VGUI se comunican a través de un sistema de mensajes a los cambios de estado de señal o eventos hacia los contenedores o contenidos (o cualquier otro panel). Los mensajes no son enviados directamente (por ejemplo, llamando a una función de escucha del panel.) pero son entregados al VGUI quien los entrega a los paneles objetivo. Así tras el contenido del mensaje, paneles emisor y receptor deben ser especificados como identidicadores VPANEL . VGUI envía mensajes de evento para informar a los paneles sobre cambios o eventos (movimiento del ratón, focos de entrada, etc...).
El nombre del mensaje y el contenido son especificados usando un objeto KeyValues (\public\KeyValues.h
). La clase KeyValues es un estructura muy genérica y flexible para almacenar registros de datos que contengan cadenas de texto, enteros o numeros float. A KeyValues object has a name and a set of data entries. Each entry has a unique key name and corresponding value. KeyValues also allow creating hierarchical structures using sub keys, though most VGUI messages are flat data records. KeyValues don't have data/type definitions or similar, so you can add or remove entries of any type as you like. Thus sender and receiver must both know the internal structure (e.g. key names, types and their meaning) of a KeyValues message object to communicate successfully. Here some sample code how to use KeyValues in general:
// create a new KeyValues Object named "MyName" KeyValues *data = new KeyValues("MyName"); // add data entries data->SetInt( "anInteger", 42 ); data->SetString( "aString", "Marvin" ); // read data entries int x = data->GetInt("anInteger"); Con_Printf( data->GetString("aString") ); // destroy KeyValues object again, free data records data->deleteThis();
To send a message you can call the Panel member function PostMessage(...)
or directly ivgui()-> PostMessage(...)
. The name of the KeyValues object is also the message name used for later dispatching. VGUI will call the target panel's OnMessage(...)
function, which will dispatch the message to a previous defined message handler. A panel can register new message handlers with one of the MESSAGE_FUNC_*
macros, which adds a handler function to the message map. Never override OnMessage(...)
to handle new messages, always use a macro. Please note that when you use the macro MESSAGE_FUNC_* only those specific messages will be thrown to your function. If you want to catch the newLine (ENTER) message from a txtEntry element you will use MESSAGE_FUNC_PARAMS( NewLineMessage, "TextNewLine",data)
. NewLineMessage
is function to call, "TextNewLine" means all messages who's names are "TextNewLine" will be redirected and finally data is the input that will be passed to your function. In this case NewLineMessage
will look like this 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"); }
The sending panel creates a KeyValues object, adds message parameters and sends the message (to its parent in this case). The KeyValues object is destroyed by VGUI after it has been processed.
void MyChildPanel::SomethingHappened() { if ( GetVParent() ) { KeyValues *msg = new KeyValues("MyMessage"); msg->SetString("text", "Something happened"); PostMessage(GetVParent(), msg); } }
Using PostMessage()
the sending panel must address a single, specific target, which means that all other panels interested in a state change must be known and addressed individually. To avoid hard coding these dependencies, panels have a public event system called action signals. A panel fires generic events with PostActionSignal(KeyValues *message)
and interested panels can register as listeners for these signals with AddActionSignalTarget(Panel *target)
. These action signals are widely used by VGUI controls, for example messages like "TextChanged"
fired by class TextEntry or "ItemSelected"
used by class ListPanel
. All action signal messages contain a pointer entry "panel" that points to the sending panel.
The example from above using action signals would need the parent panel to register as a listener, preferably in the constructor after the child panel has been created. The child panel just uses PostActionSignal()
instead of PostMessage()
:
MyParentPanel::MyParentPanel() { ... m_pChildPanel->AddActionSignalTarget( this ); } void MyParentPanel::OnMyMessage (KeyValues *data) { const char *text = data->GetString("text"); Panel *pSender = (Panel *) data->GetPtr("panel", NULL); } void MyChildPanel::SomethingHappened() { KeyValues *msg = new KeyValues("MyMessage"); msg->SetString("text", "Something happened"); PostActionSignal ( msg ); }
To catch the return/enter key from a txtEntry
element use this.
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); // with the txtEntry Type you need to set it to pass the return key as a message } void MyParentPanel::NewLineMessage (KeyValues *data) { // when the txtEntry box posts an action signal it only sets the name // so it is wise to check whether that specific txtEntry is indeed in focus // Is the Text Entry box in focus? if (m_pChildPanel->HasFocus()) { // We have caught the message, now we can handle it. I would simply repost it to the OnCommand function // Post the message to our command handler this->OnCommand("Submit"); }// End HasFocus() } // Dawid Joubert 2006-02-11
A commonly used action signal is the "Command" message, since no message handler needs to be installed. Panels just need to derive the virtual Panel function OnCommand(const char *command)
and check for the correct command string. The "Command" message is used by all Button classes and is fired whenever the button is pressed. Here is the example from above using the Command message:
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, "SomethingHappened")) { DoSomething (); } else { BaseClass::OnCommand(command); } } void MyChildPanel::SomethingHappened() { KeyValues *msg = new KeyValues("Command"); msg->SetString("command", "SomethingHappened"); PostActionSignal ( msg ); }
Schemes
A VGUI scheme defines the general "look & feel" of your panels by specifying used colors, fonts & icons of control elements. Schemes are defined in resource files, for example hl2\resource\clientscheme.res
. A new panel inherits by default the scheme settings used by its parent. The VGUI scheme manager can load a new scheme with LoadSchemeFromFile(char *fileName, char *tag)
which returns HScheme
handle. To make a panel using a loaded VGUI scheme call the panel member function SetScheme(HScheme scheme)
.
Schemes set the general look of panel elements, but they don't arrange specific control elements of your panel. One way of adding elements to your panel is by doing that in source code. You can create them (usually in the constructor of the parent panel) and set properties like size and position manually using the panel member functions. This will become quite complex and time-consuming for larger dialogs with lots of control elements.
The most common way to define the layout of your panel elements is to describe all elements in an external resource file like hl2\resource\UI\classmenu.res
(a KeyValues text file). When the parent panel is created, this file is loaded and executed with LoadControlSettings(char *dialogResourceName)
. Do note, however, that this function is only defined for EditablePanel (or an EditablePanel
derivative such as Frame
). In this resource file each control element is defined in a separate section. A typical control definition looks like this:
"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" // left text alignment }
Each control property has a key name and a value. Properties defined by the base class Panel are available for all controls (like xpos, ypos, wide, tall
etc). A list of all available key names is returned by the panel member function GetDescription()
. Derived panel classes may add new fields for their specific properties. Processing these new fields must be handled by overriding the virtual function ApplySettings(KeyValues *inResourceData)
. Here you can also lookup how values for an existing control property are interpreted.
Build mode / Modo diseñador
Un metodo aun más sencillo de editar un panel y controlar su diseño es el Modo de Diseño de VGUI. Este te permite modificar y guardar el diseño de un archivo de recursos de panel mientras la aplicación está ejecutandose. Para editar un panel, solo inicia el juego y haber el panel que quieras editar, de manera que adquiera el foco. Pulsa entonces SHIFT+CTRL+ALT+B
para abrir el editor del Modo de Diseño VGUI. En Modo Diseñador puedes, de forma sencilla, Build Mode you can easily reorganizar elementos existentes y cambiar sus propiedades de control (pulsa 'Apply' para actualizar los cambios). Para añadir elementos de control nuevos, solo elige la clase deseada del combo-box en la parte baja al lado izquierdo y un control vacio de esa clase será colocado en tu panel para editarlo posteriormente. Para guardar el diseño actual en un archivo de recursos asociado pulsa el botón 'Save' (asegurate de que el archivo de recursos no está protegido contra escritura).
Drawing & surfaces
Using schemes and control properties you can change the general appearance and layout of existing controls, but it doesn't allow you to create completely new elements. To change the appearance of a panel you have to override the two virtual functions Panel::Paint()
and Panel::PaintBackground()
. In these functions you can use drawing functions provided by the ISurface interface to place lines, rectangles, text, images etc. When updating the screen, VGUI calls first PaintBackground()
and then Paint()
for each panel and its children panels recursively. Drawing coordinates are relative to the panel you draw in. Here same example code how to draw a red box in the upper left corner:
void MyPanel::Paint(void) { BaseClass::Paint(); surface()->DrawSetColor( 255, 0, 0, 255 ); //RGBA surface()->DrawFilledRect( 0, 0, 20, 20 ); //x0,y0,x1,y1 }
To draw text on a panel surface you need to set the used font first. Fonts have names like "DefaultSmall" and their properties like True-Type-Font, size and weight are defined in scheme resource files. The font handle can be retrieved by calling GetFont("Name")
of the current panel scheme, then the surface can be told to use it as the current font. After setting the color and position for the next text output, the text itself must be passed as wide char string (Unicode) to DrawPrintText(...)
. This text will not be printed as it is and not localized anymore, so localization tokens must be translated beforehand.
void MyPanel::Paint(void) { wchar_t *pText = L"Hello world!"; // wide char text // get the right font handle for this scheme vgui::IScheme *pScheme = vgui::scheme()->GetIScheme(GetScheme()); vgui::HFont hFont = pScheme->GetFont( "DefaultSmall" ); surface()->DrawSetTextFont( hFont ); // set the font surface()->DrawSetTextColor( 255, 0, 0, 255 ); // full red surface()->DrawSetTextPos( 10, 10 ); // x,y position surface()->DrawPrintText( pText, wcslen(pText) ); // print text }
To draw a texture or image, VGUI has to load the texture from disk once (panel constructor) and generate a corresponding texture ID. This texture ID is then used as reference when drawing the texture. Here an example how to load and draw the texture "\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, "vgui/your_folder/mylogo" , true, false); } void MyPanel::Paint(void) { vgui::surface()->DrawSetTexture( m_nTextureID ); vgui::surface()->DrawSetColor(50,50,50,100); vgui::surface()->DrawTexturedRect( 0, 0, 100, 100 ); }
Proportionality
Coordinate values that you specify in configuration files are relative to a 640x480 resolution if SetProportional(true)
is called before a file is loaded (which, by default, it is). These values will be scaled to the correct size no matter what the user's current resolution may be. For example, a "tall" value of 120 will always mean the panel takes up 1/4th the height of the screen, and an "x" value of 320 means that the panel's left edge will always be centered.
c-<wide/tall>
in your xpos
and ypos
values to center an element on-screen at different aspect ratios.However, sometimes you will need to calculate coordinates for custom painting and forgo the luxury of configuration files. The problem is that values that you specify in code are relative to the user's native resolution, which may or may not be the same as the current game resolution (thus rendering surface()->GetScreenSize()
useless). The solution to this problem is to use scheme()->GetProportionalScaledValue()
. This allows you to specify a coordinate on the 640x480 resolution and have it be remapped to the current resolution.
Localization
VGUI text elements support automatic localization for the preferred user language. Lets assume a text label should show "Hello world!" then you could set this text directly with SetText( "Hello World.")
. But if the user would choose a different language than English this label would still show the English text. Therefore you should always use localization tokens to tell VGUI to translate the token into user language, in this example SetText( "#MyMod_HelloWorld")
. Tokens are strings starting with the hash sign '#' as control character to tell VGUI that this is not plain text.
VGUI keeps a global translation table to map tokens to plain text representation. These translation tables are loaded from resource files, where each file has an extra copy for every supported language (e.g. \resource\hl2_english.txt, \resource\hl2_german.txt
).
A new token definition for your game would look like this:
In mymod_english.txt:
"MyMod_HelloWorld" "Hello world."
In mymod_german.txt:
"MyMod_HelloWorld" "Hallo Welt." "[english]MyMod_HelloWorld" "Hello world."
If your game folder is named "mymod" the Source engine will automatically load the correct translation file (/resource/gamedir_language.txt
). You can load additional translation files using the ILocalize interface function AddFile(...)
.
You can use the ILocalize
interface also to manually translate a token into current user language, e.g. vgui::localize()->Find("#MyMod_HelloWorld")
. This function returns the translation as a 16-bit wide char string (Unicode).
VGUI uses Unicode for all text representation to support languages that use more then the 255 ASCII characters, like Chinese or Russian. Language resource files are encoded as Unicode. Note that the files will not be loaded properly unless they are specifically saved with proper Unicode encoding. To convert strings between ANSI ASCII and Unicode during runtime you can used the ILocalize
interface functions ConvertANSIToUnicode(...)
and ConvertUnicodeToANSI(...)
. Also a very useful function is ConstructString(...)
which is basically like sprintf(...) for Unicode strings.