DrDialog, or: How I learned to stop worrying and love REXX - Part 6
By Thomas Klein
Sorry for skipping an issue in our series last month, but here we are: The container control.
Now, as I told you before, this control is very special. It contains a wealth of GUI comfort and is incredibly versatile and useful, both visually "appealing" and providing comfortable functionality for the end user.
Unfortunately, this requires a lot of information, data and control to make it do what you want. Finding a good start into the bunch of methods, events and commands that is supported and/or required is not that easy at first sight. Today, I'll try to actually give you a structured guide to what is contained in the help files. Along with this reading, you'll find a sample application that gives you a first glance on most of the tasks when dealing with a container control.
(This sample application is also used to provide most of the examples and screenshots in this article.)
Basically, from a end-users point of view, the container control represents a sort of "folder". It can be used to view entries in detailed view, multi-column view or even as a hierarchical 'tree'-type structure (and more). The special thing about it is, that you (as the developer) have complete control over all its features: What it shows, what it does and how it reacts to user interaction. The drawback about this on the other hand is, that you must deal with all that, as the container control does nothing on its own. ;)
It's not automatic like e.g. pointing it to a directory and here we go - nope. In such cases, the programmer (you) would be forced to retrieve everything that's contained in a directory "manually" and feed it into the container control.
To give you an overview on what can be done with the container control, we'll figure out some kind of small address manager (or buddy list if you want). It contains several entries that are made up of name, an associated bitmap and additional data like phone number and email-address.The different types of view that can be achieved with the container control are...
Entries are represented by their associated label only, in a single column
Same as TEXT but multiple columns
Entries are represented by their associated label along with their associated icon on the left, in a single column
Same as NAME but multiple columns
Like "icon view" in workplace folders: Entries are represented by their bitmap with their name centered below it. A 'grid' algorithm is used to arrange the entries within the container.
Like "details view" in workplace folders: Entries are represented by a row in a table-like view.
This view can be made up of one or more columns in either a single window or a two-part view that shows a movable, vertical split bar to size the window parts.
Entries are represented like in NAME, but in addition it shows a parent/child relationship of entries by indenting child entries to the right below their respective parent entries. Parent entries will show leading bitmaps that can be used to expand or collapse the view to show the parents child entries (if any).
Like "tree view" in workplace folders and actually the same like OUTLINE but with lines leading from a parent entry to its according child entries.
To give you a little "appetizer" on dealing with container controls: Changing from one of the views shown above to another just takes one line of code - a single statement...;) Of course, there's some work to be done prior to it - like actually "filling" the container.
The amount of work you need to face is depending on the type(s) of view you're going to provide to the end user: A "simple" view type like BITMAP requires only a single and easy statement to add an entry to a container... whereas - as you might imagine - the DETAIL view requires a lot more data because of its extended format and the amount of data it is capable to show. The same is true for the hierarchical view types HIERARCHY and OUTLINE. While they still only display bitmap and text of an entry, there is need for additional control of who's the parent of who. But once you provided the necessary data it's fun to work with containers.
But before going into details on how to add entries to a container, let's talk about some general information...
Basics of end-using a container control
Before drag-dropping a container control onto your dialog, you should be aware of the bunch of work that you might be faced to, according to the way the user is intended to use it... to give you an impression of what I'm talking about: Imagine an address book application that is built upon a detail view container control like the one shown above.
What is going to happen if a user adds an entry? Quite simple: You pop up a "new entry" dialog and add the new entry to the container afterwards - so far, so good. Now... what happens if the user selects to delete an entry? This might happen due to a context menu (pop-up menu) selection, a "del" keystroke event for the control or whatever process that you need to figure out. In addition, if a column was not defined as read-only (we'll talk about that later) the user is able to change a single column data of an entry by clicking on it while holding down the ALT key. The container control will handle the steps necessary: Change the display "cell" into an entry field, accepting the new data - but regardless of what is happening in the user interface YOU need to provide the steps that take care of storing the new data into your file...
To sum up: You should know the capabilities of the control in matters of usability and draw some kind of user interaction guidelines from it: How do you want the user to add, remove or edit entries? Okay - here we go with events and methods (functions, statements) for a container control:
Events for a container control
As a container control contains (as its name implies) multiple objects, it might be interesting to know the actual object that was e.g. changed or selected. Such information indeed exists, but it must me retrieved first using a special function named EventData. The syntax for EventData is
call EventData (stem)
where stem is the NAME of a stem variable. If you want to receive the EventData information into a stem named "myvar", you must specify
call EventData ("myvar")
Yes - including the quotes!
Now, if you don't know stems you might wonder what that's about. Let me put it this way: A stem is a sort of one-dimensional array (a list) of variables that all have the same "root" name while being identified through an index number. The notation used to deal with stems is quite easy. First, there's the "root" name (the stem name), then a dot, then either the actual index of an entry or a variable that holds the index' value. If you want to create a stem named "myvar" that is made up of three entries, you would simply do:
myvar.1 = "first" myvar.2 = "second" myvar.3 = "third"
Then, you would put the upper bound of that array (the number of entries) into the entry 0:
myvar.0 = 3
This last statement is not mandatory if you set up stems manually, as you know how many entries are contained in your stem, but it's a common method in rexx - even the system functions that are e.g. used to search files will use entry 0 to specify the amount of entries a stem is made up of. This way, the programmer can use entry 0 to determine how many entries are actually returned into the stem. This is absolutely required for looping through the entries for example. Anyway - this should be enough to get you a first picture on stems and we'll concentrate back on the EventData function (we'll get back to stems in a later part of the series).
Okay, let's check the EventData syntax again... this is what is stated about it in Drdialog's help:
EventData( [stem] )
Returns the list of data items associated with the
current event in the stem variable specified by stem. If
stem is omitted, it defaults to EVENTDATA.
The number of data items returned is specified by
stem.0. Stem.1 through stem.n contain the individual
data items associated with the current event. For a
description of the data returned by a specific event for
a particular control type, refer to the Controls section.
First, note that actually no stem variable needs to be specified. In this case, the system uses a built-in default to provide a stem variable named "EVENTDATA".
Second, as I mentioned above, stem entry 0 is used to specify the actual amount of entries in the stem.
Now there's something else we should make clear before finally taking a look at the events.
While the items (entries) of a list or combobox are referred to by a simple index scheme like 1 = first entry, 2 = second entry and so on, the items in a container use a different system. Each item has a unique "id" that is created by the system when the item is added to the container. These identifiers need neither to be in sequence, nor ordered. Don't try to use them for calculating something. They are actually only meant to identify items within a container and have no significance "outside" the container. In addition, when the same program is run a second time, the same items will show different ids, which makes them completely unusable for e.g. database record identification or any other "static" references... We'll get back on how to deal with that later - this paragraph was meant to get you to know the item ids, so that you're prepared for the events discussion. Note that the examples below are based upon the assumption that no stem variable was specified and thus the "built-in" variable EventData is used instead.
|for the container control event...||CALL EVENTDATA will give you...|
The user has changed the value of a container item field
The user has either pressed the ENTER key or double-clicked in the container
The "emphasis" of a container item (select, mark, cursor) has changed
The user has requested a context-sensitive menu for the container (by clicking mouse button 2)
Of course there's some more events supported by the container control, but those are more "simple" and don't require you to retrieve additional information:
|Init||The container control is initializing (during the dialog open event)|
|Scroll||The container control has scrolled|
|GetFocus||The container control has been given the focus (= has become te "active" control within the dialog)|
|LoseFocus||The container control has lost focus (= another control in the dialog has become "active")|
|Drop|| An object was dropped on the container control|
This requires you to use the EVENTDATA call in conjunction with the DROP event to retrieve more details about what's going on - we'll deal with the drop events in a later part of the series...
A final example on how to use The eventdata stuff: If you want to react on a changed event, you need to
- select the CHANGED event handler of the container control and
- code the following:
say "ID of item is" EVENTDATA.1
say "change occurred for" EVENTDATA.2
- or, if you prefer to use a stem variable on your own, code
say "ID of item is" myvar.1
say "change occurred for" mydata.2
Don't worry, we'll get back on "what's next" later...
Okay, great. Now that we know the events for a container... how the heck do we actually put items into it? ;)
Here's for the methods (or "functions" if you prefer) - note that there's some more methods that are applicable to other controls as well like visible and so on. Because of the fact that we already discussed them in the previous part of the series, we'll focus on those who are somehow "special" when dealing with containers:
The VIEW function
Let's start with the VIEW function. It is used to set up the "look" of your container (remember the screenshots above? That's what it's used for). To set up a container named "mycontainer" as a single-column list of names (labels), you go with
call mycontainer.view "Text"
Note that you optionally only need to specify the first letter of the view type - thus, if you code down
call mycontainer.view "T"
will do the same as the statement above. Setting up a view type ideally is done during the INIT event of the container. In addition, when using the containers INIT event handler to set up the view, you don't need to specify the controls name, because the current "context" of the program is stuck to the container (because of the event handler), so you can narrow down the whole stuff to
call view "T"
Now that you know how easy the VIEW function can be, let's check its entire syntax:
oldview = container.view([type], [title], [bitmapwidth, bitmapheight], [expandbitmap, collapsebitmap])
Phew! First, let's recall that such functions can be used to SET something, GET something or do both actions at once. You might use the "function notation" that returns values to GET the current settings of a view. But honestly, this is not what we are interested in at the moment. We rather would like to SET something, so let's use
call container.view [type], [title], [bitmapwidth, bitmapheight], [expandbitmap, collapsebitmap]
As we already know, the type parameter holds the view type we want to achieve ("text" in our above sample).
The title of the container control ("buddylist" in the screenshots) is a string that you want to put above the actual contents. Note that some special leading characters can be used to format the title:
|| ("pipe" character)||Title will be centered|
|_ (underscore)||A separator line will be drawn between the title and the contents (items) of the container|
| indicating the end of the formatting character sequence, all following characters will be used as title text|
This is optional - if not specified, the title text will begin with the first character that is not recognized as a formatting character (a character which is not included in this list)
If no formatting characters are specified, the title will be centered with no separator line. If you want to have a title "This is my first container" written above the contents, have it left-justified and with a separator line, the [title] parameter for the view call will be:
"<_This is my first container"
Okay, the next parameter is [bitmapwidth, bitmapheight]. This can be used to specify a size, to which all of the item bitmaps of the container will be sized. If this optional parameter is not used, all bitmaps will be shown in their respective size. If 0 (zero) is used for both dimensions, the system bitmap size is used. I think this is in pixel units, as no explicit information is contained in te help files. Don't blame me for not testing it... ;) To make all bitmaps display as 24 by 24 pixels regardless of their actual size, the parameter would look like "24, 24"... as far as I am correct...
Next, we have [expandbitmap, collapsebitmap]. These optional parameters are used to specify alternate bitmaps used to display the bitmap button contained in a tree view type (outline and hierarchy) which the user can use to expand or collapse (close) individual "branches" of the view - you might know that from a drive folder in tree view, when you can click on a "+" sign to open a subdirectory view... If the parameter is omitted, the default built-in bitmaps (plus and minus sign) will be used.
Bitmaps in DrDialog are specified in two ways - either by a file name like "c:\mybmps\bitmap01.bmp" or by using a special notation to use bitmaps contained in a dll - we'll talk about this later. DrDialog comes with a BITMAP.DLL that holds several bitmaps. To use bitmap number 52 of this dll, you would write "bitmap:#52".
To keep things simple, we don't care about these features at first and just use defaults where possible. A complete VIEW call would look like this for example:
call mycontainer.view "Text", "<_This is my first container"
As we should now by now, square brackets in syntax notation mean "optional". Thus, if you want to change a containers title without changing the view type, you simply omit the first parameter but not it's comma so that the programming language "parser" knows that you actually OMIT a parameter. In this case, the statement would be
call mycontainer.view , "<_This is my first container"
Looks a bit strange at first, hm? But if you don't specify the single leading comma, the parser will assume "<_This is my first container" as the first parameter, thus being the view type... that's why the comma is quite important. And here we are: Now you're able to set up views and play with the title formatting. Next, the real important statement you all are waiting for...
The ADD function
...is used to add a new entry into a container. It's a function that returns a value and - of course - can be used in a CALL notation to simply add the entry without caring about whatever it returns, but we should take a look at the return value, because it's the new entry's ID. This ID is used in conjunction with other statements, even with additional add-calls: When creating hierarchical views (hierarch, outline), it is used to specify the "parent" that you would like to receive child items for example.
If you set up a "simple" type of view that is made up of labels (and optionally bitmaps) only, you might prefer to use
call mycontainer.add "New entry"
or with a bitmap named "new.bmp" contained in C:\mybmps...:
call mycontainer.add "New entry", "C:\mybmps\new.bmp"
To achieve more control over the adding process (like obtaining a sort order) we need to take a look at the syntax of the add function (this is my personal version, slightly different from the one mentioned in Drdialog's help):
newid = mycontainer.add(label [,bitmap] [,where] [,reference-id] [,data])
|label||is the label (or title, name, caption) that shows up for the entry in the container|
|bitmap||is the bitmap displayed along with the items label in name, flowedname and bitmap view|
|where|| determines the sequence for inserting the new item.|
This can be either "First", "Last" or "Next" and it is partly used in conjunction with the next parameter reference-id.
If reference-id is not used, "first" will insert the new item as the first one in the container and "last" will make it be the last one (this is the default if you don't specify where).
If however reference-id is used, "next" will make the new item be inserted after the one specified by reference-id, whereas "first" and "last" used in conjunction with reference-id will make the new item become the first or last child item of the one specified by reference-id
|reference-id|| Refers to an item that is already in the container|
This must be the id that the already existing item was given by the system at the time it was added to the container
|data|| This is an optional parameter useful for storing any kind of data along with the new item.|
Hey! This can be used to store the database record key (or equivalent) for the new entry...
I know that where and reference-id are quite "clumsy" both to explain and to understand... let me put it this way: Unless you're dealing with parent/child-relationships in hierarchical view types, you actually don't need to deal with reference-id. Neither do you need the "first", "last" or "next"-operands for the where parameter. If you need to provide a sorted container, simply do the sorting somewhere else, then add the items in the sorted order without having to rely on where and reference-id.
The way I do it is to use an "invisible" list box on my dialog that already comes with built-in sort algorithms in its add function. Then I simply read the sorted list and make the appropriate entry being added into the container.
However - when it comes to adding new entries after the container is populated you're better off with using the parameters described herein, but we'll talk about that later, as retrieving data about the items in a container requires additional functions... the same is true for "z-order" issues, that you might have read about in the help file. This is not mandatory to talk about now. Let's go on.
Detail view containers
In detail view, we have to deal with two more issues than simply use the view and add function... we need to set up the columns and we need to supply the data that is contained it the "cells" (in a column field of an entry). Both tasks are accomplished by using the SetStemfunction. The concept of SetStem is to set up a collection of data for each column (format or data content), then passing all the information at once. Luckily we already know stems in basic, hm?
As usual, we'll first take a look at the syntax, then strip off the parts we don't need now and look at how to use the rest...
call mycontainer.setstem [stemname] [, "[+/-]SELECT"] | [,"[+/-]MARK"] | [,"FORMAT" | 0 | item]
This syntax in a more readable way is made up of only two parameters:
call mycontainer.setstem [stemname] [,action]
[stemname] Is the name of the stem variable that holds the values for the action to be done. If you don't specify an explicit variable on your own, a stem variable named STEM is expected to hold the values, and this is why the parameter is shown as "optional" (in square brackets).
As for the action to be performed, there are three possible values for it:
|"[+/-]SELECT"|| means either "+SELECT" or "-SELECT" and is used to select or unselect all items of the container whose ids are specified in the variables STEM.1 through STEM.n|
STEM.0 specifies the number of stem "entries" - the array boundary if you want to put it this way
|"[+/-]MARK"||means either "+MARK" or "-MARK" and is used to mark/unmark all items of the container whose ids are specified in the variables STEM1. through STEM.n - again, STEM.0 specifies the number of stem "entries".|
|"FORMAT" | 0 | item||
This parameter specifies the actual "action" to be performed using the stem data.
We won't discuss the SELECT and MARK stuff now but focus on the third one. To set up a container in detail view, we need to call the VIEW statement which will give it a title (or caption if you prefer) as well as actually switch the view mode to "detail". Once this is done, there are three things left to be done to set up the entire container:
- Specify the number of columns and their format
- Specify the title of each column
- Add the entries and supply the data to be displayed in the columns
And this is exactly what setstem is used for in it's third "meaning":
- call setstem "formatstem", "FORMAT"
will use the values of a stem variable named "formatstem" to set up number and format of columns
- call setstem "titlestem", 0
will use the values of a stem variable named "titlestem" to set up the column titles
- call setstem "datastem", itemid
will use the values of a stem variable named "datastem" to supply the data for each column of the item whose id is contained in a variable named "itemid" (without the brackets of course)
So far, so good. Before going into some examples, we need to take a look at how column formats are set up. Each entry in the stem variable will carry the format for a column and this is accomplished by a format string that is made up of a combination of characters with special meaning:
|the character...||will result in...|
|= (equal sign)|| ...the column display a bitmap instead of text.|
If this character is not found within the format string for the column, the column will display text. (Thus, the default for the column is to display text, except if this format character was used)
|~ (tilde)|| ...the column being invisible.|
This is quite useful if you need to store data along with a container item but don't want this data to be visible to the user
|X|| ...the column being read-only.|
By default (if this character is not used), the column value of an item can be changed by the user by holding down the ALT key while clicking on it. However, editing a column value in such way is applicable for columns only that don't show a bitmap.
|. (dot)|| ...the container having a movable, vertical split bar to size two parts of the container with this column being the last (most right) one in the left part of the split window.|
If this character is not used in the formatting string, the container won't show a split bar in detail view.
If there is more than one column that is formatted with a dot, the most right one will be the one to show the split bar.
|^||...the contents of the column being top-aligned.|
|V||...the contents of the column being bottom-aligned.|
|- (minus sign)|| ...the contents of the column being vertically centered.|
This is default - if neither "^" nor "V" nor this one is used, the contents of the column are vertically centered within each row.
|<||...the contents of the column being aligned to the left.|
|>||...the contents of the column being aligned to the right.|
|| (pipe character)|| ...the contents of the column being horizontally centered.|
This is default - if neither "<" nor ">" nor this one is used, the contents of the column are horizontally centered.
|_ (underscore)|| ...the column title having a separator line below it.|
(note: the separator line will not cover the entire column width, but just the width of the title text)
|! (exclamation mark)|| ...the column having a vertical separator line drawn on its right side.|
To make a column simply display text that is both vertically and horizontally centered, no formatting is needed as this is the default.
Whereas, if you want a column to display text left-aligned, being read-only and have a vertical separator on its right edge, the formatting sequence would look like this:
...seems like some sort of customized "smiley" at first, but it's very effective! ;)
Okay, folks. Now that we've gone so far... let's check how to set up a container with detail view. I'll show the statements necessary to produce an output like the one shown in the screenshot at the beginning of this article. Note that lines having a leading sequence of "/*" and ending in the reverse way"*/" (without the quotes of course) are considered as comments by the rexx parser. The following sample is based upon the assumption that you provide a container control named "cnt":
/* this is all intended to be used in the 'init' event handler of the container */ /*------------------------------------------------------------------------------*/ /* setting up the container: detail view, title with separator */ call cnt.view "D", "_buddylist"
/* Now let's got for the columns. */ /* We need to set up a stem variable to hold the formatting strings. We'll call it "formatstem". */
formatstem.0 = 5 /* five columns = five entries for the stem */ formatstem.1 = "<X!" /* 1st is left-justified, read-only, with separator at right */ formatstem.2 = "=!" /* 2nd column shows a bitmap, with separator at right */ formatstem.3 = "<." /* 3rd is left-justified with split bar */ formatstem.4 = ">!" /* 4th column is right-justified with separator at right */ formatstem.5 = "|!" /* 5th column is centered with separator */
/* the actual call to submit the formatting data to the container */ call cnt.setstem "formatstem", "F"
/* set up the column titles */ /* we use a stem variable called "title" */ title.0 = 5 /* again: five columns = five stem entries */ title.1 = "#" title.2 = "symbol" title.3 = "Name" title.4 = "Phone" title.5 = "e-mail"
/* the actual call to submit the data to the container control */ call cnt.setstem "title", 0
/* now we create the 'objects' that are in the container */ /* make sure the 'bitmap.dll' is in the same path as the sample applications .res file */
/* the add function will "only" create an object with a label */ /* the column values need to be submitted with a subsequent call to setstem that */ /* makes use of the new items id - this is why we're going to use the function */ /* notation to retrieve the new items id... */ newitem = cnt.add ("Peter", "bitmap:#66")
/* for the 'simple' view types, this is already sufficient */ /* they'll use the data submitted via the 'add' function */
/* in detail view however nothing would show up, as the */
/* columns didn't receive their data yet - let's submit the */ /* column data via a stem called 'data' */ data.0 = 5 /* five columns = five stem entries */ data.1 = 1 /* value for column 1 */ data.2 = "bitmap:#66" data.3 = "Peter" data.4 = "555-12345" data.5 = "email@example.com" /* now the actual call to submit all column values for the new entry */ /* this is where we need to supply the items id which we retrieved */ /* in the add function above ("newitem")... */ call cnt.setstem "data", newitem
/* That's it for the first container item... to add another one: */ newitem = cnt.add ("Paul", "bitmap:#64")
data.0 = 5 data.1 = 2 data.2 = "bitmap:#64" data.3 = "Paul" data.4 = "555-45678" data.5 = "firstname.lastname@example.org" call cnt.setstem "data", newitem
/* end of code sample */
Comments for the above code sample:
The reason for including all the comments and writing it all in sequence is that you might copy the code sample and paste it into the "INIT" event handler of your container - given the prerequisite of naming your container "cnt" of course.
The first time I tried to mess with a detail view, there were no bitmaps shown, no matter what I tried.
I then found out, that all was my fault - of course... As the second column was intended to show a bitmap, I didn't feel the need to supply the bitmap in the add statement, as this bitmap won't show up in detail view anyway. This was wrong. Well, not actually wrong, but it had the side-effect that the container control seemed to conclude that no bitmap was present. To sum it up: If you intend to use bitmaps somewhere in your container - regardless of view type - always specify a bitmap in the add statement of an item. ;)
Comments for the code sample file that you can download:
[cntsamp2_en.zip This sample (cntsamp2_en.zip)] is a slightly enhanced version of what you just read above. It uses a subroutine to do the whole add / setstem process, which makes it more difficult to understand at first but resulting in much better structure and readability. In addition, it shows how one add / setstem - process is sufficient to supply all types of views.
Next month we'll take a look at the "rest" of the container stuff like removing / selecting items reacting to several events.
If you have any question (or you found some bugs / have some suggestions) regarding containers... don't hesitate to mail me!
Sorry to leave you now, but I have to translate this article to German before my editors shut down their office cursing my name for letting them down again... ;-)
[Editors note: We would never do that to Thomas. But it would be nice to have the articles in a bit earlier. ;-)]