Sharing data between window layout scripts

How do I share data and variables between main/child scripts/windows?

Answer

REXX GUI is fairly opened-ended on how to communicate and share data.

A "Child Window Layout" (script) is also a "REXX Object" (script). It just happens to contain one of those GUIBEGIN/GUIEND comments, and calls GuiCreateWindow to open its own window. So that's why I call it a "Child Window Layout" script instead of simply a plain REXX Object script.

Because it's a REXX Object, it needs to be loaded by some other script calling CreateObject() (to create a running instance of that script). We'll refer to this other script that calls CreateObject as the "creator script". This latter script is also the one that does the message loop around GuiGetMsg().

There are two things to bear in mind:

1) Since a Child Window Layout script is an object, once it is CreateObject'ed, its variables retain their values until the creator script finally DROP's that child object. At any time between the CreateObject and the DROP, the creator script can retrieve the current value of any variable in the child. You have a choice of approaches how you do this, which we'll examine later.

2) A child object tells its creator when to do something by calling GuiWake() and passing some "signal" (ie, any string of your choice). The creator's "GuiSignal" variable is set to this signal string, and the creator's "GuiObject" variable is set to the name of the object that called GuiWake. The creator script should be written to know what to do when it is woken up from GuiGetMsg with this "signal" from that particular object.

Taking these two concepts, let's examine a particular example. I'm going to omit some error checking to simplify the example.

Assume you have a child script that will present a window containing an ENTRY control to enter a name, and a PUSH button. Let's say that the ENTRY control has an associated variable of "NewName", and the PUSH button has a variable of "NameOk". Let's say that you've named this child script "AddAName.rex".

The creator script creates the child:

CreateObject("AddAName.rex", , , -1, "NORMAL")

The child script's Create subroutine is called. It calls GuiCreateWindow to open its window. I won't show you the GUIBEGIN/GUIEND comment in the child script, but it defines an ENTRY and PUSH button obviously.

Create:
   GuiCreateWindow(ARG(2), ARG(1))
   RETURN

Ok, now the child object is created, and its window is open. We're back in the creator script. It does its message loop:

DO FOREVER
   GuiGetMsg()
   /* We'll add meaningful instructions here later. */
END

The child script also has a CLICK event handler for "NameOk" button. Let's say that the user enters a name, and then clicks ok. Here's what the event handler for the button does:

WM_CLICK_NameOk:
   /* Get what the user typed into the ENTRY. It has a
    * variable named 'NewName' associated with it.
    */
   GuiGetCtlValue('NewName')

   /* Did the user type something into it? If so, call GuiWake
    * to cause the main script to return from its GuiGetMsg.
    * We'll pass a signal that is simply what the user typed.
    */
   IF NewName \== "" THEN GuiWake(NewName)

   RETURN

Because the child script needs to return only one piece of information, it passes that information verbatim to GuiWake as the "signal".

Ok, the creator script wakes up from GuiGetMsg. Now we have to add instructions to check for GuiObject == "ADDANAME" (meaning that our AddAName.rex script called GuiWake) and GuiSignal is not DROP'ed (meaning that the user didn't close the window, so therefore, it must be the Ok button was clicked because that's the only other time that AddAName.rex would send a signal). And our creator knows that GuiSignal has been set to what the user typed. So here's our creator's loop:

DO FOREVER
   GuiGetMsg()

   /* Did AddAName.rex cause this signal? */
   IF GuiObject == "ADDANAME" THEN DO

      /* Yes it did. There are 2 things that could have happened. The
       * user closed the window in which case GuiSignal is DROP'ed. We
       * have nothing to do except to DROP AddAName to unload the script.
       * Or, it could have been the OK button clicked, in which case
       * GuiSignal is what the user typed.
       */
      IF EXISTS('GuiSignal') THEN
         GuiSay("The user typed:" GuiSignal)

      /* We don't need AddAName.rex anymore. We got the name. So unload it. */
      DROP AddAName

   END /* AddAName signal'ed. */

END

In fact, the above is what the example tutorial does (albeit the instructions are arranged a little differently in order to accomodate adding code for other object scripts, and error checking).

But what if the child script needs to return more data? Maybe a name, plus a telephone number? Well, WM_CLICK_NameOk could do something like:

WM_CLICK_NameOk:
   /* Get what the user typed into the ENTRY. It has a
    * variable named 'NewName' associated with it.
    */
   GuiGetCtlValue('NewName')

   /* Get what the user typed into the ENTRY for the telephone
    * number. Its variable is 'NewPhoneNumber'. 
    */
   GuiGetCtlValue('NewPhoneNumber')

   /* Call GuiWake, passing both pieces of information separated by
    * a & character. It is assumed that this char would never appear
    * in a name or phone number.
    */
   IF NewName \== "" & NewPhoneNumber \== "" THEN GuiWake(NewName || '&' || NewPhoneNumber)

   RETURN

And the creator script can PARSE VAR on GuiSignal to separate the two pieces of data:

PARSE VAR GuiSignal name '&' phonenumber
GuiSay("Name =" name ||", Phone =" phonenumber)

But that's just one of many approaches. Let's say that the child's Ok button handler simply calls GuiWake with a signal string of "GET IT YOURSELF, DUMMY" as so:

WM_CLICK_NameOk:
   GuiGetCtlValue('NewName')
   GuiGetCtlValue('NewPhoneNumber')
   IF NewName \== "" & NewPhoneNumber \== "" THEN GuiWake("GET IT YOURSELF, DUMMY")
   RETURN

The child script also has two more subroutines. One returns the value of the variable NewName, and the other returns the value of the variable NewPhoneNumber:

GetName:
   RETURN NewName

GetPhoneNumber:
   RETURN NewPhoneNumber

Here's what the creator can do:

GuiGetMsg()
IF GuiObject == "ADDANAME" THEN DO

   /* Does he want me to fetch the values myself? */
   IF GuiSignal == "GET IT YOURSELF, DUMMY" THEN DO
   /* He does. Ok, call his GetName to retrieve the name. */
      name = AddAName~GetName()

      /* Call his GetPhoneNumber to retrieve the telephone. */
      telephone = AddAName~GetPhoneNumber()

      GuiSay('Name =' name ||', Phone =' telephone)
   END

   /* We don't need AddAName.rex anymore. We got the name and telephone. So unload it. */
   DROP AddAName
END

Want a slightly different approach that involves one function to retrieve the values? The child script can have this:

GetValues:
   USE ARG name, phonenumber
   name = newname
   phonenumber = newphonenumber
   RETURN

And the creator can do this:

GuiGetMsg()
IF GuiObject == "ADDANAME" THEN DO
   IF GuiSignal == "GET IT YOURSELF, DUMMY" THEN DO
      AddAName~GetValues(name, telephone)
      GuiSay('Name =' name || ', Phone =' telephone)
   END
   DROP AddAName
END

But wait. There's more!

If you read "The REXX Language -> Objects", you'll note that a creator can have direct access to an object's variables by the way he names the object. If you give it a stem name, you can directly access the variables:

CreateObject("AddAName.rex", "AddAName." , -1, "NORMAL")

Now instead of calling some sub in the child, the creator can do:

GuiGetMsg()
IF GuiObject == "ADDANAME." THEN DO
   IF GuiSignal == "GET IT YOURSELF, DUMMY" THEN
      GuiSay('Name =' AddAName.~NewName ||', Phone =' AddAName.~NewPhoneNumber)

   /* NOTE: After we drop AddAName.rex, we can no longer reference its variables. */
   DROP AddAName.
END

NOTE: The dot is added to the name for GuiObject. And after you DROP that variable, those values go away too.

It's difficult to point to any one way as "the way to do things". It just depends upon what you need to do, and whether you need features like initializing controls to particular values before a window is open, or have the last values used each time the window is reopened (persistant data). REXX GUI is meant to be a open-ended and flexible. It can be tailored to accomodate a variety of needs.


Query the value of a single control

How can I query the value of only one particular control?

Answer

To query the value of a control, just call GuiGetCtlValue(), passing the name of the variable associated with the desired control. This will set that variable to the control's current value. On the individual page for each control type, you'll usually see an example of querying the value, as well as examples to do other things with the control. To query all controls' values, just call GuiGetCtlValue without any args.


Initialize a TAB control

How can I initialize which tab a TAB control displays?

Answer

To initialize a TAB, before you GuiCreateWindow(), set the variable associated with your TAB control. In fact, this is the same way you initialize any control before the window is created.

So if your TAB control's variable is "MyTab", and you want to initially select the second item (label) which happens to be "Some label":

mytab = "Some label"
GuiCreateWindow('NORMAL')

If you want to change the selection after the window is open, just set the associated variable to a new value, and call GuiSetCtlValue. To select the label "Another":

mytab = "Another"
GuiSetCtlValue('MyTab')

