Create GUI entirely from script

Started by CTxCB, Mon 06/02/2017 18:51:26

Previous topic - Next topic

CTxCB

I need a way to create and skin a GUI from script, this is because I need one that can be any size, with any title and any text in it. Width, and Height, Title and Contents would be made on the spot. Having a default GUI doesn't fit what I'd need at all.

Crimson Wizard

#1
AGS does not support creating new GUI in script, but you can modify existing GUI, which is what we usually do.

In the Editor you create some kind of template, with the maximal amount of content that may appear on it in game (labels, buttons, and so on). Then, when you are about to display that GUI on screen in script you use GUI properties to modify its looks, define which elements are visible, and so on.

I recommend checking articles like "GUI functions and properties" and "GUIControl functions and properties" in the manual to see the full list of parameters you may set in script.

To begin with some example, I'll use same code I gave some time ago answering your question in another thread.

Consider you want GUI with a label centered on screen. Create a template GUI in the Editor of a smaller size with just one label on it, then write following function in your global script (GlobalScript.asc):
Code: ags

// Assuming your GUI is called gMyGUI and the label is called lblLabel1
//
function DisplayMyGUI(String new_text)
{
    lblLabel1.Text= new_text;
    int text_width = GetTextWidth(lblLabel1.Text, lblLabel1.Font);
    // First, ensure label and GUI (where label is placed) are wide enough
    if (lblLabel1.Width < text_width)
        lblLabel1.Width = text_width + 4; // just to add little margin
    if (gMyGUI.Width < lblLabel1.Width)
        gMyGUI.Width = lblLabel1.Width + 4; // just to add little padding
    // Now center label on GUI and GUI on screen
    lblLabel1.X = (gMyGUI.Width - lblLabel1.Width) / 2;
    gMyGUI.X = (System.ViewportWidth - gMyGUI.Width) / 2;
    gMyGUI.Visible = true; // display GUI on screen
}


Declare this function in script header (GlobalScript.ash):
Code: ags

import function DisplayMyGUI(String new_text);


Now you can call it from either GlobalScript or any room scripts, like
Code: ags

DisplayMyGUI("Hello world, I am resizable GUI");


Following same principles, you may add more setup to the GUI.

Crimson Wizard

Another example I may give is about customizing buttons.

Let's assume that you need a GUI for displaying some message, and up to 3 buttons (Ok & Cancel, or Yes/No/Cancel, and so forth).

In the Editor create a GUI called gMessage, having a label lblMessage, and 3 buttons on it, called btnBut1, btnBut2 and btnBut3.

Now, you may write couple of functions for displaying this GUI in different ways:
Code: ags

function DisplayMessageOKCancel(String text)
{
   lblMessage.Text = text;
   btnBut1.Text = "OK";
   btnBut2.Text = "Cancel";
   btnBut1.Visible = true;
   btnBut2.Visible = true;
   btnBut3.Visible = false;
   gMessage.Visible = true; // display a GUI on screen
}

function DisplayMessageYesNoCancel(String text)
{
   lblMessage.Text = text;
   btnBut1.Text = "Yes";
   btnBut2.Text = "No";
   btnBut3.Text = "Cancel";
   btnBut1.Visible = true;
   btnBut2.Visible = true;
   btnBut3.Visible = true;
   gMessage.Visible = true; // display a GUI on screen
}


Of course, you would need to choose corresponding behavior when such buttons are pressed. You would need to somehow check which mode the GUI is shown in.
Assuming you have created an event handler for btnBut1 called btnBut1_OnClick:
Code: ags

function btnBut1_OnClick(GUIControl *control, MouseButton button)
{
   // Check the text on button 1 to decide which action should be done
   if (btnBut1.Text == "OK")
   {
      // proceed with OK action
   }
   else if (btnBut1.Text == "Yes")
   {
      // proceed with Yes action
   }
}


On the other hand, you may need to remember more custom parameters, like the reason GUI was opened, or question to player it is related to. In such case you would need to create your own variables in script to store this information, and check them in button event handlers to know what to do.

CTxCB

Thanks for all this -- It's making this task look a lot simpler now. Two concerns I have, though: A) Would changing it's (the GUI's) size cut off part of the GUI Skin I've made? B) If so, would it scale it, because that might make it look bad... Ideally, I'd prefer something like what a Text Window GUI does where it draws the sprites as is on the screen, I already have all the sprites needed for something like that.

Crimson Wizard

#4
Quote from: CTxCB on Mon 06/02/2017 22:42:02
Two concerns I have, though: A) Would changing it's (the GUI's) size cut off part of the GUI Skin I've made? B) If so, would it scale it, because that might make it look bad... Ideally, I'd prefer something like what a Text Window GUI does where it draws the sprites as is on the screen, I already have all the sprites needed for something like that.

