DrDialog, or: How I learned to stop worrying and love REXX - Part 13

From EDM2
Jump to: navigation, search

By Thomas Klein

In this part of our series, we won't directly get back to DrDialog programming (contrary to what I said in the last part - sorry). I think it's best to provide you with a "smooth return from pure REXX" to DrDialog's development facilities: A long time has passed since we've dealt with the development environment and the principles of how the whole matter works. In addition, today we'll have a rather "theoretical" discussion about specific properties of DrDialog and - yes - I'll introduce you to what I've chosen to become our sample program that we'll be creating together in the upcoming parts of the series. For that reason, it's a good idea to take another look at how DrDialogs workspace is organized. It'll save us time and "gray cells" if I can assume you to be familiar with where the right buttons are....

Before proceeding, I strongly recommend that you read Chris Wohlgemuth's letter in the previous issue of the Newsletter concerning the rexxutil functions! Thank you Chris for sharing your wealth of knowledge and experience with us! Especially when dealing with computers and programming, we all should know that the there's one thing that applies for you, me and all of us - regardless of how "good" we consider ourselves to be in whatever matter: You live and you learn. This time, thanks to Chris who gave us the chance to do so. And again, I can't keep myself from repeating it again and again: Feedback is always welcome! If you're an experienced programmer (or user) and come across something that you feel is not accurate or that you could provide us with additional knowledge, you're welcome to go for it by writing a comment to either me or the editor - like Chris did. By the way: Did I mention that he's the only developer left "out there" who still extends the functionality of DrDialog? We'll talk about his set of functions in an upcoming part of this series.

The short (re-) introduction:

In the very first parts of the series we already learned about how to use DrDialogs development environment. For this part (and all remaining parts) we first need to settle on a common ground for dealing with the development environment.

I assume that you know how to access DrDialog. Let's make sure that you also know how to access the following parts within the development environment:

Mnu run.png the run time monitor window
the controls window the controls window
Mnu code.png the code notebook (or "code editor" if you prefer)

All of the above windows are available from DrDialogs main window menu item called Tools:

the tools menu

Note that you can select to leave each of them opened (floating) and even let DrDialog open them automatically when the IDE (integrated development environment) is started. This is done for each window by using its system menu:

the system menu extensions

The dialogs window:

the dialog windowIn addition, when dealing with multiple dialogs within the same program, you'll need the dialog selection window as well. This one is accessed by using the button (shown to the left) from either the tools menu or the tools window. The other windows (like color selection etc.) aren't necessary in general for now.

All code is created equal... but some code is more equal than other

So far, we learned about the style settings for a dialog and what they mean. But this is so-called "design-time" stuff. In order to handle multiple dialogs in our program, we need to know what can be done with a dialog at "run time" - from code that's to say.
In order to understand DrDialogs behavior, we first need to understand, that almost everything we code in DrDialog is attached to an event handler in one way or another. What happens within DrDialog when a program is started? Where does it start within all those event-related "loose ends" of code? How does it terminate? Again, in order to understand, we need to distinguish between two "types" of code in DrDialog:

  • event handlers
  • global code

As its name implies, the event handler code parts are attached to an event and are executed automatically each time that the event occurs. More importantly (unlike in VisualBasic for example), an event handler code cannot be called from somewhere manually unless you actually "trigger" the event if possible (like opening a dialog window to trigger the INIT and OPEN events).
This means that while we can refer to a specific method or property of a control from anywhere in our code, we can't call an event handler directly... "Pardon?" you might say... okay, here's an example:

We want a dialog that displays the current time in a text field. So at load time of the text field, we simply make it contain (display) the current time by using the REXX function call time(). In the INIT event handler of the text field (assuming its name to be txt_tst) we thus code:

call txt_tst.text(time())

You might give it a try: Wow! It works.
Now at some point in the program, we discover that quite a bit of time possibly has elapsed since we started the program and decide to include an "update time" button beneath the text field. All it should do is do the same as the INIT event handler of the text field does: Make the text field contain the current time.
Fine - we're programmers and we're lazy and we're thrifty - so why not simply call the INIT event handler in the pushbuttons CLICK event? Oops... that's where we're stuck: There's no call to do so. The INIT handler is only called by DrDialog's event handler queue system (or "message queue" in brief). Okay, okay...: So let's do it the other way round and include the actual code in the pushbuttons CLICK event. Then, change the INIT handler of the text field to call the CLICK event of the pushbutton... and: Darn - stuck again...

