Using on-chart GUI controls

Basics

AmiBroker 6.30 introduces the ability to create and utilize user-definable GUI controls such as buttons, checkboxes, radio buttons, edit fields, date/time pickers, sliders, etc., which can be placed on a chart and interacted with.

Creation of GUI controls

On-chart GUI controls can be created by calls to appropriate functions, such as: GuiButton, GuiEdit, GuiToggle, GuiRadio, GuiDateTime, GuiSlider.

All UI creation functions (GuiButton, GuiEdit, GuiToggle, GuiRadio, GuiDateTime, GuiSlider) take a control ID as a parameter. A control ID is a unique identifier used to distinguish between controls and to manipulate a given control. The control ID must be an integer number greater than zero. As the code below shows, it is a good idea to assign meaningful names to control IDs and use them instead of plain numbers.

The following example shows how to create a button:

// control IDs
idMyButton = 2;

GuiButton( "MyButton", idMyButton, 10, 20, 100, 20, notifyClicked );

The operation model of on-chart GUI controls is such that controls can be destroyed at any time when, for example, a user switches to a different chart sheet. For this reason, the formula needs to be able to recreate UI controls if needed. This is done by simply calling creation functions each time the formula is run. The creation functions are intelligent enough not to create duplicates.

If a control with a given ID already exists, its type will be checked. If an existing control of the same ID and type is found, nothing is done, and the GUI creation function returns a guiExisting value. If a control exists but has a different type, it will be destroyed and a control of the new type will be created. If a control with a given ID does not exist, it will be created. In such cases, the creation function will return guiNew. The code below shows how to check the return value:

// control IDs
idMyButton = 2;

rc = GuiButton( "MyButton", idMyButton, 10, 20, 100, 20, notifyClicked );

if( rc == guiNew ) _TRACE("Control just created");
if( rc == guiExisting ) _TRACE("Control already existing");
    

This is not really needed for buttons that don't require initialization, but it is useful when you would like to set up initial values for controls only when they are created. The following example shows how to set initial text for an edit field:

// control IDs
idEdit = 1;

function CreateGUI()
{
   rc = GuiEdit( idEdit, 10, 20, 100, 20, notifyEditChange );
  
   if( rc == guiNew ) GuiSetText("Initial text", idEdit );
  
}   

CreateGUI();

Handling events / UI notifications

GUI controls are created to allow the user to interact with them. When this happens (for example, when a user clicks on a button), an event is created and the formula gets executed again. Several controls may trigger events on various occasions, for example, when clicking on them, editing text, gaining or losing focus, etc. To decide whether a given control triggers events or not, we use the notify parameter in the GUI creation functions. For example:

GuiButton( "MyButton", idMyButton, 10, 20, 100, 20, notifyClicked );

will create a button that will trigger an event when it is clicked. We can combine various flags using | (binary OR operator) if we want to be notified about many different events by a given control. For example, the code below will create an edit control that will notify us when its contents are changed and when the edit field loses focus:

GuiEdit( idEdit, 10, 20, 100, 20, notifyEditChange | notifyKillFocus );

There are many possible notifications including:

When an event is triggered, our formula is executed again. If multiple events occur, they are placed in the queue. To handle such UI events, the formula should call GuiGetEvent() function to find out if there are any events in the queue and to handle them appropriately.

The GuiGetEvent( num, what = 0 ) function has two parameters:

The num parameter defines the index of a notification in the queue. 0 is the first received (or "oldest") since the last execution. The second parameter, what, defines what value the GuiGetEvent function returns:

Usually, there is zero (when formula execution was not triggered by a UI event) or one event in the queue but note that there can be more than one event notification in the queue if your formula is slow to process. To retrieve them all, use an increasing "num" parameter as long as GuiGetEvent does not return zero (in `what=0` or `what=1` mode) or an empty string (`what=2`).

// read all pending events
for( i = 0; id = GuiGetEvent( i ); i++ )
{
    code = GuiGetEvent( i, 1 );
    text = GuiGetEvent( i, 2 );
}

It is good practice to have a separate function that handles events, as shown below:

idMyFirstButton = 1;
idMySecondButton = 2;

function CreateGUI()
{
     GuiButton( "enable", idMyFirstButton, 10, 60, 100, 30, notifyClicked );
     GuiButton( "disable", idMySecondButton, 110, 60, 100, 30, notifyClicked );
}

function HandleEvents()
{
    for ( n = 0; id = GuiGetEvent( n, 0 ); n++ ) // get the id of the event
    {
         code = GuiGetEvent( n, 1 );

         switch ( id )
         {
             case idMyFirstButton:
             // do something
                break;

             case idMySecondButton:
             // do something else
                break;

             default:
                 break;
         }
     }
}

CreateGUI();

HandleEvents();

Manipulating UI controls

Controls can be manipulated in a number of ways:

All controls can be hidden and shown again using GuiSetVisible() function:

GuiSetVisible( id, False ); // hides the control
GuiSetVisible( id, True ); // shows the control