It will cut it, won't rescale. But if you like to have something like TextWindow, you'd have to script drawing tiles on GUI background. That is more scripting, but it's not too difficult either. One important thing is that you will have to learn how to work with DynamicSprite and DrawingSurface objects.

DynamicSprite is a sprite created at runtime, which you can modify right in script ("normal" sprites cannot be altered). You can use it as a GUI background and draw your window component on it.
DynamicSprite should be a global variable, because it needs to exist as long as GUI is on screen (at least), or the game will crash.

The general idea is that when you need to show GUI (at the point when you already know its new size) you will need to create a sprite, keep its pointer in a variable, draw your window sprites on it and attach that sprite to the GUI background.

Quick example:
Code: ags

DynamicSprite *MessageGUIBkg; // declared somewhere at the top of the GlobalScript.asc

function DisplayMyGUI()
{

    <...> // some other calculations and operations go here

    MessageGUIBkg = DynamicSprite.Create(gMyGUI.Width, gMyGUI.Height); // create a big sprite same size as GUI
    DrawingSurface *ds = MessageGUIBkg.GetDrawingSurface(); // get the drawing surface object to draw upon the sprite

    // For example, you need to draw border tiles along the top border, and your tile sprite index is #100
    int border_spr = 100; // remember border sprite index in a variable, for readability
    int x = 0;
    while (x < gMyGUI.Width)
    {
        ds.DrawImage(x, 0, border_spr);
        x += Game.SpriteWidth[border_spr]; // advance by tile's width
    }

    ds.Release(); // do not forget to call Release on DrawingSurface, or its changes may get lost!

    gMyGUI.BackgroundGraphic = MessageGUIBkg.Graphic; // assign our big sprite as a new GUI's background
}


CTxCB

Quote from: Crimson Wizard on Mon 06/02/2017 23:01:54
Quote from: CTxCB on Mon 06/02/2017 22:42:02
Two concerns I have, though: A) Would changing it's (the GUI's) size cut off part of the GUI Skin I've made? B) If so, would it scale it, because that might make it look bad... Ideally, I'd prefer something like what a Text Window GUI does where it draws the sprites as is on the screen, I already have all the sprites needed for something like that.

It will cut it, won't rescale. But if you like to have something like TextWindow, you'd have to script drawing tiles on GUI background. That is more scripting, but it's not too difficult either. One important thing is that you will have to learn how to work with DynamicSprite and DrawingSurface objects.

DynamicSprite is a sprite created at runtime, which you can modify right in script ("normal" sprites cannot be altered). You can use it as a GUI background and draw your window component on it.
DynamicSprite should be a global variable, because it needs to exist as long as GUI is on screen (at least), or the game will crash.

The general idea is that when you need to show GUI (at the point when you already know its new size) you will need to create a sprite, keep its pointer in a variable, draw your window sprites on it and attach that sprite to the GUI background.

Quick example:
Code: ags

DynamicSprite *MessageGUIBkg; // declared somewhere at the top of the GlobalScript.asc

function DisplayMyGUI()
{

    <...> // some other calculations and operations go here

    MessageGUIBkg = DynamicSprite.Create(gMyGUI.Width, gMyGUI.Height); // create a big sprite same size as GUI
    DrawingSurface *ds = MessageGUIBkg.GetDrawingSurface(); // get the drawing surface object to draw upon the sprite

    // For example, you need to draw border tiles along the top border, and your tile sprite index is #100
    int border_spr = 100; // remember border sprite index in a variable, for readability
    int x = 0;
    while (x < gMyGUI.Width)
    {
        ds.DrawImage(x, 0, border_spr);
        x += Game.SpriteWidth[border_spr]; // advance by tile's width
    }

    ds.Release(); // do not forget to call Release on DrawingSurface, or its changes may get lost!

    gMyGUI.BackgroundGraphic = MessageGUIBkg.Graphic; // assign our big sprite as a new GUI's background
}


Would this also work with the buttons too? Seeing as they'll be different sizes depending on text and stuff.

Crimson Wizard

Quote from: CTxCB on Tue 07/02/2017 00:53:15
Would this also work with the buttons too? Seeing as they'll be different sizes depending on text and stuff.
Yes, absolutely, except for button you need to set NormalGraphic property.

CTxCB

Quote from: Crimson Wizard on Tue 07/02/2017 08:29:53
Quote from: CTxCB on Tue 07/02/2017 00:53:15
Would this also work with the buttons too? Seeing as they'll be different sizes depending on text and stuff.
Yes, absolutely, except for button you need to set NormalGraphic property.
A little related to this, I'm making a helper function to make creating everything a lot easier. I've got through most of the function parameter definitions myself, since they're either int, or String, but for the text of the buttons, I want to send that in a table, an array, whatever's best for sending multiple variables at once within one parameter to make traversing and doing the job a lot more efficient. How would I define that as a parameter?

