VGUI文档

From Valve Developer Community
< Zh
Jump to: navigation, search
English (en)Español (es)Polski (pl)Русский (ru)中文 (zh)Translate (Translate)
Info content.png
This page needs to be translated.
This page either contains information that is only partially or incorrectly translated, or there isn't a translation yet.
If this page cannot be translated for some reason, or is left untranslated for an extended period of time after this notice is posted, the page should be requested to be deleted.
Also, please make sure the article tries to comply with the alternate languages guide.

Category:VGUI(en) 是VALVE专有的 图形用户界面GUI. 所有起源游戏和steam都使用vgui绘制窗口,提示以及菜单。 它同样处理 本地化: 根据用户的语言而显示相应的语言内容。

  • 所有对象都是分层次的,且都派生自vgui基类。
  • 键鼠输入都是事件驱动的,和其他gui一样。
  • 图片按钮文本都是由vgui控件提供。 (vgui_controls.lib).
  • 基础接口头文件定义在 \public\vgui\
  • 控件定义在 \public\vgui_controls\.

作为一个mod作者,你将会经常使用client.dll项目中的vgui方案来显示菜单,HUD元素或者in-game显示物品(如武器或者电脑终端等)。

面板和结构

所有基础vgui元素都派生自vgui::Panel基类, 一个定义在你游戏内的gui大小和方位的类,可以绘制和作用事件. 对话窗口, 文本框和按钮都是VGUI里面的亲类。 最先的面板是根面板(root panel),由引擎提供. 在客户端上这个面板会覆盖整个游戏区域, 但是它不会显示. 即使你可以用root panel,大多数客户端panel都共享 BaseViewport面板作为父类( g_pClientMode->GetViewport() ).

This diagram shows the hierarchy of VGUI panels in Counter-Strike:

VGUI client panel hierarchy

You can inspect your mod's panel hierarchy with the VGUI Hierarchy tool. Open it by submitting vgui_drawtree 1 to the developer console(en).

VGUI Hierarchy tool

Panel names are color-coded:

  • Visible
  • Hidden
  • Popup panel (Frame)
  • Has focus

And the following options are available as checkboxes:

Show Visible
List all visible panels
Show Hidden
List all hidden panels
Popups only
List only Popup panels (Frames)
Highlight Mouse Over
Panels are highlighted with a colored border when you move the mouse cursor over. The panel tree will be expanded to show the current panel.
Freeze
Locks current tree view
Show Addresses
Shows panel memory address
Show Alpha
Show panel alpha value, 0 = translucent, 255 = opaque
In Render Order
Sort panels in tree by rendering order

面板类

The VGUI base class Panel is derived from by over 50 different control elements defined in \public\vgui_controls\. The header files usually provide a good documentation about the control features and behaviors. The following diagram shows the class hierarchy for some common used elements:

Panel
EditablePanel Label TextEntry RichText ImagePanel
Frame Button ComboBox
MessageBox ToggleButton

Since all VGUI classes derive from class Panel, we should take a closer look at the essential member functions and properties.

The most commonly used constructor to create a panel is Panel(Panel* parent, const char* panelName), since each panel needs a parent panel it belongs to and a unique name in its hierarchy level. Once a panel is created you can make it visible or hide it again with SetVisible(bool state).

VGUI keeps a handle for each new panel, which is used to globally address a panel without passing pointers. Lots of event messaging functions use these VPANEL handles to identify source and target panels. To get the unique VPANEL handle of a panel call GetVPanel().

To set a panel's position and size use the SetPos(int x,int y) and SetSize(int wide,int tall) member functions. The size is always in pixels and doesn't scale automatically with increasing screen resolutions. The position is always relative to the given parent panel, where (0,0) is the upper left corner.

To iterate through the panel hierarchy using a certain panel as starting point, you get the parent panel by calling GetParent() or GetVParent() to get the corresponding VPANEL handle. Of course a panel can have only one parent panel, but multiple child panels (sub panels, buttons, list elements etc). GetChildCount() will return the total number of children, where each child can be queried with GetChild(int index).

If a panel should react to input events like key pressed or mouse click, the virtual OnEvent() functions like OnMousePressed(MouseCode code) or OnKeyCodePressed(KeyCode code) must be overridden. Mouse and keyboard input events can be enabled/disabled for a panel using SetMouseInputEnabled(bool state) or SetKeyBoardInputEnabled(bool state).

Another useful virtual function to override is OnThink() which is called every time the panel is redrawn. Sometimes this is too much and you want update or check the panel state less often. Then you can hook into OnTick() which is called frequently in an adjustable time interval. But VGUI must be told that it should call OnTick() for a given panel and how often, this is done with ivgui()->AddTickSignal(VPANEL panel, int intervalMilliseconds).

Windows & popups

A Panel as described above is always a 100% sub area of its parent panel, its position is always a fixed relative offset and can only be changed by code. Also it draws itself only inside the parent region, if you move a VGUI element outside the parent panel, it will be clipped. This is fine for adding control elements, images or text fields to a panel, but wouldn't allow independent pop-up windows or dialog boxes the user can move around, resize and minimize. Therefore, VGUI panels have the MakePopup() function which decouples a panel from is parent rendering and makes it a new, independent window. Still it belongs to a parent panel and becomes invisible if the parent does.

In most cases you won't call MakePopup() by yourself but use the Frame class instead. The Frame class encapsulates all common GUI windows features like title bar, system menu (close, minimize, maximize), dragging, resizing, focus and layout management.

Here a short example how to open a frame and activate it. Note that the frame will delete itself once the user closes the Frame or the Close() function is called (just make sure that your stuff is cleaned up in the 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

If a Frame is resized, the virtual function PerformLayout() is called so the frame can rearrange its elements to fit best for the new size. The current Frame position and size can also be locked with SetMoveable(bool state) and SetSizeable(bool state).

Event messaging

VGUI panels communicate via a message system to signal state changes or events to parents or children (or any other panel). Messages are not sent directly (e.g. by calling a panel listener function), rather they are handed over to VGUI which delivers them to the target panel. Thus beside the message content, a sender and a receiver panel must be specified as VPANEL handles. VGUI sends event messages to inform panels about changes or events (mouse moved, input focus changed etc).

The message name and content is specified using a KeyValues(en) object. The KeyValues class is a very generic and flexible structure to store data records containing strings, integer or float numbers. 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");
	}
}

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. It's also possible to preview the fonts, borders and colors of a scheme by using the showschemevisualizer command, with an argument matching the name of your scheme, in the console. 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"	// display this unlocalized 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

VGUI Build Mode panel

An even simpler way to edit panel and control layouts is the VGUI Build Mode. This allows you to modify and save the layout of a panel resource file while the application is running. To edit a panel, just start the game and open this panel, so it gets the input focus. Then press SHIFT+CTRL+ALT+B to open the VGUI Build Mode editor. In Build Mode you can easily rearrange existing elements and change their control properties (press 'Apply' to update changes). To add a new control element, just choose the desired class from the combo-box on the lower right side and an empty control object of that class will be place in your panel for further editing. To save the current layout in the associated resource file press the 'Save' button (make sure the resource file is not write-protected).

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(en) 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.

Tip.png提示:Use 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).

Tip.png提示:Localization files must be stored in the UCS2-Little Endian format. You may need an advanced text editor like Notepad++ to do this or you can use VGUI Localization Tool(en).

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. g_pVGuiLocalize->Find("#MyMod_HelloWorld") (or vgui::localize()->Find("#MyMod_HelloWorld") in previous versions). 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.

See also

Tutorials