Using on-chart GUI controls

Basics

AmiBroker 6.30 brings up ability to create and utilise user-definable GUI controls such as buttons, checkboxes, radio buttons, edit fields, date time pickers, sliders, etc that can be placed on chart and interacted with.

Creation of GUI controls

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

All UI creation functions (GuiButton, GuiEdit, GuiToggle, GuiRadio, GuiDateTime, GuiSlider) take control ID as parameter. Control ID is a unique identifier that is used to distinguish between controls and to manipulate given control. Control ID has to integer number greater than zero. As the code below shows, it is good idea to assign some meaningful names to control IDs and 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 destoroyed at any time when for example user switches to different chart sheet. For this reason the formula needs to be able to re-create UI controls if needed. This is done by simply calling creation functions each time formula is run. The creation functions are intelligent enough not to create duplicates.

If control with given ID already exits, it will be checked if it is the same. If already existing control of the same ID and the same type is found, nothing is done, and GUI creation function returns guiExisting value. If control exists but has different type, it will be destroyed and control of new type will be created. If control with 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 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");
    

It is not really needed for buttons that don't require initialization, but it is useful when you would like to set up some initial values for controls only when they are created. The following example shows how to set initial text for 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 user clicks on a button) an event is created and the formula gets executed again. Several controls may trigger events on several occasions, for example when clicking on it, editing text, gaining or losing focus, etc. To decide whenever given control triggers events or not we use notify parameter in the Gui creation functions. For example:

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

will create a button that will trigger 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 given control for example, the code below will create an edit control that will notify us when its contents is changed and when edit field loses focus:

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

There are many possible notifications including:

When an event triggers, 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 the appropriately.

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

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

Usually there is zero (when formula execution was not triggered by 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 increasing "num" parameter as long as GuiGetEvent does not return zero (in what =0, =1 mode) or 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 the 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 using and shown back 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 back again using GuiEnable() function:

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

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

GuiSetFont("Tahoma", 13 );

Buttons (as 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 normal color of buttons, but they also have separate colors for different states (selected, hovered) and they 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 rather prefer 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 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 state of controls

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

Each control (including on-chart controls) in Windows OS is actual "window" object. Controls (window objects in general) preseve 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 given chart pane is displayed.

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

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

This fact has one consequence - if you want to keep your control state between "destory" 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 earier, control creation functions such as GuiButton, GuiEdit, GuiDateTime, GuiCheckbox, GuiRadio, GuiSlider, etc, all return guiNew value if control is newly created or guiExisting if given control (window object) already exists. This allows us to know when we need to restore previously saved state. For example assuming that we have edit control text saved in the "mySavedText" static variable, we could restore the state using code like 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 to 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 persistent static variable it will be preserved between AmiBroker runs and restored whenever edit control is (re-)created.

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

Examples

Example code below shows many techniques described above, including setting inital 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 resonable 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 limitation. 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 huge performance decrease and next you will hit the limit and run into problems (crash)
https://blogs.msdn.microsoft.com/oldnewthing/20070718-00/?p=25963

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