VOICE Home Page: http://www.os2voice.org
|By Thomas Klein© April 2003|
Welcome to the seventh part of our "little" series on DrDialog. Today, we'll complete the subject of dealing with container controls by taking a look on how to react upon user actions that take place in a container. But first, let me mention some facts that I forgot to tell in last months issue...
Even if taking the risk of not being very accurate in matters of backgrounds and concepts, I'll try to explain it this way: All programs that are run under control of the Presentation Manager are able to make use of its objects, as it is provided by the system itself (or the PM). Thus, it won't matter what development tool you are using to create your PM-application (DrDialog, VX-Rexx, Visual Age for [whatsoever] and so on), because all it does is pass certain parameters and values to the system-provided object internally.
The reason I'm explaining this is that Drdialog's online help is quite "thrifty" on information regarding style -dialogs... to say it more politely. At least, I couldn't find any information about the meaning of the settings (except for the paragraph that explains about style settings being stored as specific bits within a 32 bit value and how to toggle specific bits by using the rexx function bitor). But because we now know that it actually doesn't matter if we're using DrDialog or e.g. Vx-Rexx to use a container control, we're able to refer to another development tool's manual to get the information we need.
But before you start complaining why the author didn't simply do that prior to write his article: I'm actually too lazy to install Visual Age C++ 3.0. I could have used two other development tools that are lying around here somewhere, but... sigh... I simply didn't have the time to take the necessary efforts to get it up and running. And (to be honest) I didn't feel the need to know all the details about it, as Drdialog's defaults and those settings I was able to figure out myself were sufficient for those programming task I did up to now. Well, one never knows - maybe I'll try some time later...
But let's get to the point finally: The style dialog of a container control is made up of three groups of settings. The first one is called selection stylesand is used to specify what behavior and features are available to the end user when selecting entries within a container. That's pretty much the same as what we know from listbox or comboboxcontrols:
Single means, that the user is able to select a single entry only. If another entry is selected, the previously selected one will be unselected.
Using Multiple will enable the user to select more than just one entry by toggling the selection state of an entry via a simple mouse click without changing the current selection state of the other entries.
Extended finally is the "full-featured" version of selecting entries that provides different combinations of mouse and keyboard inputs. A full range of entries can be speed-selected by clicking on one entry and shift-clicking the other (upper or lower respectively) entry of the range. This will select all entries from the first to the other one according to the sort order of the container. A single entry's selection state can be toggled by ctrl-clicking on it (that is, clicking while holding down the ctrl key). But beware: A simple click (without one of the modifying keys) will un-select all previously selected entries.
Container Styles does include two settings that I am not able to tell you anything about it. I tried both Mini record and Verify pointers but didn't notice any impact on either look or behavior. Perhaps that was because of these options being intended to change some "internal" behavior of the object that aren't supported by DrDialog or simply didn't show up in my rather "simple" kind of program. So, let's focus on the other two options then:
Auto position is something that you are urged to leave in its default activated state when working with DrDialog. Some features of OS/2 objects are simply not supported by DrDialog directly, like specifying an entry's position within a "icon view" container when adding it (that's to say a container with view="Bitmap"). Such containers actually require the programmer to specify where to put the entry, because - as you might know from WPS folder - entries can be placed freely within a folder (as long as you're not using column view or auto-arrange by grid). Thus, a container in bitmap (icon) view is quite the same as a WPS folder using icon view. In addition, such container objects behave like an auto-arranging WPS folder if Auto position is activated, which means that the container itself will take care of arranging the entries according to its grid. This might all be a little confusing. Let me explain what actually happens if you're using a bitmap-view container withoutAuto position. In this case, the system relies on the programmer to specify each entries position within the container when adding it. But as we don't have the means to do so in DrDialog, the container will pile up the entries on the default starting position - one on top of the other which will leave only the last one (the uppermost) being visible. That's bad. Note that Auto position only effects containers in "Bitmap"-view. All other views already take care of positioning the entries automatically due to the nature of their view type.
Read only prevents users from being able to change entries by ALT-clicking on them. This takes effect on all types of view and also prevents the user from changing column values in a "Detail"-view container, even if a column was set up to be not read-only. The developer however still has complete control over the entries, that's to say you are able to change whatever you like "from code". This is useful to make sure that the end user is required to use a developer-provided function to change entries instead of using the containers built-in capabilities. Note finally, that Read Only does not take any effect on the events being triggered, thus, clicking on an entry will still generate the appropriate event for example.
There's not much to say about the Basic styles options. Visible is used to either show the container or not. If it is made invisible, the user can't operate it of course - thus no events will be triggered. Maybe you'll come up with a scenario where this might be useful, but currently I can't figure out what that should be like.
Disabled is rather suitable for use. If you use this option (by checking), the user will actually "see" the control and its entries but won't be able to do anything with it and no events will be triggered. This is useful in cases where you might want to show something that can't be changed or handled - except if using specially provided functions (like menus or pushbutton) if any. In general compliance to the ethics of GUI this should be avoided, as the user is presented a well-known control that was stripped off all it's visible and functional capabilities. Changing an entry for example will become quite clumsy to the user, as he is somehow required to specify the entry that he wants to change and won't be able to use either mouse or keyboard within the container. You should try to figure out if there is no other programmatical mean to achieve what you want you application to do.
Group actually doesn't mean anything to me. Due to the lack of help text I'm in doubt if this is of any usable meaning in DrDialog.
Tabstop (if enabled) will make the control accessible by keyboard (the TAB key to be exact). I haven't mentioned this up to now if I remember correctly, because I assumed everybody already knows, but nevertheless here we go: By using the TAB key, the user is able to "tab" from one control to the next one (and backwards by using SHIFT-TAB). This of course is only applicable to control that were set up with tabstop enabled. The currently selected control can be operated by either using the SPACE BAR (e.g. clicking a push button or (un)checking checkboxes or radio buttons) or by using arrow keys in listboxes or entering data into entry fields.
More than once I have gotten angry about "handmade" controls that weren't designed to be either accessible or usable by keyboard although their appearance and behavior indicated them to be. But this is nothing compared to programs that in fact are usable by keyboard but that were designed without taking care of the tabbing order, which results in ridiculous random jumping from one control to another when using the TAB key. I know that Visual Basic for instance provides means to freely assign tab indices to controls at design time. When adding controls to a dialog the tab index is increased automatically... which appears to be okay at first sight. But as soon as the developer starts adding, removing or arranging controls, trouble starts as these controls retain their tab index. Apparently, many VB programmers seem to forget to do a final check of the tabbing order before starting to deliver their stuff (or test it at all) which results in such symptoms.
The reason for me telling you about this is that with DrDialog you won't have to mess with tabbing order at all, because it will be automatically determined at run time based upon size and position of the controls within a dialog window. Roughly speaking it's top-to-bottom and (within this order) left-to-right, except for control being grouped in a group frame: A group is handled separately as one large control but with the group, the same rule applies.
To sum up: Don't waste any time worrying about tabbing order in DrDialog! Its mechanism described above is the best example I've seen so far when thinking "Actually, this should be quite simple...". It's perfectly suited for 99% of all cases and works not only like you expected but like a charm too.
On return, a variable called rc contains the number of deleted entries. Again - as you might recall - we're allowed to use the CALL type notation as well, if we don't mind the number of deleted entries... in this case, syntax would rather be:
rc = [dialog.][control.]Delete([item])
To give an example, assume to have a dialog called dlg_test and a container called cnt_mine. The appropriate commands would look like this (anything between "/*" and "*/ " is just a comment):
This example is based upon the assumption that there is a certain variable called entry-id which holds the ID of the item to be deleted. We'll get right back to this - let me first mention that delete can be used without specifying a specific entry. This will result in all entries of the container being deleted. This is done by simply not using the parameter item . According to the example given above, this would read:
rc = dlg_test.cnt_mine.Delete(entry-id) /* Function notation */ call dlg_test.cnt_mine.Delete entry-id /* CALL notation */
Or when using the CALL notation:
rc = dlg_test.cnt_mine.Delete()
Of course (I know you may know this) the optional parameter of [dialog] can be left off, if delete is called from within the dialogs own code context. In addition, the second optional parameter [control] can be omitted as well if delete is called from within an event handler of the container itself.
Assuming that you would have created a pushbutton underneath your container to delete all entries, you would code the following statement in the pushbuttons Click event handler:
And your container will be empty. By using the function notation like in
...you could make use of the variable removed to know how many entries were deleted (if this would be any of use for you).
removed = cnt_mine.Delete()
Fine, but let's take a look at [item] parameter now. Being able to delete all entries of a container might be useful in some cases, but honestly - mostly we'll deal with deleting specific entries, right? As I stated above, delete can be used with specifying an entries ID. The ID is provided by the system as a return value of the add function used to add an entry to the container.
Well... this means that we must save that ID somewhere to be able to delete the appropriate entry later, right?
You might have decided to prevent the user from clicking around in the container for some good reason by specifying the appropriate style for your container. This won't mean that you won't be able to delete entries of course, but retrieving the necessary IDs to do so will become quite difficult: There is no way of collecting IDs from code at a later moment! This scenario now would really require you to store the IDs returned by the add-function in a separate list, table or whatsoever to associate them with another data value of an entry that's suitable to act as an initiator for the delete action. For instance, You want to provide a way to delete entries based upon their names. While this is quite okay, you'll have to take care of the fact, that containers can hold any number of entries with the same name (except that you took care of preventing this "manually"). Technically speaking, the container doesn't mind any data of any entry because internally it's all about IDs only. So, if you want to do things like deleting all entries whose names start with "A", you need to have a table that holds each entry's name along with the appropriate ID. This enables you to identify all entries whose names start with "A" and use the associated ID in a call to delete. Don't forget to update your reference table too. Think about using a listbox control for this purpose, because cleaning up the list will be somehow easier then.
You might already get the picture: This requires quite some efforts and thus should be another reason to use a container (in compliance to GUI ethics) only if you provide all of its capabilities to the end user too. Okay, let's take a look on how an entry that was selected by the user can be deleted.
This raises the following question: If the user requests a delete action - what are we intending to delete? The current entry or the selected one(s)? Hmm... that's not easy, right? This has actually nothing to do with the IDs that are needed for the delete function, but it's rather a matter of basic understanding. Like I said, with a Style setting of simple you won't find it hard to react upon a delete request, as the current entry is the only one that is selected. If the container uses one of the other two more complex selection styles, you might...:
Eventdata() will return two values when being used in the context of a select event: The ID of the entry that the event has occurred for and the change of state ("emphasis") for that entry. If you downloaded last months sample application, you're able to easily check this on your own: Load the .RES file into DrDialog, bring up the code editor tab for the containers select event an enter the following statements:Now, run the program and watch for the messages appearing in the run time monitor (the list beneath the "running guy" icon) whenever you click or move to an entry. Next, change the containers selection Style into extended or multiple and run the same program again with taking a look at the messages generated by selecting one or more entries or moving from one entry to another. You'll notice that eventdata will report different states for the entries according to Style-settings and usage of either mouse or keyboard. The only actual state that you can "rely" on is the selected/unselected state of entries...
call eventdata say "----" say "id:" eventdata.1 say eventdata.2
Now, let's see what means are available to the end user in a typical scenario to delete entries and how to react upon them. I took the chance to verify this by using a WPS folder that - gladly - didn't contain important files...
As we decided, this refers to selected entries. In a container using simple selection this would equal one entry whereas with other selection styles this could equal one or more entries. So we need to find out which entries are selected in the container first (if any). To achieve this, we'll use the getstem function that we already discussed (in parts) in last months issue. It is used to retrieve information about several entries at once via a stem variable. Its syntax looks like this:
In order to un-mystify what it's about: This is "my" version of it:
call [dialog.][control.]getstem [stemvar] [, "Select" | "Mark" | "Cursor" | 0 | entry-id ]
If getstem is used right within an event handler of the container, you won't need to specify the[dialog]and[control]parameters. But as we said that this all is intended to be part of a pushbutton's click event (with the pushbutton being part of the same dialog as the container), we need to specify the control we're referring to, thus only [dialog]can be removed from the call. Using it however won't hurt at all. Assuming the containers name to be cnt_test would result in the following call syntax:
call [dialog.] [control.]getstem [stemvar] [, type]
stemvar is the Name of a stem variable we want the results to be placed in. Note that - as always when dealing with stem variables - we need to provide its name in quotes (single or double quotes doesn't matter) for rexx to understand that this is a variables name instead of a variable that holds the name. As you might have noticed by the first two syntax notations for getstem, the stem variable name is an optional parameter - thus, it can be omitted. In this case, getstem will provide a variable on its own called "stem".
call cnt_test.getstem stemvar,type
The Type parameter specifies which data we want to retrieve about the entries of a container. Valid values for Type are as follows:
|if Type was set to...||...the stem variable entries will contain:|
|"Select"||- the IDs of all container entries which are currently selected|
|"Mark"||- the IDs of all container entries which are currently marked|
|"Cursor"||- the ID of the container entry which is the "current" one, thus the one that has the cursor|
|0 (this is a zero)||- the column titles used in detail view|
|an entry-id||- the column values of the entry specified by entry-id|
So far so good. Now, to get to know the IDs of all entries in cnt_test which are currently selected and put them into a stem variable called "selection", we need to code:
...and if we prefer to not specify a stem variable on our own (thus using the provided one called "stem"), it should read:
call cnt_test.getstem "selection","Select"
Note that the comma in front of "select" needs to be retained in this case for rexx to understand that the first (optional) parameter was omitted.
call cnt_test.getstem ,"Select"
The next (and final) simplification to getstem is the fact, that there is a default value for the Type parameter as well (the value used if the parameter was omitted) which is "select". As we wanted "select" to be the value of type, the same stuff as above can also be coded by using the default (omitting the type parameter):
And again, if we don't want to provide a stem variable name on our own, we could even code:
call cnt_test.getstem "selection"
Well, that's a quite handy syntax, isn't it? ;)
Fine. Now we have a stem variable whose entries contain each ID of a selected entry of our container. In addition, getstem provides us with the total amount of selected entries (equaling the number of stem entries) by its entry #0 (you remember that mechanism from last month?). In all, this enables us to build a loop processing all entries from the first one up to the last (because we know how much there are). We didn't discuss loops up to now, I know, but you might want to just take a look at what's coming up:
This code (actually not processable due to the bracketed line) makes the variable i being set to 1 and getting incremented by 1 as long as this value won't exceed the value of the variable selection.0. Then, the loop sequence will end. Each incremental step will process the stuff that is contained "between" the header (do-line) and the footer (end-line) of the loop. To give a "real" example showing how to print out all IDs, this is how it's done:
do i = 1 to selection.0 [enter the statements to be processed for each entry here] end
Because the value of i is incremented by 1 at each pass of the loop, the first pass would print (or "say") the contents of selection.1, the next pass would print out the contents of selection.2, then selection.3 and so on, until the last entry (=value of selection.0, the total amount of entries) was processed. Selection.0 (the contents of the "0th" entry of the stem variable selection to be exact) contains the total amount of stem entries, thus equaling the total amount of selected entries in the container.
do i = 1 to selection.0 say selection.i end
With that in mind, getstem is well suited to provide a kind of status line for your container, which could be used to show information on the number of selected entries. This of course doesn't make quite sense if your container is using a selection Style of single, as there'll always be only one selected entry. ;)
But hey - do we like to know the IDs of those entries? No Sir - we want to get rid of them! Now that we know that selection.i within our loop contains the appropriate ID of a selected entry, why not finally combine it with the delete function we discussed above to make it become...:
If you think that the whole stuff is getting too complicated due to the ID mechanism, let me explain something:
do i = 1 to selection.0 call cnt_test.delete selection.i end
In practice you'll be deleting entries 7, 9, 11 and 13 (instead of 7, 8, 9 and 10). And if this takes place you're even lucky, because if the list is actually made up of 10 entries only, your third call to delete (which then would delete entry #11) would give you an error message for deleting an entry which doesn't exist (any more) - not to mention that you actually didn't even wanted to delete it. ;) Well, bad luck then. So the only way to get around this problem is either using a listbox by means of selected entries as well or coming up with another clue... (maybe we'll talk about such things in a later part - don't hesitate to remind me if you're interested in it).
This problem would arise for containers as well, if they were handled by indices instead of IDs. But as DrDialog doesn't simply provide us with that means, there's no need for worrying about such matters. Excuse me for this little tour but maybe you've got the picture of the advantages of using IDs.
Okay. The user is able to select entries and click the pushbutton. The entire event handler for the Click event of the pushbutton would thus consist of the following lines:
Not too bad what we get out of that little piece of code, right?
call cnt_test.getstem "selection" do i = 1 to selection.0 call cnt_test.delete selection.i end
I would like to finally introduce you to the subject of handling edit requests for container entries, which will complete our "container tour" for the time being. A users edit request can either refer to the name of an entry or to a column value if the container is in detail view. Let's take a look at name changes first - this is done by (as you might know) ALT-clicking on an entry. The title (name) display will then be switched into an entry field. How about an example?
That's what it looks like. The container control comes with all necessary capabilities built-in. The only thing you need to do is actually react to the changed event which is triggered upon completion of the change. But take into consideration, that the changed event does not automatically mean that the actual value was changed - it's rather used to signal that a change request was processed and that data entry is completed (the container has switched back from entry field to "usual" display for that entry).
Of course we want to know the new name, after the user has (possibly) changed it - to store it on our address book file (last months example) for instance. In case that you are using a container control to represent a directory on your hard disk, you now may want to rename the actual file according to the users input. To retrieve the (new) name of an entry, we require two functions: eventdata and item.
...then run the program. Now change any entries name by ALT-clicking on it and entering a new name. The run-time monitor will show:
call eventdata say "---" say "Change occurred for entry id" eventdata.1 say "The changed entry data was:" eventdata.2
The ID displayed for you will certainly be different from the one in the screenshot above, as IDs are dynamically created by the system upon creation of an entry. But the last printed line should actually contain "VALUE", as this is used by DrDialog (or eventdata) to signal that the name (title) of an entry was changed. Don't think about this too much now, go on and run the program again, change the containers view type by selecting "Detail" in the dropdown list named View: and change a column value for an entry, like...:
Once you completed your input, the run time monitor will show (more or less):
Ahh! Despite the fact that in your case, most probably the ID (and according to the column you chose the second lines value too) will differ from the screenshots value, there is one BIG difference from the previous run: eventdata will not return "VALUE" but rather a number. This number indicates the column whose value was changed. According to these two examples, we now know both entry and data that was changed - either name or column value. Now let's see how to use the item function (or method) to retrieve the actual new value that was entered in the change request. The syntax is as follows:
content = [dialog.][control.]item([entry-id [,"VALUE" | "BITMAP" | "DATA" | column] [,new-value) )
The function can be called to assign a new value and/or retrieve the current one. This is done by using the last optional parameter to assign a new value, while content will be set to contain the previous value upon call completion. If new-value is omitted, there'll be no change but it will just retrieve the current value.
content = [dialog.][control.]item([entry-id [,type] [,new-value) )
As you might have noticed by to the syntax diagram, all parameters are optional. If you omit all of them, item will retrieve the total number of entries within the container into content. This could be useful. But now let's finally find out how to determine the kind of change that happened to an entry by using item. This is done with the type parameter:
|Setting type to...||...will give you:|
|"VALUE"||the entry name (title)|
|"BITMAP"||the name of the bitmap assigned to the entry|
|"DATA"||the data value that optionally can be assigned to an entry|
|a column number||the column value assigned to the given column of an entry|
So we're able to retrieve almost every kind of information about an entry. Note that item does provide some more features but as we either did already discuss them or they are not of importance to us now, we won't go into details about it any further.
As always, we're free to omit the leading qualifying parameters [dialog.] and [control.] if the item function is called directly from within the changed event handler of the container. Due to the possible information returned by eventdata in matters of edit requests, there are two ways of using item accordingly - in the first case, this deals with retrieving the new entry name by
and in case of a column value that was changed by coding
content = item(entry-id, "VALUE")
...with entry-id being the first value returned by eventdata (the entry id) and column-number (in the second example) being the column number that is returned as the second value by eventdata. But before starting to think about a way how to code two different calls in order to react to what was returned by eventdata, let's take a close look on what is returned by eventdata in its second value: Either this is VALUE or a column number... get the picture? There's a close match between the second returned value of eventdata and the type-parameter required to call item. In short, we're able to code the following command:
content = item(entry-id, column-number)
This will make us retrieve the new value in either case, regardless of whether an entry name or a column value was changed. Of course this won't help having to code an appropriate routine to distinguish between the two cases, but that's not really hard - we only need to test if eventdata.2 contains VALUE. If so, then an entry name has been changed, while all other cases would equal to a column value change.
content = item(eventdata.1, eventdata.2)
Please note that your browser possibly will wrap lines - each of the two say -statements must appear on one line
call eventdata say "-------------------" who = eventdata.1 what = eventdata.2 if what = "VALUE" then say "Entry with ID" who "is now called" item(who, what) else do call getstem "title", 0 say "For" item(who, "VALUE") "there was a change in" title.what "which now contains" item(who, what) end
Some comments to the above routine:
Comments on line 4:Phew! If you happen to be a stem variable newby, this might be quite puzzling at first sight, but - by the way - this is the case for almost every other attempt to explain stem variables too that I've found. The best way to understand what's behind those stem variables in my mind is to play around with them. You'll be surprised of the number of times that you'll hear yourself say "Ah", "Oops" or "Huh?"... ;)
If a column value was changed, we want a message to be displayed containing the column title. All titles are retrieved using the getstem function in line 9, which will store them in a stem variable named title. In order to retrieve the actual title of the column which contains the changed value, we simply need to refer to the appropriate stem entry (entry 1 = 1st column and so on). Fine. In theory, we could code this to read title.eventdata.2 to directly refer to the appropriate entry in the stem variable. In practice, it won't work:
Rexx assumes the notation of a stem variable entry to be of the form stem . entry with the last dot indicating the two parts.
In our example this would mean that rexx will try to use the second (".2") entry of a stem variable called "title.eventdata". Unfortunately, there is no stem variable of that name. We thus need to find a way to tell rexx that we want the value of eventdata.2 to be "the part after the dot". There are (at least) two ways of accomplishing this, but I'll use the one which is easier to read:
- We use a variable to store the contents of eventdata.2 (see line 4: what).
- This variable will then be used as "the part after the dot" (see line 10: "title.what")
Of course you're free to live a life without stemmed variables, but... actually they can't be avoided when having to deal with more complex tasks in rexx. I'll dedicate an entire part of this series to stem variables I guess. If you didn't understand the above stuff, then simply take it as it is and focus on container behavior and usage.
And changing Peter's phone number (in detail view) into "12345678" would result in...:
Okay. That's it for today - happy programming (or playing)! I hope I managed to give you some clues about working with container controls. If there are any questions about this article (or any other previous one), don't hesitate to mail me. Next month (I think), we'll deal with basic rexx commands and functions. You'll be able to use them in both DrDialog as well as "pure" rexx scripts...
See you next month!
[Previous Page] [Newsletter Index] [Next Page]
VOICE Home Page: http://www.os2voice.org