NOTE: If your TAB is using INDEX style, then you use a number instead of the label.

This is the way you change any control's value after the window is open. Although for different controls, you may need to set the variable in a different way, they all follow this same procedure.

And to query a control's value, just call GuiGetCtlValue, and there you have the new value in its associated variable:

GuiGetCtlValue('MyTab')
GuiSay('Label named "'|| mytab ||'"is selected.')

Again, this is the same for all controls.


Pass args when creating a child layout

When creating a child window, can you pass arguments from the main script via CreateObject or GuiCreateWindow?

Answer

Your parent script creates a child object with CreateObject(). Your parent script may optionally pass extra args to CreateObject. CreateObject then passes these extra args to the child's "Create" subroutine. Read "The REXX Language -> Objects -> A REXX Object", the section "Passing arguments to an object script's Create()".

GuiCreateWindow() merely creates a window for the script that calls it. It has nothing to do with the interaction of scripts per se. The child script that RPC creates for you simply creates its own window by calling GuiCreateWindow in its Create() subroutine.

In fact, the child window layout script that RPC automatically creates for you assumes that your parent script may be passing two extra args to CreateObject() -- the handle to any parent window (or -1 if you want a modal child window), and the show state (ie, "NORMAL", "HIDE", etc). If your parent script passes one or both of those extra args, then the child script's Create() opens its window by calling GuiCreateWindow.

Notice that if your main script doesn't pass any extra args to CreateObject, then the child script doesn't bother to create its own window right then. Instead, it has another subroutine called CreateWindow which you'll notice does the GuiCreateWindow call. So, the child expects your main script to follow up with a call to that child subroutine, if your main script passes no extra args to CreateObject. (Why have this? Well, maybe your main script wants to do something else inbetween the time it creates the child object, and has the child open its window. Who knows?)

RPC creates a child window layout script this way just to give you a skeleton script that allows a fair bit of flexibility, but is pretty much "ready to go" if you just need a simple, modal dialog.

But this is all fairly open-ended. You can decide what, if any, args you want to pass to the object when it is first created, and what is the purpose of those args. Sometimes, I'll modify the child script's Create() if I don't need those "default" args, and instead pass other extra args that I do need. (Of course, there's no limit to the extra args you can pass to CreateObject, so you could pass the two "default" ones, as well as additional ones).

So yes, CreateObject does give you the ability to pass any number of args to your child object at the moment it is created. Those args can be used for any purpose. And there is no requirement that the child script call GuiCreateWindow as soon as its created (ie, in its Create subroutine). Think of the window layout scripts that RPC creates as a "suggested design". They work with minimal fuss. But if you want something else, dig in and rewrite what you need.


Get a handle of a control from its variable name

How do I get a handle of a control, given the name of the REXX variable associated with it?

Answer

Call GuiInfo with a "HANDLE" operation.

Note: The GuiWindow variable must be set to the handle of the window that contains the control.


Closing a window programmably

I have a "Cancel" button in my window. When the user clicks on it, I'd like to have my script close the window (as if the user clicked on the close box). How can I do that?

Answer

The best way to handle a "CANCEL" button is to simply send the window a "CLOSE" message. That way it simulates the user clicking on the close box. Your CLOSE event handler, if you have one, will then be called. And if you don't have one, then the default behavior happens and the window closes, and the creator script (calling GuiGetMsg) wakes up with GUIOBJECT set to the child object's variable name, and GUISIGNAL DROP'ed.

WM_CLICK_MyCancelButton:
  GuiSendMsg(, 'POST CLOSE')
  RETURN

The same thing applies if you have a "File -> Exit" menu item.


Determine focus

How can I determine which control has the focus in a window?

Answer

Calling GuiGetCtlPlacement without any args returns a handle to the window with the focus. From there, you can possibly get its associated variable via GuiInfo.


GUI control isn't initialized to its variable

Assume I have a WINDOW definition with an entry control in it. The entry has an associated variable name of "my_entry".

Here are two approaches to opening the window. The first approach comes up with the entry control containing the string Here is some text. The other approach doesn't.

/* Initialize the variable for the entry. */
my_entry = 'Here is some text'

/* Open and show the window. */
GuiCreateWindow('NORMAL')


/* Initialize the variable for the entry. */
my_entry = 'Here is some text'

/* Open the window, but don't show it yet. */
GuiCreateWindow()

/* Now show the window. */
GuiSetCtlPlacement(,,,,,,'NORMAL')

Answer

If you pass the Show arg to GuiCreateWindow() (ie, you pass a show arg of "NORMAL" for the first approach), then GuiCreateWindow() initializes all the controls to be in sync with their REXX variables. If you do not pass the Show arg, then GuiCreateWindow() says He doesn't want to show me. That probably means that he wants to maybe set up some of the associated variables after I return, or otherwise monkey with the controls. I had better not initialize the controls yet. I'll let him do that.

And you can do that simply by calling GuiSetCtlValue without passing any args. So here is your solution:

my_entry = "Here is some text"

/* Create the window, but don't show it nor initialize
 * the controls to their final states.
 */
GuiCreateWindow()
/* Here you can modify the controls or their variables. */
...

/* Now sync all the controls with their variables' values. */
GuiSetCtlValue()

/* Finally show the window. */
GuiSetCtlPlacement(,,,,,,'NORMAL')


Handle the same event for many controls

Is it possible to have an event for several of the same type of controls all use one event handler? For example, say I have 20 buttons that all do essentially the same operation when clicked upon. Could I use one CLICK event handler (subroutine) for all 20 buttons? If so, how would that one subroutine know which button was clicked?

Answer

You can. The ARG(1) passed to any handler for a control (ie, your WM_CLICK handler) is the handle to the control. This is unique for each control. Passing this to GuiInfo, doing a "VARIABLE" operation, will give you the variable name associated with that control.

I'm assuming you want to write a generic handler that determines which button is clicked, and performs a particular operation on some data associated with that button. (ie, All 20 buttons don't actually do the same thing. They just perform the same operation on each, unique set of data).

I think the best approach is to have a unique ID (name) for each button, and then put all the labels on the same subroutine. Make the first instruction pass ARG(1) to GuiInfo(), doing a "VARIABLE" operation, to get the variable name associated with that control.

From there, you can maybe use that variable name to access some compound variable, or do a SELECT to determine what data to use. Here's an example of a script that shows these options:

/*
GUIBEGIN
WINDOW , 129, 283, 400, 200, POPUP|CAPTION|SYSMENU|MINBOX|MAXBOX|THICK, , My Window
  FONT 8, 400, MS Shell Dlg
  PUSH 5, 6, 40, 14, TABSTOP, , Button1, , Button 1
  PUSH 6, 26, 40, 14, TABSTOP, , Button2, , Button 2
  PUSH 6, 45, 40, 14, TABSTOP, , Button3, , Button 3
DEND
GUIEND
*/

LIBRARY rexxgui
GuiErr = "SYNTAX"
GuiHeading = 1
GuiCreateWindow('NORMAL')

button1.!data = "my data for button 1"
button2.!data = "my data for button 2"
button3.!data = "my data for button 3"

again:
DO FOREVER
  GuiGetMsg()
  CATCH SYNTAX
    CONDITION('M')
    SIGNAL again
  CATCH HALT
  FINALLY
    GuiDestroyWindow()
END
RETURN

wm_click_button1:
wm_click_button2:
wm_click_button3:
  varname = GuiInfo("VARIABLE", ARG(1))
  SAY varname
  data = VALUE(varname || ".!DATA")
  SAY data
  SELECT varname
    WHEN "BUTTON1" THEN SAY "Button 1 clicked"
    WHEN "BUTTON2" THEN SAY "Button 2 clicked"
    WHEN "BUTTON3" THEN SAY "Button 3 clicked"
  END
  RETURN


SETFOCUS event doesn't change the focus

/*
GUIBEGIN
WINDOW , 0, 0, 342, 91, POPUP|CAPTION|SYSMENU|MINBOX|MAXBOX|THICK, , My Window
    FONT 8, 400, MS Shell Dlg
    PUSH 240, 47, 40, 14, TABSTOP, , setfocus, UP, MyButton
    ENTRY 51, 27, 93, 12, H_AUTO|BORDER|TABSTOP, , usb_size
DEND
GUIEND
*/

LIBRARY rexxgui
GuiErr = "SYNTAX"
GuiHeading = 1
usb_size = 'ABCD'
GuiCreateWindow('NORMAL')
again:
DO FOREVER
 GuiGetMsg()
 CATCH SYNTAX
   CONDITION('M')
     SIGNAL again
   CATCH HALT
   FINALLY
     GuiDestroyWindow()
END
RETURN

WM_CLICK_MyButton:
 GuiSay('Please specify a valid whole number for the USB size')
 GuiSendMsg('usb_size', 'SETFOCUS')
 RETURN