Well, this is not tragic: It's only a single line of code. We could simply duplicate it and put it in both the text INIT and the buttons CLICK event... but... what if there's more code than a single line? Do you really want multiple copies of entire paragraphs in your program? Looks somewhat silly, right? In addition, if you find a bug in that routine, you'll have to correct it multiple times. That's not "smart." And that's where it comes to the global code part:

The global code contains subroutines and/or functions which are not attached to events. In addition, they can be called from code manually and are accessible from everywhere in your code - regardless if you find yourself in an event handler somewhere or in another global code part.
The solution is to put the time()-code in a separate subroutine. Then, in both event handlers, we simply call that routine. In order to create such a global function (or subroutine), we need to switch the code editor from the event-related parts to the global parts by clicking the "globe" icon:

Code global.png

The code editor will now display tabs for all global routines defined so far. If there are none, you'll see an empty window. The entry field at the top of the window is used to type in the name of the function that you want to edit or create. Type in the name you want the function to be called - e.g. "set_curr_date" (without the quotes) and hit return. You'll notice the code editor to create a new tab with that name.

Set curr date.png

In the code entry field you then can type the code for that function. In our above case it would read

call txt_tst.text(time())

...wrong!
This routine is global. Thus, it can be called from anywhere in our program. In effect, this means that the routine doesn't know about the current "control context" that it'll be run in: It needs to know the "full qualified name" of the control it should deal with - that's to say: The dialog which contains the control plus the controls name in order to be able to access that specific control. We thus need to prefix the controls name with the name of the dialog that it's contained in. Assuming the dialog being named MyDialog, it must read

call MyDialog.txt_tst.text(time())

Fine. Now you might remember that we don't need to name the dialog. In this case, we can still refer to it by its ID number in the form of "D100" for example ("D" is the prefix for a dialogs, 100 is the ID number). The id number of a control (or dialog) is displayed in the bottom status line of DrDialogs background panel once the mouse pointer is on top of the corresponding control:

Ctrl ids.png

If the text field has no name, it can be accessed by its ID number as well: Controls wear a prefix of "C" along with their ID number. If the text fields id number would be "101", the full qualified name would be:

D100.C101

and thus, the global function code would read:

call D100.C101.text(time())

Now that your global function code is complete, you don't need to "save" it someway (except of course before you close DrDialog or edit another program). DrDialog stores it automatically once you switch from the function to another part of the code editor. Note that if you don't enter any code, DrDialog will automatically remove the (empty) function from the list of global functions. You'll need to recreate it - but as there was no code in it... no problem.

Before going on, let's see how to call that new function from within both event handlers: All you need to code at this point is to put

call set_curr_date

in both event handlers. Done.
This might not be the perfect example to demonstrate all benefits of a dedicated function. Simply imagine that there's dozens of lines of code instead just one. Also note that we have complete control of that function and that this allows us to make it support arguments if we want to do so. This means that we can modify the function to behave in different ways if called with different parameters.

An X-ray of DrDialog

This was an example for a global routine/function and how it is created. Now that you know the differences between event handler code and global routines... let's get back to the initial question: How does a DrDialog-made program actually start?

Well, DrDialog first looks in the global part, if there is a routine called INIT. If there is one, it'll be executed and thus will become the "initial" event handler if you want to put it that way.
If there is no such function, DrDialog will load the dialog window with the smallest "id number". As the existence of a dialog window starts with an INIT event (because the dialog window is first INITialized before being displayable) the dialogs INIT event handler will be the "initial" event handler of your program.

How about the program end? There's two ways: a "manual" way and DrDialog's "built-in" way.
The built-in way consists of DrDialog checking the dialog window list. When the last existing dialog window is closed, the program will be terminated.
The manual way consists of a single command: "Exit" (without the quotes) will make the program terminate. Caution: This will explicitly terminate the program immediately - so please behave in regard of opened files if any! Don't issue the exit command from somewhere deep inside the programs code. That's what sluggish programmers do. You should always try to quit your logic in a "controlled" fashion.
One never knows of course, but actually with DrDialog you shouldn't find yourself in the need of that command if your program's logic is "intact".