Crimson Wizard

#8
As far as I remember you can only pass dynamic arrays, the ones created with new command. The syntax is:
Code: ags

function f(String arr_of_ints[], int arr_len)

usage:
Code: ags

String texts[] = new String[10];
<...>
f(texts, 10);


There is pretty annoying thing that there is no command to get array length, so you need to pass its length along.

CTxCB

Code: ags
import function DisplayAdvancedTextWindow(int width, int height, int offsetX, int offsetY, String offsetXType, String offsetYType, String title, String contents, int buttonCount, String buttonType[], int buttonTypeCount, String buttonText[], int buttonTextCount, String buttonCallback[], int buttonCallbackCount)

I'm trying to do that in my GlobalScript Header, so it can be referenced by rooms and everything without having to replicate the same code in hundreds of rooms but I get the error "Failed to save room room999.crm; details below - GlobalScript.ash(5): Error (line 5): too many parameters defined for function" despite the fact that the above matches the amount of parameters which the function has. Is there some limit I've reached? The worrying this is that I probably can't decrease the number of parameters by much.

Crimson Wizard

#10
I may only suggest to define a struct to set up parameters, and possibly functions with less parameters which set up this struct for every typical situation (like known combination of buttons, as I mentioned few posts above).

In the past I met similar situation when there were too many parameters to pass (this is not only technical problem, but also big inconvenience to type them all the time), so I ended scripting a preset system, where you first save number of presets, describing certain aspects of an object or action, and then start that action passing index of preset + few optional parameters.

Since AGS 3.4.0 script also support creation of your own managed structs that you can pass by pointer, but at the moment they are restricted only to having non-pointer types inside, i.e. ints, floats and bools. You may use that too, example with pseudo code
Code: ags

managed struct WindowPos
{
    int width;
    int height;
    int offsetX;
    int offsetY;
};


function DisplayAdvancedWindow(WindowPos *wndpos, .....);


WindowsPos wndpos = new WindosPos
wndpos.width = 100;
wndpos.height = 100;
DisplayAdvancedWindow(wndpos, ... other params);


BTW, I do not know what offsetXType is, but if you could use enumeration instead of string there, they could also be included in such managed struct.

CTxCB

Quote from: Crimson Wizard on Tue 07/02/2017 12:03:43
BTW, I do not know what offsetXType is, but if you could use enumeration instead of string there, they could also be included in such managed struct.
offsetXType and offsetYType are either "Pos" meaning Positive, or "Neg" meaning Negative, and using an if statement, it decides whether the offset should be applied going Postively (E.g: To the right of X or Down from Y) or Negatively (E.g: To the left of X or Up from Y).

Crimson Wizard

Quote from: CTxCB on Tue 07/02/2017 12:21:41
offsetXType and offsetYType are either "Pos" meaning Positive, or "Neg" meaning Negative, and using an if statement, it decides whether the offset should be applied going Postively (E.g: To the right of X or Down from Y) or Negatively (E.g: To the left of X or Up from Y).
Hmm, could not you just use negative values for negative offsets? That would save 2 extra parameters.

CTxCB

Quote from: Crimson Wizard on Tue 07/02/2017 12:58:49
Quote from: CTxCB on Tue 07/02/2017 12:21:41
offsetXType and offsetYType are either "Pos" meaning Positive, or "Neg" meaning Negative, and using an if statement, it decides whether the offset should be applied going Postively (E.g: To the right of X or Down from Y) or Negatively (E.g: To the left of X or Up from Y).
Hmm, could not you just use negative values for negative offsets? That would save 2 extra parameters.
I think I'm going to keep the current structure and just remove the height as an argument. Since I want that to be dynamic by taking the fixed elements on the window and adding the height of the label (multiple lines), how can I calculate and wrap lines of text in a label?

Crimson Wizard

Label wraps lines automatically.
Along with GetTextWidth there is GetTextHeight function that would tell what the text's height will be given particular width, which may be used to set up label's height somehow.

CTxCB

Quote from: Crimson Wizard on Tue 07/02/2017 13:50:18
Label wraps lines automatically.
Along with GetTextWidth there is GetTextHeight function that would tell what the text's height will be given particular width, which may be used to set up label's height somehow.
Final question before I'm ready to do this entirely. I want to take the text from a button, calculate it's width in pixels and add 8 pixels to that, to create the "perfect" button width with padding. How can I do this?

Crimson Wizard

Quote from: CTxCB on Tue 07/02/2017 14:04:23
Final question before I'm ready to do this entirely. I want to take the text from a button, calculate it's width in pixels and add 8 pixels to that, to create the "perfect" button width with padding. How can I do this?

Using exactly same principles as I mentioned in the first reply:
Code: ags

int width = GetTextWidth(MyBtn.Text, MyBtn.Font) + 8;

SMF spam blocked by CleanTalk