Just run the script, press the setfocus button, and when you've received the message, try and mark the entry field and remove the text to replace it with something else. I can't

Answer

This is a common problem that catches a lot of Windows programmers. You can't set the focus from within a SETFOCUS event, or an event that causes the focus to be set to a control such as WM_CLICK. It's like a catch-22 situation. What you should do is use GuiSendMsg with the 'POST' option (ie, "POST SETFOCUS") so that the SETFOCUS message is delayed until after your WM_CLICK handler returns.


Console Window, GUI, and the Pull Instruction

I have a Rexx Gui script in which I use a "pull" instruction to get text from the user. The problem is that, if the user brings some other window to the front (and the console window gets hidden as a result), the user may not realize that my script is still waiting for him to enter text and press the ENTER key, before my script can continue.

Answer

You shouldn't use PULL instructions in a GUI script. It can confuse the user to make him enter text at a console prompt when he's supposedly using a GUI program. TO get text from the enduser, use an ENTRY control in a GUI window. Unlike with the PULL instruction, an ENTRY control doesn't demand that the user enter text before anything else can happen.


Create a console window in a GUI script

Is it possible for a GUI script to create a "console" window like the "Rexx Console" in Rexx Programmer Center?

Answer

Yes. You can embed a console window into a GUI window. First, your script should explicitly register the RXCONSOLE add-on DLL (using a LIBRARY statement). Use GuiInfo to get the handle of your GUI window (into which you'll place the console window). Then, call ConCreate, passing that window handle as the first argument. This will create a "console control" embedded inside your GUI window. You can then use ConSay and ConPull to output and input text to this console. When you destroy your GUI window, the console control will automatically be destroyed.

In fact, you can embed multiple console windows right inside of a REXX GUI window, just like a control. You can change text colors and font too. Here's an example.

NOTE: Click on the "Do PULL" button and type some text to see how to PULL text from the console. To see how the PULL color can be changed, click on the "Change PULL color" button before you press the ENTER key to finish PULL input. (Note: you'll have to click back into the console window to make it active again. Then you can press ENTER).

/*
GUIBEGIN
WINDOW , 81, 238, 400, 200, POPUP|CAPTION|SYSMENU|MINBOX|MAXBOX|THICK, , Console example
 PUSH 5, 8, 40, 14, TABSTOP, , SAY1, , Say 1
 PUSH 51, 8, 40, 14, TABSTOP, , SAY2, , Say 2
 PUSH 99, 8, 40, 14, TABSTOP, , CLEAR, , Clear
 GROUP 145, 1, 250, 25, , , , , Background color
 RADIO 150, 11, 39, 10, AUTO|GROUP, , BackColor, , Black
 RADIO 195, 11, 39, 10, AUTO, , , , White
 RADIO 240, 11, 50, 10, AUTO, , , , Light gray
 RADIO 296, 11, 49, 10, AUTO, , , , Dark gray
 RADIO 351, 11, 39, 10, AUTO, , , , Gray
 PUSH 5, 29, 71, 14, TABSTOP, , TextColor, , Change text color
 PUSH 89, 29, 78, 14, TABSTOP, , PullColor, , Change PULL color
 PUSH 175, 29, 40, 14, TABSTOP, , Pull, , Do PULL
 PUSH 301, 29, 43, 14, TABSTOP, , ChangeFont, , Pick font
 PUSH 348, 29, 46, 14, TABSTOP, , SetFont, , Set Courier
 PUSH 224, 29, 68, 14, TABSTOP, , Pull1, , PULL 1 character
DEND
GUIEND
*/

LIBRARY rexxgui, rxconsole
GuiErr = "SYNTAX"
GuiHeading = 1

/* Set a few defaults */
backcolor = 1
textcolor = 1
pullcolor = 1

GuiCreateWindow('NORMAL')
again:
DO FOREVER
 GuiGetMsg()
 CATCH SYNTAX
   CONDITION('M')
     SIGNAL again
   CATCH HALT
   FINALLY
     GuiDestroyWindow()
END
RETURN

/* Called when the window and its controls are created, but
 * before it is displayed
 */
WM_INITDIALOG:
  /* Get the size of the window */
  GuiGetCtlPlacement(GuiWindow, , , "width", "height")

  /* We want to place our console under the buttons. So
  * we need to find where the top of the "lowest" button
  * appears* in the window (ie, its Y position), and its height.
  * We add these two together and subtract from the
  * main window height. Now we know how much height
  * we have free for the console to occupy
  */
 GuiGetCtlPlacement("TextColor", "SayX", "SayY", "SayWidth", "SayHeight")
 sayy = sayy + sayheight
 height = height - sayy

  /* Create a console window embedded into our main window.
   * We pass our GuiWindow handle as the second arg to
   * ConCreate(). We also specify the CHILD option. By
   * not specifying the "NODEFAULT" option, text is output
   * to this window with a ConSay command... from
   * anywhere. Text is gotten from this window with a
   * ConPull command... anywhere.
   * NOTE: We don't pass a handle variable name because we
   * don't need it. We can call the other Con functions, omitting
   * the first arg, and it will operate upon the default console.
   */
  ConCreate(, GuiWindow, "CHILD", , sayy, width, height)

  RETURN ""

/* Called when the SAY 1 button clicked */
WM_CLICK_Say1:
  /* Output to the console window */
  ConSay(, "Button 1 was clicked")
  RETURN

/* Called when the SAY 1 button clicked */
WM_CLICK_Say2:
  /* Output to the console window */
  ConSay(, "Button 2 was clicked")
  RETURN

/* Called when the Clear button clicked */
WM_CLICK_Clear:
  ConClear()
  RETURN

/* Called when the Text Color button clicked. This
 * changes the color of text we print via ConSay
 * or SAY.
 */
WM_CLICK_TextColor:
  /* We just increment the TextColor count. When
   * its 1, we set it to mostly Red. When its 2, we set
   * it to Green. When its 3, we set it to Blue.
   * When its 4, we roll back to 1. Ie, We cycle through
   * 3 different colors. Of course, you can select among
   * millions of colors if you want. 
   */
  textcolor = textcolor + 1
  IF textcolor > 3 THEN textcolor = 1
  SELECT textcolor
    WHEN 1 THEN DO
      red = 180 /* Range for each color value is 0 to 255 */
      green = 40
      blue = 40
    END
    WHEN 2 THEN DO
      red = 40
      green = 180
      blue = 40
    END
    OTHERWISE DO
      red = 40
      green = 40
      blue = 180
    END
  END
  ConSetColor(, , red, green, blue)
  RETURN

/* Called when the PULL Color button clicked */
WM_CLICK_PullColor:
  /* Set the color of text entered via ConPull. We
  * do the same thing as per the Text color. But
  * the PULL color can be different.
  */
  pullcolor = pullcolor + 1
  IF pullcolor > 3 THEN pullcolor = 1
  SELECT pullcolor
    WHEN 1 THEN DO
       red = 180
       green = 40
       blue = 40
    END
    WHEN 2 THEN DO
      red = 40
      green = 180
      blue = 40
    END
    OTHERWISE DO
      red = 40
      green = 40
      blue = 180
    END
  END
  ConSetColor(, "PULL", red, green, blue)
  RETURN

/* Called when one of the Background radio buttons clicked.
 * We have a choice of 5 colors for the background.
 */
WM_CLICK_BackColor:
  GuiGetCtlValue('BackColor')
  SELECT backcolor
     WHEN 1 THEN str = "BLACK"
     WHEN 2 THEN str = "WHITE"
     WHEN 3 THEN str = "LIGHTGRAY"
     WHEN 4 THEN str = "DARKGRAY"
     OTHERWISE str = "GRAY"
  END
  ConSetColor(, "BACK", str)
  RETURN

/* Called when the Do PULL button clicked */
WM_CLICK_Pull:
  /* Prompt the user to enter text, then get that
   * text. ConPull doesn't return until he enters
   * the text.
   */
  ConSay(, "Enter some text, then press ENTER")
  text = ConPull()

  /* Echo what he typed.
   * Note: If the user closes the window, or aborts
   * while ConPull is entering text, then HALT will
   * be raised. So, we'll never get here.
   */
  ConSay(, text)
  RETURN

/* Called when the Do PULL 1 character button clicked */
WM_CLICK_Pull1:
  /* Exactly the same as with "Do PULL", but we pass
   * a second arg which is how many characters we want
   * the text limited to. Here, that is 1. The allowable
   * range is 1 to 255.
   */
  ConSay(, "Press a key")
  text = ConPull(, 1)

  /* We could echo it with one ConSay call as so...
   * ConSay(, "You pressed the" text "key.")
   * But by passing a third arg of "NOBREAK" to
   * ConSay, you can suppress the line feed. So
   * here is how we can spread the printing across
   * 3 calls to ConSay, but keep it all on 1 line.
   */
  ConSay(, "You pressed the ", "NOBREAK")
  ConSay(, text, "NOBREAK")
  ConSay(, " key")
  RETURN

/* Called when the Pick Font button clicked */
WM_CLICK_ChangeFont:
  /* Preset the font dialog to pick and set the font */
  ConSetFont(, "")
  RETURN

/* Called when the Set Courier button clicked */
WM_CLICK_SetFont:
  /* We specifically set the font to "Courier New", height
   * of 16, and a style of BOLD. Other styles we can add are
   * ITALIC and UNDERLINE, each separated by a | character,
   * for example "ITALIC|UNDERLINE"
   */
  ConSetFont(, "Courier New", 16, "BOLD")
  RETURN

Alternately, if you need only an "output" console-like window (ie, you don't need to get input as with a PULL instruction), then you can create a child window that is filled with a readonly multi-line ENTRY. The benefit of this is that your script can access the "line history" (ie, lines that have been displayed).

Here's a child script (called console.rex) that implements this "console window":

/*
GUIBEGIN
WINDOW , 76, 282, 249, 146, POPUP|CAPTION|SYSMENU|MINBOX|MAXBOX|THICK, , Console
    FONT 8, 400, MS Shell Dlg
    ENTRY 0, 0, 248, 144, MULTI|V_AUTO|H_AUTO|READONLY|BORDER, , CONSOLE
DEND
GUIEND
*/

/* console.rex child script */

create:
  GuiErr = "SYNTAX"
  GuiHeading = 1
  GuiCreateWindow('NORMAL', ARG(1))

  /* Initially nothing displayed in the console window */
  consolechars = 0

  RETURN

destroy:
  GuiDestroyWindow()
  RETURN

/* Called when the user resizes the main window */
wm_size:
  DO
     /* Size the "Console" control to fill the window */
     GuiSetCtlPlacement("CONSOLE",,,ARG(3),ARG(4))

     CATCH SYNTAX
  END

  /* Don't let Rexx Gui process this event. */
  RETURN ""

setconsolelimit:
  GuiSendMsg("CONSOLE", "LIMITTEXT", ARG(1))
  RETURN

/********** WriteConsoleText() *************
 * Writes some text to the "CONSOLE" edit control.
 *
 * WriteConsoleText(text, eol)
 *
 * text = The text to write.
 * eol = 1 if the line should be ended. Omit this if not.
 */
writeconsoletext:
    /* See if we've totally filled the ENTRY with text. If so,
     * we have to empty it out before we can display any more.
     */
    consolechars = consolechars + LENGTH(ARG(1))
    IF consolechars > 10000 THEN DO
        consolechars = LENGTH(ARG(1))
        GuiSendMsg("CONSOLE", "SETSEL", 0, -1)
        GuiRemoveCtlText("CONSOLE", ARG(1))
    END
    ELSE
        /* Output the text. NOTE: It's assumed that the character position
         * is already at the end.
         */
        GuiAddCtlText("CONSOLE", ARG(1))

    /* Set the character position to the end of the ENTRY */
    GuiSendMsg("CONSOLE", "SETSEL", -1, -1)

    /* Does the caller want the end of a line inserted? */
    IF ARG() \== 1 THEN DO
        GuiSendMsg("CONSOLE", "SETSEL", -1, -1)
        GuiAddCtlText("CONSOLE", "")
        consolechars = consolechars + 2
        GuiSendMsg("CONSOLE", "SETSEL", -1, -1)
    END
    RETURN

And here's an example main layout that uses it:

/*
GUIBEGIN
WINDOW ,0,0,400,200, POPUP|CAPTION|SYSMENU|THICK|MINBOX|MAXBOX, , My Window
    FONT 8, 400, MS Shell Dlg
DEND
GUIEND
*/

LIBRARY rexxgui
GuiErr = "SYNTAX"
GuiHeading = 1
GuiCreateWindow('NORMAL')

/* Open our CONSOLE window, using the console.rex child object script */
CreateObject("console.rex", , , GuiWindow)

/* Arbitrarily set a limit of 10000 maximum chars displayed
 * in the console window before it clears.
 */
console~SetConsoleLimit(10000)

/* Output some text to the console window */
console~WriteConsoleText("This")
console~WriteConsoleText(" is")
console~WriteConsoleText(" line 1.", 1)
console~WriteConsoleText("This is line 2.", 1)

again:
DO FOREVER
  GuiGetMsg()
  CATCH SYNTAX
    CONDITION('M')
    SIGNAL again
  CATCH HALT
  FINALLY
    GuiDestroyWindow()
END
RETURN


Resize window/control problem

I'm having problems with a script that creates a window containing a list box, and then tries various ways of making the window and list box 80% narrower. Here's my NarrowWindow function to resize the listbox or window. It is called once to resize the window, and again to resize the listbox. But, I'm getting incorrect sizing:

narrowwindow(GuiWindow)
narrowwindow("MyListBox")
RETURN 

narrowwindow:
  GuiGetCtlPlacement(ARG(1), x_pos, y_pos, width, height, enable, show)
  IF x_pos > 1023 THEN x_pos = x_pos - 1024
  width = TRUNC(width * .8)
  GuiSetCtlPlacement(ARG(1), x_pos, y_pos, width, height)
  RETURN

Answer

There's one big problem with your script. In your call to GuiGetCtlPlacement, you're supposed to be passing the names of the variables where you want the values stored. So for example, if you want the window's x position to be stored in the variable x_pos, you can call GuiGetCtlPlacement as so:

GuiGetCtlPlacement(whichwindow, "x_pos", etc...

But you're calling it as so:

GuiGetCtlPlacement(whichwindow, x_pos, etc...

NOTE: You're not quoting the variable name.

So you're really telling GuiGetCtlPlacement, "store the value using the variable name that is stored in x_pos". Of course, if x_pos has never been assigned a value, then it just so happens that the value is "X_POS" (since a variable's value defaults to its own name uppercased). On the other hand, if x_pos happens to equal "367" then you're telling it to store the width in a variable named "367". (I didn't add coding to test if you're passing an illegal variable name. So that's why GuiSetCtlPlacement isn't complaining. But neither is it setting the variable you expect). And that is what is causing your troubles since you reuse the same variables for multiple calls to NarrowWindow (without any PROCEDURE keyword, which would "fix" your problem in this particular instance, but not the right way. The right way is to quote the name.

Conclusion: If a function says "pass the name of a variable", then make sure you quote the name if you're passing it directly.


Get the screen resolution

I have a script I wrote that was working fine until one day when my client changed the screen resolution on his monitor down to 800x600. When he did that, my dialog window became way to large to be managable. How can I retrieve the screen resolution so that I can code for varying resolutions?

Answer

This tells you the inner area of a maximized window (ie, the area where you can place controls inside your window):

FUNCDEF('GetSystemMetrics', '32u, 32u', 'user32')
width = GetSystemMetrics(61 /* SM_CXMAXIMIZED */)
height = GetSystemMetrics(62 /* SM_CYMAXIMIZED */)
SAY "width" width
SAY "height" height


Accomodate different screen resolutions

I'm have a script that moves controls whenever the window is resized (to keep the controls entirely visible in the window when it is shrunk, for example).

When I get a SIZE event, I simply take the size of the window, and use GuiSetCtlPlacement to move a control a set amount from the bottom and right side of the window. For example, I may move the cancel button by subtracting 30 pixels from the height of the window to get the new X position, and subtracting 50 pixels from the width of the window to get the new Y position.

What I'm seeing is large differences in where my controls end up at different screen resolutions. For example, at work (standard screen with 1024 x 768) they'll be placed nicely. At home (laptop with 1680 x 1050) they get placed differently (overlapping each other and more to the right).

Either the GuiSetCtlPlacement coding is wrong (for differing screen resolutions) or I need to "adjust" how 100 & 30 pixels are treated based on the screen resolution.

Answer

Your algorithm isn't doing any "scaling".

Notice that when you change resolutions, all of the controls get bigger, as does the window itself. That's because, at lower resolutions, there are less pixels on screen, and each pixel is bigger.

So, if you don't do any scaling, then you're simply moving things around on the screen, but not taking into account the fact that things are different sizes at different resolutions. It's no wonder why you've got controls "colliding" at different resolutions, and not being placed at the same relative (ie, scaled) positions.

Yes, you've got to scale the 100 and 30 units based upon the screen resolution. But more than that, you also have to realize that controls (and the text inside them) are bigger at lower resolutions.

When the user changes the resolution (a DISPLAYCHANGE event happens in each of your windows when the user does that), this is the point to determine how scaling changes.

I suppose one way to do things is to note the maximized window width and height at the resolution at which you're calculating your 100 and 30 units. We'll call these "RefWidth" and "RefHeight". Then when you get the DISPLAYCHANGE, get the window's new maximized width and height, and you can scale the 100 x offet and 30 y offsets:

(NewMaxWidth / RefWidth) * 100
(NewMaxHeight / RefHeight) * 30


Display JPG in a window

How can I display a JPG file in a window?

Answer

There are 2 ways to do this.

First, you can use an HTML control. The advantage of this is that you have a lot of control over formatting by using various HTML tags. Also, the HTML control automatically gives you scroll bars if the image is larger than the display area.

You'd send the HTML control an HTML string that contained an IMG tag like so:

GuiAddCtlText("MyHTML", '<IMG src="MyPicName.jpg">')

Another very easy way is to use REXX GUI's GuiPicture() function, and handle the PAINT event for the window which will display any images. There's a example script called showpic.rex on my web site to demonstrate the GuiPicture function.


Suppress GuiFile()'s Cancel message

When I present the GuiFile dialog, and the user clicks the CANCEL button, I get a SYNTAX condition raised, and an error message pops up in my REXX GUI script. How do I get rid of this message?

Answer

What is probably happening right now is that you're letting the CATCH SYNTAX in the main script's message loop handle this particular instance of SYNTAX. And that CATCH SYNTAX displays an error message. But remember that you can use CATCH SYNTAX anywhere. If you want to handle it differently at a particular place, just put a DO END there and put a CATCH inside it. For example:

DO
   /* Store the filename in the variable FN. */
   fn = ""

   /* Present the dialog. */
   err = GuiFile('FN', , 'This is the title')

   CATCH SYNTAX
      /* Get the error message. */
      err = CONDITION('D')
END

/* Check for an error. */
SELECT err

   WHEN "" THEN DO

      /* Filename was successfully chosen. Here, you'd
       * do something with FN.
       */

   END

   WHEN 'DLL function "GUIFILE" reported "Cancel"' THEN NOP /* Ignore CANCEL. Note we assume GuiHeading = 1. */

   OTHERWISE
      /* Some other error, so display it. */
      CONDITION('M')
END

Another option is to DROP the GuiError variable so that you revert to manual error checking. But then, you'd need to restore it afterwards:

/* Revert to manual checking of errors. */
oldGuiError = GuiErr
DROP GuiError

/* Store the filename in the variable FN. */
fn = ""

/* Present the dialog. */
err = GuiFile('FN', , 'This is the title')
IF err \== "" THEN DO

   /* Filename was successfully chosen. Here, you'd
    * do something with FN.
    */

END

/* If not Cancel, display the error message. */
ELSE IF err \== "Cancel" THEN GuiSay(err)

/* Restore */
GuiErr = oldGuiError


Change a control's color

How can I change the color of a PUSH button?

Answer

For some controls, there are some window events that happen when the operating system needs to know what color to draw the control. For example, a CTLCOLORSTATIC event happens when the OS needs to know what color you want for a TEXT control. (The colors.rex example on my site demonstrates this). For an ENTRY, there's a CTLCOLOREDIT event (and the color2.rex example shows this).

For some of the other, newer controls, such as a PROGRESS bar, there are specific messages you can send to change colors of the control.

For a PUSH, CHECK, or RADIO, there is supposed to be a CTLCOLORBTN event. But I've never been able to get it to work, and searching google for WM_CTLCOLORBTN reveals that, apparently, Microsoft never implemented it. So, to change a button color, you actually have to create an "Owner drawn" button and draw the thing yourself. Not very pleasant. You may want to consider an alternative method of indicating a change in the state of the displayed data, such as using bitmaps.


Disable a control

How can I disable a PUSH button, and then later enable it as well?

Answer

The flag arg to GuiSetCtlPlacement() will let you enable or disable a control. Pass the name of the REXX variable associated with the control as the first arg.

To disable a PUSH control whose REXX variable is "MyButton":

GuiSetCtlPlacement("MyButton", , , , , "DISABLE")


One script modifies control in another layout script

I have a main window layout script and several child window layout script. One of the child scripts is a "progress report" window (ie, a simple window with one list control to which lines of "progress" are added). The main script creates each of the child objects.

The main script CreateObject()'s the child progress window using a REXX Object name of "progress". So the main script can add a line of data to the progress window by calling its add_to_progress_list() function as so:

progress.~add_to_progress_list("some text")

If I want to add a line of data to the progress window from another child layout script, can I do that directly? I notice if I put the above call in the child script, it complains that it can't find the progress object. That makes sense since it wasn't created in the child script. It was created in the main script.

Or must I instead GuiWake() the main script in order to tell it to write something on behalf of the child script?

Answer

In such a case, what I'd probably do is add a routine to the "Progress" object script to return the handle to its listbox. Assuming the listbox has an associated variable named "MyListBox", here's the routine:

GetMyListbox:
   RETURN GuiInfo('HANDLE', 'MyListBox')

Now, in your main script, after you CreateObject() the Progress script, call its GetMyListBox() function and store that listbox handle in some variable of your choice:

CreateObject('MyProgress.rex')
MyListboxHandle = MyProgress~GetMyListbox()

Your main script now has that handle, so it can pass it to GuiSendMsg() to send that listbox any messages. For example, your main script can add a string like so:

GuiSendMsg(MyListboxHandle, 'ADDSTRING', , 'My string')

Note: I don't quote MyListBoxHandle. I don't want to pass the variable name. I want to instead pass the handle stored there.


Now it's just a matter of giving your other scripts that handle, and they can also use it with GuiSendMsg().

You have a couple choices. You can pass MyListBoxHandle as an additional arg when you CreateObject the other scripts. The Create() function for the script can then use ARG() to grab it, and store it in some variable for future use.

Or, you can have a function in each of your child scripts called "SetMyListBox" like so:

SetMyListbox:
   MyListboxHandle = ARG(1)
   RETURN

And after you CreateObject the child, your main script will call that, passing the listbox handle:

CreateObject('MyChild.rex')
MyChild~SetMyListbox(MyListboxHandle)

These 2 choices are quick and easy. There's only one problem with them. Say that the user closes the Progress window, but some other children are still open and think that they still have a valid handle to the listbox. Now, GuiSendMsg is smart enough to detect an invalid handle (and do nothing), but if you then reopen the Progress window, those children won't have an updated handle (unless your main script calls each child's SetMyListBox again).


A better alternative is to have your own custom message that is used to tell your main script to return that listbox handle. Let's say you use a custom message number of 35000.

First, each child script needs the handle to your main window. So change SetMyListBox() to SetCallerHandle():

SetCallerHandle:
   /* Store the handle passed to me. */
   MyCallerHandle = ARG(1)
   RETURN

Now, in your main script, after you create each child, call its SetCallerHandle like so:

/* Create some child window layout. */
CreateObject('MyChild.rex')
/* Give this child the handle to my own window. */
MyChild~SetCallerHandle(GuiWindow)

Now whenever your child script needs that listbox handle, it does the following:

/* Tell my creator to give me the listbox handle.
 * I do this by sending a custom message #35000.
 */
GuiSendMsg(MyCallerHandle, 35000)
/* Store what he returned */
MyListboxHandle = GuiSignal

If your child script always does the above before sending the listbox one (or a series of) messages, it will have an updated handle.

One last thing you need to do is add an EXTRA event handler to your main script, and look for that custom message number of 35000 like so:

WM_EXTRA:
   SELECT ARG(3)
      /* Is this a custom message of 35000? If so,
       * return the listbox handle to whomever sent
       * me this custom message.
       */
      WHEN 35000 THEN RETURN MyListboxHandle

      /* Ignore all other custom messages. */
      OTHERWISE
   END
   RETURN


User abort in REXX GUI script

I have a REXX GUI script that processes a file, looping through it line by line, and updates a TEXT control to indicate progress to the user on each loop. While the loop is working, there doesn't seem to be a way for the user to halt this script. Pressing Ctl-C or ESC has no effect. For production work, the script may be looping through 100,000 records or more. Some way for the user to stop the script from within this loop is required.

Answer

Whenever you're not in a call to GuiGetMsg, then the user can't interact with the windows. Normally, when you call GuiGetMsg, it halts your script until the user does something. So you wouldn't want to stick a normal message loop inside of your "work loop" because each time you hit that message loop, your script would halt until the user did something.

But GuiGetMsg has a "CLEAR" operation. This doesn't halt your script. Rather, it simply checks whether the user has done anything up to this point, and if not, it immediately returns.

I think that the best thing to do would be to pop open a child "Progress window" with a checkbox that says "Abort" (which is initially not checked). (You can also display your status messages to a TEXT control in this window).

On my script examples site is an example called "abort.rex" that demonstrates this.


TIMER events stopped by modal child

I have a main script with a TIMER control that checks the OS clock and displays it in status bar. Now when i open a child script and interact with it (push button, open file dialogue, etc...) the TIMER stop.

Answer

If your child window is modal, then yes, this is normal behavior. File dialog is also modal. If your child window isn't modal, then you can continue to get timer events. Otherwise, timer events happen only with active windows. You could add a TIMER to the child window to keep getting the events.


Update a control periodically

I have a multi-line ENTRY displaying some information about the status of processes. This information changes every 2 minutes. Is it possible to refresh the control automatically without any user intervention?

Answer

You could use a timeout set for 2 minutes, and just redisplay the page upon every timeout. See the "TimeOut.rex" example for how to repeatedly time-out at a prescribed period.


Working ENTER key in a single-line ENTRY

I have a window containing a single-line ENTRY control. When the user presses the ENTER key, nothing happens. I don't get any indication that the user finished entering text. How can I make it otherwise?

Answer

The way the ENTER key works in a single-line entry control is not all that intuitive. But this is the way that Microsoft designed it. In order to recognize the ENTER key, you have to put a PUSH control in the window, and give it the "Default button" style. (ie, Click that checkbox in the Properties dialog for your PUSH button).

Then, when the user presses the ENTER key in the entry, you'll receive a CLICK event for the button. That's not the way I would have designed it, but that's the way some MS employee did.

If you don't actually want the PUSH button to show, you could also give it the "Hide" style.

An alternate is to handle the UNFOCUS event for your ENTRY. That happens whenever the user tabs to another control, clicks on another control, or presses the ENTER key. In this case, you won't need a default button.


OK button selected when user presses ENTER

I have a window containing an ENTRY and an OK button. I want the user to be able to type some text into the ENTRY, and then press the ENTER key to automatically select the OK button.

Answer

When designing your window, make sure your button has its Default property checked (in its Properties dialog). This means that an ENTER keypress will cause that button's CLICK event to happen automatically:

/*
GUIBEGIN
WINDOW , 105, 172, 128, 55, POPUP|CAPTION|SYSMENU|MINBOX|MAXBOX|THICK, , My Window
 FONT 8, 400, MS Shell Dlg
 ENTRY 11, 10, 105, 12, H_AUTO|BORDER|TABSTOP
 PUSH 43, 33, 40, 14, DEFAULT|TABSTOP, , Button, , Push
DEND
GUIEND
*/

LIBRARY rexxgui
GuiErr = 'SYNTAX'
GuiHeading = 1
GuiCreateWindow('NORMAL')
again:
DO FOREVER
  GuiGetMsg()
  CATCH SYNTAX
    CONDITION('M')
    SIGNAL again
  CATCH HALT
  FINALLY
    GuiDestroyWindow()
END
RETURN

wm_click_button:
  GuiSay('Button')
  RETURN

If you do not wish the button to appear in the window, you can check its Hide property. It will still function as so.


Select all lines in an ENTRY or LISTBOX

How do I select all lines in a multi-line ENTRY control?

Answer

GuiSendMsg("MyEntry", "SETSEL", 1)


Scroll to selection in a multi-line ENTRY

Try the following code:

/*
GUIBEGIN
WINDOW , 0, 0, 400, 200, POPUP|CAPTION|SYSMENU|MINBOX|MAXBOX|THICK, , My Window
    FONT 8, 400, MS Shell Dlg
    MENU
    ENTRY 0, 0, 165, 95, MULTI|V_AUTO|H_AUTO|RETURN|VSCROLL|HSCROLL, , TEXT
DEND

MENU
    HEADING &File
        ITEM &New
        ITEM &Open ...
        ITEM
        ITEM &Save
        ITEM Save &as
        ITEM
        ITEM E&xit
    <
    HEADING &Edit
        ITEM &Find
        ITEM &Repeat find (F3), repeat_find, F3
    <
DEND
GUIEND
*/

LIBRARY rexxgui
GuiErr = "SYNTAX"
GuiHeading = 1
DO i = 1 TO 100
    text.i = 'This is line 'i
END
text.0 = 100
text.25 = 'Hello'
text.50 = 'Hello'
text.75 = 'Hello'    
start_i = 1    
GuiCreateWindow('NORMAL')
again:
DO FOREVER
    GuiGetMsg()
    CATCH SYNTAX
            CONDITION('M')
            SIGNAL again
    CATCH HALT
    FINALLY
        GuiDestroyWindow()
END
RETURN

/**********************************************************/
editfind:

/* Always look for Hello */
DO i = start_i TO text.0
  IF POS('Hello', text.i) \== 0 THEN
    DO
      line_nr = i; col_nr = 1; length_search_str = 5
      calculate_start_pos()
      start_i = i
      RETURN
    END
END
RETURN

/*****************************************************************
 We've found the string we're looking for. Make sure we highlight 
 it                                                               
*****************************************************************/ 
calculate_start_pos:

  start_pos = 0

  /* This loop seems strange, but it is needed to get the starting
   * position of the string FROM THE START OF THE STEM VARIABLE   
   */   
  DO i = 1 TO (line_nr - 1)
    /* The 2 at the end is for the CRLF on each line */
    start_pos = start_pos + LENGTH(text.i) + 2
  END

  /* Make the line found the first line shown in the entry */
  start_pos = start_pos + (col_nr - 1)
  end_pos = start_pos + length_search_str

  GuiSendMsg('text', 'SETSEL', start_pos, end_pos)


  /* ADDED THIS LINE! */
  GuiSendMsg('text', 'SCROLLCARET')


  RETURN

To see what I would like to do, simply run the script, select the edit menu and then the Find one. You won't actually see much happen, but if you scroll down in the entry control, you'll see that the first occurrence of the word "Hello" is highlighted. How do I "scroll" the entry so that that line (line 25) becomes the first visible line ? I've seen and use GuiSendMsg("listbox", "SETTOPINDEX", number) but this is only for list controls isn't it?

Answer

Yes, "SETTOPINDEX" is for a list control. After you've selected the text you want to make visible, you can send a SCROLLCARET message to scroll that line into view. I've modified your above code to show that additional line.


Dynamically change an ENTRY style

Is there a way to change an entry style from "numbers" to "string" without having to remove the ENTRY, and then re-add it with the changed style?

Answer

There's no official way to do it on-the-fly. (You may be able to monkey with trying to hack into changing the control's styles, but sometimes this works and sometimes it doesn't).

The only alternative is to put two, identically sized edit controls in the extra same spot. One of them will be numbers, and the other is strings. Then, you just SHOW the one you want, and HIDE the other one. (Do it in this order, and the user shouldn't notice any change visually). Call GuiSetCtlPlacement, passing "SHOW" or "HIDE" for the seventh arg.


Dynamically remove item from listbox

How do you delete the first item if you're not using INDEX style (ie, GuiRemoveCtlText doesn't delete by index), and you also don't know what the item's text is?

Answer

Send a DELETESTRING message, and pass the index, minus 1 (ie, referenced from 0).

/* Delete the first item, whatever it is */
GuiSendMsg("MyList", "DELETESTRING", 0)

If you have a single select listbox, and you need to get the index of the current selection (referenced from 0), you can always send the GETCURSEL message, and GUISIGNAL has that index.

/* Get the selected item's text */
GuiSendMsg("MyList", "GETCURSEL")
SAY "Item" GuiSignal+1 "is selected."

Incidentally, you can send a GETTEXT message to retrieve the text of the currently selected item. GUISIGNAL is set to the text.

/* Get the selected item's text */
GuiSendMsg("MyList", "GETTEXT")
SAY GuiSignal


Re-arrange list-box items

I'm looking for a way to re-arrange items in a list-box. I do not want an alpha or numeric sort, more like a to-do list, where I can select an item and move the item up or down the list.

Example: There are ten items in the list-box. I want to move the ninth in the list to number two, with the original number two becoming three, three becoming four and so on. Then I want the fourth to number ten, with the original number ten becoming nine, nine becoming eight and so on. Last, I want to delete number seven, with eight, nine and ten moving up one spot, leaving nine items in total.

Answer

There's a example script on my web site that shows you how to make a "drag list box".


RADIO button selection always the same

Run the following script. Press the SHOW button. Select the Replace radio button. Press the OK button. I can't get DSNUTILB_append to contain the value 2. Are my radio definitions incorrect?

/*
GUIBEGIN
WINDOW , 0, 0, 397, 116, POPUP|CAPTION|SYSMENU|MINBOX|MAXBOX|THICK, , My Window
    FONT 8, 400, MS Shell Dlg
    PUSH 286, 81, 40, 14, TABSTOP, , Show_button, , Show
    PUSH 340, 81, 40, 14, TABSTOP, , ok_button, , Ok
DEND
GUIEND
*/

LIBRARY rexxgui
GuiErr = "SYNTAX"
GuiHeading = 1
usb_size = 'ABCD'
GuiCreateWindow('NORMAL')
again:
DO FOREVER
 GuiGetMsg()
 CATCH SYNTAX
   CONDITION('M')
     SIGNAL again
   CATCH HALT
   FINALLY
     GuiDestroyWindow()
END
RETURN

wm_click_show_button:
  dsnutilb_append = 1
  GuiAddCtl("FRAME 239, 52, 120, 14, ETCHED, , DSNUTILB_frame")
  GuiAddCtl("TEXT 244, 55, 31, 8, GROUP, , DSNUTILB_text, , Data") 
  GuiAddCtl("RADIO 270, 55, 42, 10, AUTO|GROUP|TABSTOP, , DSNUTILB_append,, Append")
  GuiAddCtl("RADIO 316, 55, 42, 10, AUTO|TABSTOP, , DSNUTILB_replace,, Replace")
  RETURN

wm_click_ok_button:
  GuiGetCtlValue()
  SAY dsnutilb_append
  RETURN

Answer

When adding radio buttons, you may need to pass a third arg to GuiAddCtl(). This arg is the value you want when the button is selected. The first button in the group must have the value 1 (and this is the default if you omit that arg). But for your subsequent buttons, pass that arg:

GuiAddCtl("RADIO 316, 55, 42, 10, AUTO|TABSTOP, , DSNUTILB_replace,, Replace",, 0)


Get the contents of a listbox

I have a LIST box. I have added the contents to it, one line at a time, by adding each line with a call to GuiAddCtlText(). So, I don't have some REXX stem variable that contains the entire contents of the listbox. Is there some way to read the contents of the listbox, or must I maintain/sync some REXX variable to that contents?

Answer

Yes, you can read the contents of the listbox, line by line, on-the-fly. I'll assume your list box has the variable "MyList" associated with it, and it's a single select list box:

/* Get how many lines total in the list */
GuiSendMsg("MyList", "GETCOUNT")
count = GuiSignal

/* Read each line, one at a time */
DO i = 1 TO count

   /* Get the text into some variable */
   GuiSendMsg("MyList", "GETLBTEXT", i, "MyVar")

   SAY myvar
END


No KEYDOWN event when typing in an ENTRY

I'm handling the "KEYDOWN" event for my window (WM_KEYDOWN). Should this event happen at every letter I type in an entry-box? Nothing happens.

Answer

No, you don't receive KEYDOWN when any control is activated. All of the controls siphon off KEYDOWN for their own use.

An ENTRY generates a "CHANGE" event when the text in it has changed.

A KEYDOWN happens only when no control in the window has the focus. (ie, The window itself has the focus).


Horizontal scrollbar on a listbox

When I add a long string (that extends beyond the right edge of the listbox), I don't see any horizontal scroll bar on the listbox.

Answer

The operating system doesn't show a listbox's horizontal scroll unless it needs to be shown (ie, there's a string longer than will fit in the visible area). But if the listbox refuses to cooperate, there is a message you can send it to tell it how much of a scrollable area it is supposed to have.

To tell a listbox how big its horizontal scroll area is you need to figure out how wide (in pixels) is the longest string in the listbox. This depends upon the size of font you use. Then, you send the listbox a SETHORIZONTALEXTENT message with this width. GuiAddCtlText has a feature to calculate the width of a string that will be added to a control, and it takes into consideration the font currently assigned to the control. If you pass a third of arg of "LENGTH" to GuiAddCtlText, then it will return the width and height of the text. Here's an example, assuming a listbox with an associated variable of "MyList".

/* Here's the longest string we'll add to the list */
mystring = "Here is a long string"

/* Calculate and return the width and height */
dimensions = GuiAddCtlText("MyList", mystring, "LENGTH")

/* Separate the width and height */
PARSE VAR dimensions width height

/* Send a message to the listbox to set its scrollable area to that width, plus a little space */
GuiSendMsg("MyList", "SETHORIZONTALEXTENT", width + 4)


Resize a View Control's column

How can I dynamically resize a VIEW control's column?

Answer

Send a SETCOLUMNWIDTH message:

GuiSendMsg("MyListView", "SETCOLUMNWIDTH", columnnumber, width)

where MyListView is the name of the REXX variable associated with your list view control, ColumnNumber is the column's number (where 0 is the first column, 1 is the second, etc), and Width is the desired width in pixels.

There are also two special values you may use for Width:

-1 = Automatically sizes the column.

-2 = Automatically sizes the column to fit the heading text. If you use this value with the last column, its width is set to fill the remaining width of the list-view control.


Get selected line number in Multi-line ENTRY

How do I get the line number the user selected in a multi-line ENTRY?

Answer

Send the control a GETSEL message to get the character offset where the selection starts, and then send a LINEFROMCHAR message, passing GETSEL's selection start offset. Something like this):

GuiSendMsg("MyMulti", "GETSEL")
GuiSendMsg("MyMulti", "LINEFROMCHAR", GuiSignal.1)
/* GuiSignal is the line number */


Replace 1 line of text in an ENTRY

How do I replace one line of text in an entry, for example, the third line?

Answer

Select all the text on that line, and replace it with new text.

line_to_change = 3

/* Get the character position of the start of this line */
GuiSendMsg("MyEntry", "LINEINDEX", line_to_change - 1)
linestartposition = GuiSignal

/* Get the line length */
GuiSendMsg("MyEntry", "LINELENGTH", linestartposition)

/* Select all chars on the line */
GuiSendMsg("MyEntry", "SETSEL", linestartposition, linestartposition + GuiSignal)

/* Overwrite the line with new text */
GuiSendMsg("MyEntry", "REPLACESEL", , "Some new text")


Get selected text in an ENTRY

I have a multi-line entry control. The user selects half of line 2, all of line 3, and first third of line 4. Is there a way of getting this data directly into a REXX variable? Or do I have to copy the selected text to the clipboard, and then do something like the clip.rex example script (on the REXX scripts page) to "grab" the text from the clipboard?

Answer

Unfortunately, Microsoft provided no easy, direct way to get only the selected text of an entry control. So your easiest option is to send the control a "COPY" message, and then get the text from the clipboard yourself. (One alternative is to have another, hidden entry control. You can send the visible one a "COPY" message, and then immediately follow up with a "PASTE" message to the hidden one. Then you can get the text from the hidden one with GuiGetCtlValue).


Set cursor position in ENTRY

I have a multi-line ENTRY control, and I add a lot of lines to this control. Assume I add 50 lines, but my control is only designed to show 15 at a time. The result is that only the last 15 rows are shown. I can scroll up to the first line using the scroll bar, but is there any way for my script to automatically scroll to the first line?

Answer

Send it a SETSEL, passing values of 0, 0:

GuiSendMsg("MyEntry", "SETSEL", 0, 0)

Then send a SCROLLCARET:

GuiSendMsg("MyEntry", "SCROLLCARET")


Selected text not shown highlighted

I send an ENTRY a SETSEL to select some text, but I don't see it being highlighted. Why?

Answer

The text is indeed being selected, but you don't see this because the entry isn't highlighting its selection. Why?

Normally, an entry highlights its selection only when the entry has the focus (ie, is in a state where the user can type text into it). If the entry doesn't have the focus, then it doesn't highlight its selection.

This behavior can be modified by enabling the "Keep selection" style (in the ENTRY's Properties dialog). Then the selection is always highlighted, even when the entry doesn't have the focus.

Your entry doesn't have the focus when you open your window, because it's not the first "focus-capable" control in the window. (The "SetText" PUSH button is). And you're not using the "Keep selection" style. And that entry doesn't have the focus when you select its text. So that's why it's not displaying its highlight. (And of course, if the user clicks inside the entry, instead of using the TAB key to tab there, then this removes the selection you set).

If you really want to see the selection, the solution is to either give the entry the focus before (or after) you set a selection, or use the "Keep selection" style.

If you pass GuiSetCtlPlacement only the variable name associated with a control, it will set the focus to that control. So you can do:

GuiSendMsg("MyEntry", "SETSEL", 2, 8)
GuiSetCtlPlacement("MyEntry")


UNFOCUS event and the ENTER key

For an ENTRY control's UNFOCUS event, the docs mention:

If the user presses the ENTER key in a single line ENTRY control, then it causes an "UNFOCUS" event. So you can simply handle this event to catch an ENTER keypress, in lieu of using a PUSH button with the default style.

But you'll also get the unfocus event when using the tab key to move from the entry to some other control, or just clicking on some other control. So how do you know the user really pressed ENTER, rather than just tabbing from one control to the next?

Answer

You don't.

But, it makes sense to respond to an ENTER key on a specific entry only if you need to know whether he changed the text, and to utilize any change, as soon as he makes the change (and before he changes some other control).

In that case, you'd also want to know whenever he changed the text, but then pressed the TAB key to move to another control (without pressing ENTER). This also causes an UNFOCUS event.

Otherwise, if you don't need to know to know about the change until such time as the user has finished setting up other controls in the window, then it's best to just grab the entry's value via GuiGetCtlValue() when the user finally clicks some "OK" button (or closes the window, or operates some other control that does require you to have the current contents of the entry).


Detect changed text in an ENTRY

Is there any way of determining if the user has changed the contents as of an ENTRY (other than saving the old value somewhere, getting the new value and then comparing them)?

Answer

Yes. For an entry, you can send it a GETMODIFY message as so:

GuiSendMsg("MyEntry", "GETMODIFY")
IF GuiSignal == 1 THEN SAY "It's modified"
ELSE SAY "It's not modified"

After you read the value, if you then want to mark the entry as no longer modified, you can send it a "SETMODIFY" message:

GuiSendMsg("MyEntry", "SETMODIFY", 0 /* 1 to mark it modified, 0 otherwise */)


Initialize a multi-line ENTRY

I had a single-line ENTRY control. Its associated REXX variable was "text". I initialized its value as so before opening my window:

text = "Some string"

It worked fine.

Then I changed it to a multi-line ENTRY. It doesn't initialize with the same string.

Answer

When an ENTRY is multi-line, its variable is considered a stem (not a simple) variable. Therefore "text" is considered a stem variable, and it needs to be initialized like so:

text.1 = "Some string"

And the same thing is true with a LIST box. When it's multiple select, its variable is considered a stem. When it's single-select, its just a simple variable.

Some controls need numerous pieces of information to make a selection, so they use a stem variable. Other controls need only one piece of information to make a selection. So they use a simple variable. And certain controls can be "single" or "multiple" select, so depending upon which mode, their variable may be considered a stem or a simple variable.


SPIN and buddy control initialization

I'm having trouble initializing a spin control.

MyEntry = 7
GuiSetCtlValue("Spin1")

These lines are executed after window creation.

Answer

I assume that "MyEntry" is the variable associated with the buddy (ENTRY) control. I think what you're missing here is that the SPIN's value is tied to whatever is in the buddy control. So, you need to set the buddy ENTRY control's value (ie, not the SPIN):

MyEntry = 7
GuiSetCtlValue("MyEntry")

And the spin should be automatically updated to the same.


Clear out a VIEW control

How do I "clear out" the existing items in a VIEW control, so I can add new items instead?

Answer

GuiSendMsg("MyView", "DELETEITEM")


Button label that spans 2 lines

How do I enter a push button label that spans two lines? I've tried the ||'0A0D'X|| trick but it didn't work.

Answer

If you're entering the text in the PUSH Properties dialog (ie, the "Caption" field), use a \n where you want the next line to begin:

Line 1\nLine2

Also, make sure you check the Multiline style.

If you're setting the text with GuiAddCtlText() then you use "0D0A"x, not "0A0D"x. The order of those two special characters is significant. And you still need the Multiline style.


Scroll a listbox

I have a list box to which I add (append) numerous lines -- more lines than can be viewed simultaneously. At some point, the listbox is "full" and subsequent items I add are "off the bottom of the page". The user has to scroll down to see newly added items.

Can I make the listbox scroll down automatically to a newly added item?

Answer

Yes, you can make a list box scroll. You send it a SETTOPINDEX message. The third arg is the line number you want to appear at the top of the listbox. (This is referenced from 0 -- the first line is 0, the second line is 1, etc).

/* Set the 10th line */
GuiSendMsg("MyList", "SETTOPINDEX", 9)


Set the color of a PROGRESS bar

I have a progress bar. I want to set its color, so I tried sending it a SETBARCOLOR message with GuiSendMsg. You need to pass the RGB colour (number). Where do I find out what value yields what colour?

Answer

An RGB value is a numeric value where the red value is from 0 to 255, the green value is 0 to 255, and the blue value is 0 to 255. You combine them like:

rgb = (redvalue * 65536) + (greenvalue * 256) + bluevalue

One way of seeing what Red, Green, and Blue values you need to yield a given color is to run the colordlg.rex script on my examples site.

Click on one of the "custom colors" boxes. Then click the "Define custom colors" button.

Now you can click on the large palette to get a coarse idea of the color you want, and then click on the rightmost color bar to finetune the value. You'll see the Red, Green, and Blue components displayed.


Show COMBO's dropdown list

I have a combo with a dropdown list. I also have a push button beside it. When the user pushes my button, my script fills the combo with various items. I'd like to automatically display the combo's dropdown list as soon as I fill in the combo (so the user doesn't manually have to open the combo himself by clicking on the combo's arrow button). Is there a way to do this?

Answer

Send a SHOWDROPDOWN message:

GuiSendMsg("MyCombo", "SHOWDROPDOWN", 1 /* A 1 shows it, or a 0 hides it */)


Change COMBO's dropdown area (size)

How can I change area (size) of the COMBO's drop down list?

Answer

To modify the dropdown area, you should be able to send the combo a SETITEMHEIGHT message like so:

GuiSendMsg("MyCombo", "SETITEMHEIGHT", -1, 100)

The fourth arg is the desired height in pixels.

But I wouldn't monkey with this. If you set it to show one line, then when you add more lines, it doesn't automatically expand to view more than one line. You get a single line showing. You have to go and change the height again. And changing the screen resolution may just give more headaches since pixel sizes are different at various resolutions as far as fonts are concerned.

Of course, you can set the COMBO's drop area size when you design your window. It's best to make it as big as you'll ever need it in that way.


Readonly COMBO

Can I change a COMBO style to "read-only", instead of just "disabled"?

Answer

For a "read-only" COMBO box, go to its properties dialog, and select the "Preset" Type.


TREE items not being hidden

/*
GUIBEGIN
WINDOW , 0, 0, 293, 303, POPUP|CAPTION|SYSMENU|THICK, , Choose one
 FONT 8, 400, MS Shell Dlg
 TREE 7, 9, 276, 271, BUTTONS|LINES|ROOT|SINGLE|REALHEIGHT|BORDER, , MyTree, , MyItems
 PUSH 258, 287, 30, 13, DEFAULT|TABSTOP, , ok_button, , OK
DEND
GUIEND
*/

tree_click = 0
myitems.1='Parent 1'
myitems.2='Parent 2'

/* Add children to Parent 1 */
DO i = 1 TO 3
 myitems.1.i = 'Child' i || '|HIDE'
END
/* Add children to Parent 2 */
DO i = 1 TO 3
 myitems.2.i = 'Child' i || '|HIDE'
END

LIBRARY rexxgui
GuiErr = "SYNTAX"
GuiHeading = 1
GuiCreateWindow('NORMAL')
again:
DO FOREVER
  GuiGetMsg()
  CATCH SYNTAX
    CONDITION('M')
    SIGNAL again
  CATCH HALT
  FINALLY
    GuiDestroyWindow()
END
RETURN

The HIDE option doesn't seem to be working for either of the parent items in the tree.

Also, I'm seeing some odd behavior with parent items not collasping when expected. Also the very first time I click on Parent 1, I have to click twice before it collaspses.

Answer

You aren't setting the HIDE option for any parent items. Change your code as so:

myitems.1='Parent 1|HIDE'
myitems.2='Parent 2|HIDE'

Don't bother setting HIDE on the child items, as that's now irrelevant.

As for the other issues, I don't see them on my system. But it appears that certain versions of Microsoft's common controls DLL may have problems with the SINGLE option. Try unchecking that property from the TREE.


Add an item/label to a TAB control

How do I add an item/label to a TAB control?

Answer

Call GuiAddCtlText. This is explained, and there's an example, on the "Controls Reference" page for the TAB control. Look under the subheading "Add labels". To remove an item, call GuiRemoveCtlText. Again, this is explained on the reference page for the TAB control.

For any question about a specific control, look on that control's page under "Controls Reference".

For an example of using a TAB control, see the tab.rex script on my Script Examples page.


Text limit for a menu item

My script crashes if a string with more than 116 chars is used in menu caption (ie, a file name for a MRU sort of list).

Answer

REXX GUI has a limit on menu strings. It really doesn't make sense to have a menu label that is so long. It makes a clumsy menu. The best thing to do is to trim off the path and just use the filename part, like RPC does.


Dynamically add a TIMER

When I add timer via...

GuiAddCtl("TIMER 4000")
...then my script aborts immediately like it doesn't know what to do.

Is there any special place in the script I should or shouldn't do this?

Answer

Yes, make sure you don't add the timer until after you create your window with a call to GuiCreateWindow.


Hide the scrollbar in an HTML control

How can I remove the scrollbar from an HTML control?

Answer

If you're setting an HTML string, then you can remove the scroll bar by adding a <BODY scroll="no"> tag to the start of your string, and a tag to the end of it. For example:

myvar = '<BODY scroll="no"><B>This is a test.</B></BODY>'
GuiAddCtlText("MyHTML", myvar)

In addition to "no", you can use the following in the BODY tag:

"no"     (No scrollbar)
"yes"    (Always a scrollbar)
"fixed"   (A scrollbar is displayed only when needed. This is the default option)