Menus as VGUI elements
From GMod Wiki
Lua: Menus as VGUI elements |
Description: | Shows people how custom VGUI elements can be used |
Original Author: | Brian Nevec |
Contributors: | -UNC- TZScribblez™ |
Created: | 7th March, 2009 |
Updated: | 16th May, 2011 |
So you want to make, say, a gamemode, and you are hopping through other people's gamemodes to see how they work. Once you've made the bulk of your gamemode, you might want to add menus. So again, you are hopping through another gamemode to see how they did it. When you do come across menus, you don't see them splattered in a function which gets called by a console command; you see them bundled up nicely in a separate, custom VGUI element. So you think: "What the hell is this guy doing, and why is he doing it this way?". Well, for one, he's doing a very good thing.
"So what's so good about custom VGUI elements?"
- You get more direct control over what your menu does
- It's far more efficiently organized
- It's simpler to write and edit
"So... How can I do this?"
I am here to explain that. I expect you to have some basic knowledge in working with VGUI. If you don't, this page can help with that. This small article shows just what you can really do with custom VGUI elements.
We will be creating a simple name entry menu which has a text box and two buttons. Here's the outcome:
Basically, I will be adding code, function by function, and explaining what each does. So, here we have a brand-new, blank VGUI control with some of the custom functions we are going to use:
local PANEL = {}; function PANEL:Init() end function PANEL:Invalidate() end function PANEL:SetupEvents() end function PANEL:PerformLayout() end function PANEL:GetEntryValue() end function PANEL:SetEntryValue(Text) end function PANEL:EndEdit() end function PANEL:ApplyEdit() end vgui.Register("NameEntryMenu",PANEL,"DFrame");
In PANEL:Init(), we will be creating and setting up our controls. Here we create each individual VGUI control that will make up our menu. We also set any default variables we'll require later, and call any other functions needed to get the menu ready for display.
PANEL:Invalidate() will position and size our controls. The reason for this is that if you are adding new controls somewhere you don't have to copy the layout generation code. We can call this function at any time to ensure that our controls remain positioned and sized appropriately, which can be extremely important when dealing with menus that add, remove, resize, or reposition controls as the player uses the menu.
PANEL:SetupEvents() will set up events for our controls to respond to, and define their responses. Actions resulting from button presses, text box inputs, or slider movements are coded here for future use. This function is somewhat unnecessary, as anything coded here can probably go in PANEL:Init() instead, however it helps us keep our panel code organized, and makes it easier to change what functions are called from our controls.
PANEL:PerformLayout() is function that gets called when the control needs to be reformatted, such as when the menu is resized, or another function, PANEL:InvalidateLayout(), is called.* Since we have our own function for this, PANEL:Invalidate(), we will just call this function from BaseClass and then call the Invalidate function. Calling a function from BaseClass just means that instead of calling our custom-written function, we call the "parent's" function. The parent of our custom VGUI element has its own set of these functions, and using BaseClass, we can call those. This is important because sometimes we want to call the standard, generic version of a function that we custom-wrote, usually to say something to the effect of "do this and this, and then do what you'd normally do".
- Technically, you can just put the code from PANEL:Invalidate() in this function and call it instead. This means less lines of code, in exchange for less flexibility. By defining your own function you make it easier to change the way your controls are positioned and sized in the middle of gameplay.
PANEL:GetEntryValue() will get the text in our text box and allow us to use it in the rest of our code.
PANEL:SetEntryValue() lets us set what text is displayed in our text box.
PANEL:EndEdit() will just close the menu, nothing else.
PANEL:ApplyEdit() will look at the text in the text box, run the console command "say <text>", and close the menu. This is what our entire menu boils down to: This function outlines and performs the tasks our menu is designed to do.
local PANEL = {};
Create our panel object.
function PANEL:Init() local Width = 200; self:SetTitle("Enter your name"); local EntryPanel = vgui.Create("DPanel",self); EntryPanel:SetWide(Width); self.EntryPanel = EntryPanel; local TextEntry = vgui.Create("DTextEntry",EntryPanel); TextEntry:SetEnterAllowed(true); self.TextEntry = TextEntry; local ButtonPanel = vgui.Create("DPanel",self); ButtonPanel:SetWide(Width); self.ButtonPanel = ButtonPanel; local GoButton = vgui.Create("DButton",ButtonPanel); GoButton:SetText("Okay"); self.GoButton = GoButton; local CanButton = vgui.Create("DButton",ButtonPanel); CanButton:SetText("Cancel"); self.CanButton = CanButton; self:Invalidate(); self:SetupEvents(); self:SetEntryValue(LocalPlayer():Nick()); self:MakePopup(); self:Center(); end
This is our PANEL:Init() event. Here, we set the title of our form, create our controls, call self:Invalidate() to position and size our controls, call self:SetupEvents() to define what our buttons do, call self:SetEntryValue() to set the default value of the text box to our name, call self:MakePopup() to open the panel and give it focus. Notice that we are not creating the actual window; this is because the element we are basing this from is the window. Try to remember that these functions are run once our custom panel is displayed.
You may have noticed that inside each function, names like PANEL:Invalidate() become self:Invalidate(). This is because each function is run from within the PANEL object, and rather than use PANEL:Function(), we can use self:Function() because the PANEL object "knows" that "self" refers to itself. If that was too confusing, just remember that any function in the form OBJECT:Function() can, and should, use "self" to refer to "OBJECT".
function PANEL:Invalidate() self.EntryPanel:SetPos(5,23 + 5); self.EntryPanel:SetTall(5 + self.TextEntry:GetTall() + 5); self.TextEntry:SetPos(5,5); self.TextEntry:SetWide(self.EntryPanel:GetWide() - 5 - 5); local X,Y = self.EntryPanel:GetPos(); self.ButtonPanel:SetPos(5,Y + self.EntryPanel:GetTall() + 5); self.ButtonPanel:SetTall(5 + self.GoButton:GetTall() + 5 + self.CanButton:GetTall() + 5); self.GoButton:SetPos(5,5); self.GoButton:SetWide(self.ButtonPanel:GetWide() - 5 - 5); self.CanButton:SetPos(5,5 + self.GoButton:GetTall() + 5); self.CanButton:SetWide(self.ButtonPanel:GetWide() - 5 - 5); local X,Y = self.ButtonPanel:GetPos(); self:SetWide(self.EntryPanel:GetWide() + 5 + 5); self:SetTall(Y + self.ButtonPanel:GetTall() + 5); end
This is our PANEL:Invalidate() function. Basically, it positions and sizes every child control and then positions and sizes the parent form. You want to make this as dynamic as possible, so you don't need to change values around when you call this function. Notice how each control is positioned according to the positions and sizes of other controls. Functions like GetWide() and GetTall() return the width and height of a control. We can then use these values to ensure that our controls don't overlap and are spaced appropriately. It's important to use this method to define your positions and sizes, rather than just using static numbers, because it allows the menu contents to scale with the window size. Although this is not as important if the window size also remains constant, it is important to understand in order to size your windows and controls appropriately for players using a different screen resolution from your own.
All the "5"s you see in this code are used to insert some space between controls. You can use a variable instead to allow you to easily change the spacing between controls.
function PANEL:SetupEvents() local Form = self; local TextEntry = self.TextEntry; function self.GoButton:DoClick() TextEntry:OnEnter(); end function self.TextEntry:OnEnter() Form:ApplyEdit(); end function self.CanButton:DoClick() Form:EndEdit(); end end
This is the PANEL:SetupEvents() function. Why do we localize the form? Because the OnEnter() and DoClick() functions aren't part of our PANEL object, they're parts of our child controls. If we use "self" inside these functions, it will refer to the child control instead of our PANEL. This is a problem because ApplyEdit() and EndEdit() are not defined for the child controls, and we will get an error if we try to call those functions from within them. Since "Form" is defined at the beginning to refer to our PANEL object (because it is equal to "self", which refers to the PANEL at that point), we instead use it to call ApplyEdit() and EndEdit(), to ensure that they are called from the PANEL object.
function PANEL:PerformLayout() self.BaseClass.PerformLayout(self); self:Invalidate(); end
This is PANEL:PerformLayout(). First we call the "BaseClass" version of PerformLayout(), which, if you were paying attention earlier, you'll remember just means that we're telling the panel to do whatever panels normally do when PerformLayout() is called. Second, we call our Invalidate() function, to resize and reposition our objects.
function PANEL:GetEntryValue() return self.TextEntry:GetValue(); end function PANEL:SetEntryValue(Text) self.TextEntry:SetValue(Text); end
These functions are PANEL:Get/SetEntryValue(). PANEL:GetEntryValue() basically just forwards the results of the GetValue() function from the TextEntry control, allowing us to read from the text box without having to refer to the TextEntry control each time. So later, we can use self:GetEntryValue() instead of self.TextEntry:GetValue(), which is quite a bit nicer to write and easier to read.
PANEL:SetEntryValue( Text ) forwards Text to the TextEntry control's SetValue() function. This allows us to set the text in the text box without referring to TextEntry each time, just like the previous function did. This changes self.TextEntry:SetValue( Text ) into the much nicer self:SetEntryValue( text ).
function PANEL:EndEdit() self:Close(); end
PANEL:EndEdit() just closes the form, nothing more, nothing less.
function PANEL:ApplyEdit() local Text = self:GetEntryValue(); if Text && #Text > 0 then RunConsoleCommand("say",Text); self:EndEdit(); end end
This is PANEL:ApplyEdit(). It checks if we have valid text in the text box, and then runs the console command "say" with the argument Text and closes the form. This basically makes the player say whatever's in the text box and closes the form.
vgui.Register("NameEntryMenu",PANEL,"DFrame");
Finally, we register our new element. We'll name it "NameEntryMenu" and base it on DFrame, the standard Derma "window", so that when the panel is displayed, it looks like a DFrame with all our customizations applied, including our child controls and all our functions.
local function OpenMenu() vgui.Create("NameEntryMenu"); end concommand.Add("openentrymenu",OpenMenu);
This is just a simple console command to create the menu.
The code came out fairly long but the line count doesn't really matter. If you ignore my style of large amounts of whitespaces it will come out shorter for you, but may be harder to read.
Hopefully this article taught you something new.
Goodbye!