But as with every rule, there's an exception:
Assuming you don't want any dialog windows in your program because it simply does batch processing, you should rather use "pure REXX" instead of DrDialog. If, however, you still prefer to use DrDialog, you're faced with a special property of it, that we already discussed in one of the initial parts of the series: DrDialog automatically creates a default dialog window. If you remove it, it will create a new one.
This is due to DrDialog being entirely event-driven, thus relying on a dialog to start from. You're free to set its style to "not visible" and simply do all the processing based upon a set of "global" routines and the global INIT routine as your starting point. But this won't work, because the program won't terminate with the completion of the last command of the INIT routine. Instead - once the last command was completed - the "neglected" dialog window will appear, although initially being set invisible. Oops. In order to overcome this "problem" use the EXIT command as last command in your command sequence: That'll learn it! Or better: Simply rely upon "pure REXX" if you don't need any dialogs.

So this is what needs to said about working without any dialogs. But what, if you need multiple dialogs in your program - like a main screen, an options dialog and maybe a "splash" page (or logo/about screen if you prefer)? What needs to be considered?
In this case, we need to know how to control dialogs from code: opening them, closing them, making them "lock" the program to a dialog shown in front of others. To be able to cope with our expectations, we must know how DrDialog behaves in matters of dialog control and event handling. (This is the "theoretical" part mentioned at the beginning. Although being not really easy to read, you should work yourself through it to understand some basic principles of how DrDialog works...

DrDialog is event-driven and uses a message (or "events") queue. In general this means that events are stored in a queue and are processed based on the order that they appear in the queue. That's what other programming languages will do in a multitasking graphical enviroment as well. What's special about DrDialog is the way in which this queue is "embedded":
I'll try to explain it my way - if there is somebody out there who knows a better (or "right?") way to describe it, he's welcome to let us know. Well, okay... DrDialog uses the operating systems rexx interpreter, which means that there's only one single queue for it. Because the GUI-related events are processed by DrDialogs internal runtime which runs in the same environment, the same queue is shared for processing rexx-commands as well as events. This leads to some kind of different behavior in matters of "synchronicity". If you take VisualBasic for example, you'll notice that commands and events are treated the same way in matters of program sequence: If in a global routine or somewhere else you OPEN a dialog from code, VB will immediately invoke the whole processing in a nested way and then return to the command following the OPEN. To give you an impression of how this would look like, here's an arbitrary command sequence:

- some command 1
- OPEN Dialog "A"
- some command 2

VB would process it like this:

- process some command 1
- Call to OPEN dialog A:
-- process INIT event handler of dialog A
-- process OPEN event handler of dialog A
- process some command 2

Don't worry - you'll see what this is about if we now check the sequence that DrDialog will use.

DrDialog uses a different approach. Because both the REXX code of your program and the possible events share the same queue, the REXX commands of the current procedure have "priority" over the events triggered. This means, that DrDialog will queue all GUI-related events that refer to different dialogs for later execution - while going on with the REXX statements of the current event handler. In our above example, we had the following sequence of commands:

- some command 1
- OPEN Dialog "A"
- some command 2

DrDialog goes like this:

- process "some command 1"
- append "OPEN dialog A" to the queue
- process "some command 2"
>handler finished<
- Retrieve "OPEN dialog A" from queue
-- process INIT handler of dialog A
-- process OPEN handler of dialog A

You get the difference? In practice, this behavior leads us to the following problem:
As we're used to think of processing in a "procedural way", we assume it to be feasible to make a program sequence wait for a user input. While this is right, it won't work that way with a dialog of DrDialog. So if you require user actions (like inputs) from a dialog in order to proceed a specific sequence of commands, you can't simply invoke a dialog, wait for the user to click some "ok" button and then return to the calling procedure, because DrDialog will first complete the current procedure before displaying the dialog window.

Making a program wait for a user input is batch programming by the way, and DrDialog is event-driven. Thus, the right approach to solve the problem is not to find out how-the-heck you can make the processing wait for the user input... but rather start the processing from off the user input, thus: From off the dialog!

Another reason behind this is, that we are running multitasking and multithreading operating systems. Instead of having a program that waits and waits for a user input (which means that it somehow consumes cpu resources) it should rather do nothing (giving cpu time to other tasks) until the user requested some kind of action.
In practice, this would mean that you display the password dialog and stop. That's it. The remaining process will be triggered once the user clicks the <OK> button. Thus, the processing is not part of some routine that implicitly displays the password input dialog, but rather part of what happens when the user has completed password input. Know what I mean? ;)

The big stuff

Okay. So far, this also somehow applies to having to deal with a single dialog. Now here we go with something more interesting....

Imagine that you came up with the idea of a new personal information manager program. You actually don't have any clue what it will work like in the end but you already know, that you need

  • a main screen (for doing the "boring" stuff like input-output of dates, notes and contacts...)
  • an "about" screen (for showing a fancy logo and copyright remarks... yeah)
  • an "options" screen (for those who are never satisfied if there ain't nothing to change)
  • and finally a password input to restrict access to some files

You should start by figuring out, how these dialogs depend on each other and the sequence in which they will appear when the program is started. In order to complicate this task, we assume that you want to do the following:

  1. When the program starts, you want the "about" screen to show the logo and copyright along with a message like "loading - please wait" until all settings are loaded from INI-files.
  2. The logo should always appear in the middle of the screen, regardless of resolution.
  3. The logo screen should display messages about the loading process (like "loading settings...", "preparing screen...", etc.
  4. During the loading process, you want the main screen to already appear "inactively behind" the logo screen
  5. When the loading is done, you want the logo screen to disappear and activate the main screen
  6. If the user opens an existing file, you require him to enter a password that is needed to do so
  7. If the user passed a filename over the command line (as a parameter to the program like via a WPS object) you want the program to automatically load that file and present the user with the password screen.
  8. If the user can't provide the correct password and clicked "cancel", you want the program to remain in the previous state, which means either file previously opened remains the open file or there is no open file (which means: The user can't do anything in the program except editing options, close the program, create a new file or try to open another - password-protected - file).
  9. Finally you want the logo screen also to be the "about" screen that is presented to the user if he selects "about" from the main screens "help" menu item. In this case, the logo screen should remain open until the user clicks on an "OK" button.

That's a lot. And that's just a subset of the stuff compared to the "boring" parts like reading, writing and the presentation of data.
And that's execatly what we will be doing as our sample program.
Gee... poor me! That'll cost me another dozen of articles! Why didn't you come up with something easier? ;)

"Global" variables:

Before leaving you with great expectations and lots of questions, let's have a look at another bit of knowledge about DrDialog:
Now that we know that we can't call event handlers "manually" like other functions, it might lead us to another question: How to pass information from one dialog to another? This would be great like for example opening a dialog and passing it some parameters like position, size, title and so. No way. At least, no direct way to do so.... The answer is: Global variables.

In most programming languages you will find special ways of "telling" if a variable is available to the current function only or from anywhere within the entire program (= being "global"). In DrDialog this is kept very simple: All variables declared by you are "global" automatically. It doesn't require specific commands or syntax to make a variable become "global". But beware - there's no way of making them become "local" in return: If you alter the contents of a specific variable in one event handler or function, it'll be changed "everywhere".

Would you like a quick-n-dirty sample? Here's one:
Start a new program in DrDialog. The default dialog will be created automatically. Now, we'll create an INIT function in the global code part (see above how this is done) and make it contain the following command:

MyDataField = "Hello"

Then, we'll dragdrop a pushbutton onto the dialog. It'll read Push. Now double-click it to bring up the code editor with the event handlers for that button. Switch to its INIT event handler by clicking on the appropriate tab. Then enter

call text MyDataField

and run the program. You'll notice that the pushbuttons text will become the contents of the "global" variable we set up in the INIT routine of the program.

That's it for this part. I hope you're looking forward to next month. If you ever wanted to know how to make a dialog become "application modal" (locking the program to one dialog only) in DrDialog make sure to be part of the party! ;)