All controls can be disabled (so they don't react to user input and become 'grayed') and enabled again using GuiEnable() function:

GuiEnable( id, False ); // disables the control
GuiEnable( id, True ); // enables the control

You can set the font for all GUI controls using the GuiSetFont call:

GuiSetFont("Tahoma", 13 );

Buttons (like all other controls) by default use system colors. But buttons can have their own custom colors defined using GuiSetColors (other controls only use system colors).

GuiButton( "First", idFirst, 0, 50, 200, 30, 7 );
GuiButton( "Second", idSecond, 200, 50, 200, 30, 7 );
GuiButton( "Third", idThird, 400, 50, 200, 30, 7 );
// two first buttons will have red text, border and black background
GuiSetColors( idFirst, idSecond, 2, colorRed, colorBlack, colorRed );
// and the third green text, border and blue background
GuiSetColors( idThird, idThird, 2, colorYellow, colorBlue, colorYellow );

In this simple example above, we are just setting the normal color of buttons, but they also have separate colors for different states (selected, hovered) and these can be set too.

All controls can have their text updated via GuiSetText and retrieved using GuiGetText.

// control IDs
idEdit = 1;

function CreateGUI()
{
   rc = GuiEdit( idEdit, 10, 20, 100, 20, notifyEditChange );
  
   if( rc == guiNew ) GuiSetText("Initial text", idEdit );
  
}   

If you prefer the numeric value of text in the control you can use GuiSetValue()/GuiGetValue() pair (especially useful for sliders to get their position). Sliders can also have their min-max range defined using GuiSetRange() as shown in the example below.

idSlider = 1;

if ( GuiSlider( idSlider, 10, 30, 200, 30, notifyEditChange ) == guiNew )
{
   // init values on control creation
   GuiSetValue( idSlider, 5 );
   GuiSetRange( idSlider, 1, 100, 0.1, 100 );
}

Preserving the state of controls

Basic controls such as push buttons don't have 'permanent' state. They are only used to trigger some actions when a user pushes the button. But some controls have their state (additional information stored in them). For example, the text entered in an edit field, the position of the slider, the date picked in the date/time control, checkbox state, etc.

Each control (including on-chart controls) in Windows OS is an actual "window" object. Controls (window objects in general) preserve their state on their own as long as they "live", i.e., as long as they exist as "window objects", which means as long as a given chart pane is displayed.

So as long as you don't care about preserving state when a chart is not displayed, you don't need to do anything. Otherwise, follow the steps below.

The thing is that on-chart GUI controls are dynamically created by your code when the formula is executed. They can be automatically destroyed if you close a chart window or if you switch to another sheet. Then they can be recreated next time the formula executes.

This fact has one consequence: if you want to keep your control state between "destroy" and "recreation", you have to store the state yourself. This can be done using, for example, static variables (or permanent static variables if you want to keep the state between AmiBroker runs).

As discussed earlier, control creation functions such as GuiButton, GuiEdit, GuiDateTime, GuiCheckbox, GuiRadio, GuiSlider, etc., all return guiNew value if a control is newly created or guiExisting if a given control (window object) already exists. This allows us to know when we need to restore a previously saved state. For example, assuming that we have an edit control's text saved in the "mySavedText" static variable, we could restore the state using code such as the one below:

// control IDs
idEdit = 1;

function CreateGUI()
{
     rc = GuiEdit( idEdit, 10, 20, 100, 20, notifyEditChange );

     if( rc == guiNew ) GuiSetText( StaticVarGetText( "mySavedText" ) , idEdit );

}�

Now we may also want to store the text in a static variable whenever it changes:

function HandleEvents()
{
     for( n = 0; id = GuiGetEvent( n, 0 ); n++ )
     {
         switch( id )
         {
             case idEdit:
                 StaticVarSetText( "mySavedText", GuiGetText( idEdit ), True /* make it persistent */ );
                 break;
                 /// the rest of the event handling code ....
        }
     }
}

As we store the value of edited text on each change into a persistent static variable, it will be preserved between AmiBroker runs and restored whenever the edit control is (re-)created.

The same technique can be used for other controls as well.

Examples

The example code below shows many techniques described above, including setting initial values on creation, modification of control properties, enabling/disabling controls, showing/hiding controls, handling GUI events.

// control identifiers
idSlider = 1;
idEnable = 2;
idDisable = 3;
idShow = 4;
idHide = 5;

function CreateGUI()
{
     if ( GuiSlider( idSlider, 10, 30, 200, 30, notifyEditChange ) == guiNew )
     {
        // init values on control creation
        GuiSetValue( idSlider, 5 );
        GuiSetRange( idSlider, 1, 100, 0.1, 100 );
     }

     GuiButton( "enable", idEnable, 10, 60, 100, 30, notifyClicked );
     GuiButton( "disable", idDisable, 110, 60, 100, 30, notifyClicked );
     GuiButton( "show", idShow, 10, 90, 100, 30, notifyClicked );
     GuiButton( "hide", idHide, 110, 90, 100, 30, notifyClicked );
}


function HandleEvents()
{
    for( n = 0; id = GuiGetEvent( n, 0 ); n++ )
    {
       switch ( id )
       {
          case idEnable:
             GuiEnable( idSlider, True );
             break;

          case idDisable:
             GuiEnable( idSlider, False );
             break;

          case idShow:
             GuiSetVisible( idSlider, True );
             break;

          case idHide:
             GuiSetVisible( idSlider, False );
             break;
       }
    }
}

CreateGUI();
HandleEvents();

Title = "Value = " + GuiGetValue( idSlider );



Caveats

Please be reasonable with Gui* functions and be aware of Windows limits. As all controls in Windows (buttons, edit boxes, etc.) are actual Window objects, they are subject to Windows limitations. And there is a limit of 10000 windows PER PROCESS. So don't try to create thousands of buttons (like a button for every bar of data) because, first, you will see a huge performance decrease, and next, you will hit the limit and run into problems (e.g., a crash)
https://blogs.msdn.microsoft.com/oldnewthing/20070718-00/?p=25963

The best practice is to keep the number under 100-200. If you need more, consider using low-level graphics instead.