[Omnis-Newsletter] Omnis Technical Newsletter
omnis-news-admin@omnis.net
omnis-news-admin@omnis.net
Wed, 20 Feb 2002 16:49:17 -0000
February 20th, 2002
========================================
UNSUBSCRIBE OPTIONS: You have been sent this email because you have directly
signed up for, or expressed an interest in receiving a technical newsletter
when you downloaded an evaluation of Omnis Studio, or registered the Lite
version of Omnis Studio. If, however you feel you have received this email
in error you can unsubscribe as well as change your subscription options at
www.omnis.net/newsletter.
N.B. If you subscribed by checking a box on one of our forms, you will not
have received a password. You will need to submit your email address at
www.omnis.net/newsletter and select the 'email me my password' option on the
next page in order to receive this.
========================================
WELCOME!
Welcome and thank you for subscribing to the Omnis Technical Newsletter.
Published fortnightly, it is intended for Omnis developers of all levels and
experience, for those people evaluating Omnis Studio, or for developers
moving from a similar tool. We think you'll find the content both
interesting and useful for your Omnis development needs and hopefully it
will help you become more productive in Omnis application design.
In the first article in this newsletter, Geir Fjaerli continues his tutorial
by tidying up the logon process and showing you how to add a toolbar class
to the Task application. In the second article, David Swain discusses the
ins and outs of using radio buttons and check boxes in your applications.
You may find it easier to work through the exercises and examples in this
newsletter by printing it out before you begin.
CONTENTS:
-About the Authors
-Building part 18: Toolbars and menus, part 2, by Geir Fjaerli.
-AmerOmnis 2002: the North American Omnis developer conference
-Radio Button and Checkbox Strategies, by David Swain.
-Copyright and Unsubscribe details
========================================
About the Authors
The Omnis Technical Newsletter contains high quality content from two
leading and well respected Omnis developers, Geir Fjaerli and David Swain.
Geir Fjaerli is based in Norway and has been an Omnis professional developer
for many years as well as a Regional Sales Manager for Omnis. He is
currently working freelance again developing a range of products (in Omnis
of course) including his Prophet5 sales and customer relationship management
solution, now available for Mac OS X. Geir is a regular contributor to the
annual Omnis Developer conferences in the US and Europe, speaking about
Object-Oriented programming in Omnis and SQL development, amongst other
things.
David Swain is the founder and president of Polymath Business Systems, for
many years a leading provider of Omnis training. His expertise in Omnis
programming and his ability to make complex concepts understandable are
recognized throughout the worldwide Omnis community. Latterly, David has
appeared at Omnis Developer conferences providing his own Studio 101
introductory course to Omnis programming and application development.
========================================
Building part 18: Toolbars and menus, part 2.
By Geir Fjaerli, Sunshine Data
Email: geir@sunshinedata.net
Web: www.sunshinedata.net
Hi, and welcome back to our Omnis Studio tutorial.
Here is our work so far:
Part 1: Hello World, our first little Studio application
Part 2: The Studio Classes. The classes are the building blocks of an
application, and include windows, reports, menus etc.
Part 3: The Omnis Integrated Development Environment. Here we looked at the
Component store, the Property Manager and the Catalog.
Part 4: The Data Structure. Including basic terminology.
Part 5: Creating schemas and database tables.
Part 6: Adding table classes and using our schemas in application windows.
Part 7: What did the wizard build, and a tour of the method editor.
Part 8: The logon window and object class.
Part 9: The task window.
Part 10: The task window continued.
Part 11: Designing a proper data entry interface.
Part 12: Implementing the data entry interface.
Part 13: Implementing the data entry interface, part 2.
Part 14: Implementing the data entry interface, part 3.
Part 15: Implementing the data entry interface, part 4.
Part 16: Implementing the data entry interface, part 5.
Part 17: Toolbars and menus.
You should have those handy, and your "tasks.lbs" library file.
As usual, I strongly suggest that you refer to these parts if you haven't
already, or if you feel uncertain about the details. From part 5 onwards,
each new part will be based on programming done in previous parts, so you
have to follow them all. (For back issues, please go to
www.omnis.net/newsletter and click on the Newsletter Archive link.)
WHERE ARE WE?
In the last issue we discussed menus and their properties, and we designed
and implemented our Main menu. The purpose of the menu is to let the user
open the Task and Employee windows. Later we shall also add a report or two
to the menu. We ended that exercise by adding code to install the menu from
our Startup_Task $construct. Today we shall continue on this subject with an
initial look at toolbars. But first, let us refine our startup logic,
including the Logon window and the Main menu.
THE LOGON SEQUENCE PROBLEM.
While we ended the last issue with a working menu, we still had a problem at
start-up. I said: "There is one small issue with this. The menu is installed
immediately after the logon window is opened. So the user can open the task
or employee windows before he has logged on. That is not so good. How do we
solve this?"
Well, you may have given this issue some thought in the two weeks since our
last issue. Basically, there are two approaches to this problem:
1. We can "wait and see".
2. We can act on confirmation.
So how do these two options work? Well, the observant reader will recall
that we have had this discussion before, when we designed our data entry
interface. The logon window needs to be modal, no other actions being
allowed until the user has confirmed and has been logged on. The two
approaches above are both ways to achieve this goal.
You may remember that when we created the Task window logic, I mentioned a
built in command called Enter Data. (Check part 12.) What this does is it
pauses the currently executing method, to let the user enter data etc, and
then moves on after receiving an OK or Cancel. The logic is as follows:
Prepare for the data entry
Enter data ;; The method will stop here while the user enters his data
If flag true
Perform save
End if
Here one method controls both the pre- and post-entry processing, with Enter
Data working as a "wait" statement in between. This is the "wait and see"
approach.
Do $cinst.$close()
Now in our Task window we decided not to use Enter Data, and rather roll our
own. We had one method for all the pre-entry stuff, and then the save button
called a separate method for the post entry. That is the "act on
confirmation" approach.
The reason we went for the second approach in the Task window is the
limitation of the Enter Data command. Basically it is global, and stacked.
Also, in addition to setting the mode, it affects the state of all open
windows. So it will be difficult to control if and when we operate with
multiple open windows. Therefore I advised against it.
But for our Logon window we could actually use it. Because in this case
there is only ever going to be a single modal window open. That is the
purpose. But, never say never, as the saying goes. Maybe it is not such a
good idea after all. Let us look at the other option before we decide.
We want the menu to be installed only after the logon is completed. So why
not move the Install menu to the Logon method itself? The Logon button in
wLogon has the following event handler for evClick:
Do iSession.$logon Returns #F
If flag true
OK message (Icon) {Successfully logged on...}
Other code...
End if
Now we could put our menu install code in here, inside the If flag true.
That way the menu would only be installed when we were successfully logged
on.
THE LOGON METHOD.
We will go for this last approach and put our Install menu inside the Logon
window. At the same time we shall fix some other issues with this window.
First open the Startup_Task in the method editor, and cut out (using Ctrl+X
or Cmd+X) the line we added in the last issue:
Do $clib.$menus.mMain.$openonce('*')
This will add the statement to the clipboard, ready to be pasted.
Now open wLogon in design mode, and double-click the Logon button to edit
its event method. Click the line that says OK message and then press
Ctrl/Cmd+I to insert an empty line between it and If flag true. Then hit
Ctrl/Cmd+V to paste the previously cut statement into this empty line.
The first lines of the logon event method should now look like this:
On evClick ;; Event Parameters - pRow( Itemreference )
Do iSession.$logon Returns #F
If flag true
Do $clib.$menus.mMain.$openonce('*')
OK message (Icon) {Successfully logged on to
[iSession.iSessionName]...}
This will install the menu after a logon, before displaying the OK message.
While we are at it, I think we should bring up a message if the logon failed
as well. Now the $logon method will tell us what went wrong, so we don't
have to. But after that, currently, nothing happens. The logon window just
sits there, and that is not very informative. So click on the last line of
the method, and insert two blank lines above it again using Ctrl/Cmd-I. Then
add the following two lines of code:
Else
OK message {Logon failed.}
So now we get a message should the logon fail. Still not very informative,
but at least we know something is wrong. Omnis offers methods and functions
to display actual errors, so later you may improve on this.
CLOSING THE LOGON WINDOW
What more can we do with the logon process? Well, we discussed in an earlier
issue that it was a bit odd to have the Logon window open in the background
at all times. Maybe we should close it when we have logged on.
A simple Close window command would take care of that for us. This however
is not so straightforward, for the following reasons:
* First of all, the actual logon code is stored as methods in an object
class, and this is an instance var (iSession) of the window. So if we close
the window, we loose the object as well.
* Closing the logon window also logs off the session.
* The logon window offers an option to log off the session. If the window is
closed, we do not have this option anymore.
* The logon window actually has a second function. It displays the tables
and their columns in a tree list. If we close the window, we will not be
able to do this. But this is not a function we need anyway, it just came
"for free" with the stuff the wizard built for us.
MOVING THE iSession VARIABLE.
Now loosing the object is not a major problem, since we don't really use it
after we have logged on. But it would be nice to keep it, since it is the
logical place to keep all our session related code. So how do we keep it
even if we close the window? By moving it somewhere else. The easiest would
be to simply make it a task variable of our Startup_Task. A task variable
exists as long as the task exists, and is visible to all instances opened
under that task. The Startup_Task is opened and closed with the
applications, so that fits our purpose well.
Note: Since the task variables are "global" to a lot of instances, you don't
want to crowd it with variables. A variable should always be of the
narrowest useful scope. But this I think would be one. Looking at our
Startup_Task, there isn't that many variables there anyway. There is our
custom "constants" for the edit modes, and the tSessionName which was used
in $construct of wEmployee to set the current session.
Now to move an instance variable to a different scope, Omnis allows us to
drag and drop between the tabs in the variable pane. That only works if the
window (both design and runtime) is closed, and only the method editor is
open. This would be one way to do it, but not without side effects. The
reasons touch on fairly advanced task related subjects, so let us just rule
that solution out for now.
A better way would be to simply make a new task variable. Let the instance
var be for now, select the task vars tab and create a new variable named
tSession, type Object and subtype oTask.
Now we need to point all methods to the new variable so we can use Find and
Replace on the Edit menu. (This was explained in detail in part 9, refer to
this if you are unsure how to use Find and Replace.) In the Find box type
iSession, and in the Replace box type tSession. As usual, I recommend doing
a Find all before doing the actual replace, that will tell you if the
replace will affect some unintended parts of the application. In this case
it should not, if so go back to the Find window and perform the Replace all.
Now that all methods point to the new variable, we can delete the old one.
In the variable pane in the method editor of wLogon, right click the
iSession variable and select Delete variable. Omnis will ask you if you are
sure. Or if the variable is still in use somewhere, it will tell you so, and
the variable is not deleted.
CLOSING THE WINDOW SHOULD NOT LOG OFF.
OK. Now that the object is a task variable, let us deal with the next issue.
Currently, when we close the logon window, we also log off the session. This
one is simple to fix. The code run when an instance is closed is called
$destruct. In this method in wLogon we find the following line of code:
Do tSession.$logoff
Simply delete this line from the method. That will stop the logoff from
happening.
LOG OFF USING THE MENU.
Now we may actually want a way to log off, and with he window closed we
cannot use the Log off button anymore. An alternative would be to add a Log
off option to the menu instead. The log off then should close all open
windows, since none of the windows will work without a session, remove the
menu to prevent the user from opening them, and open the Logon window again.
To implement this, close the logon window and method editor, and open the
mMain menu editor. Click the last line and hit the Return key twice. The
first will add a separator line, on the second new line type "Log off"
(without the quotes). Double click it to open the method for that menu item,
and add the following code:
Close all windows
Do tSession.$logoff()
Open window instance wLogon/*
Do $cinst.$close()
This will first close all open windows, next it will log off our session,
third it will open the logon window, and finally it will close itself.
"Closing" a menu means removing the instance from the menu bar. $cinst is a
notational shortcut for "current instance" and can be used from within an
instance to point to itself. This way we can code generically without having
to know the exact name of the instance.
One bit of warning: I do not particularly like using the Close all windows
command. If you only have the task application open, it is no problem. But
the user may have other Omnis applications open as well, using the same
runtime. And Close all window will do exactly that, including windows which
belong to other Omnis applications. So if we had more time we would have
made sure that we only closed our "own" windows.
Now, remember that none of the changes we have done to the menu will take
place unless we remove and install it. The same is true for other instances
of modified classes. Since our changes this time was rather complex, it may
be a good idea to simply restart the application. But first, one last thing
to complete our logon code.
CLOSE THE WINDOW ON LOGON.
The last section took care of the issues with closing the logon window. Now
there is only one thing to do, and that is closing the Logon window after a
successful logon. So close all other design windows, and open the event
method for the Logon button in wLogon again. Below the last line in the If
flag true branch, before the Else, add:
Do $cinst.$close()
This will close the window.
Now restart the application. After a successful logon, the logon window
should now close, and you should have a working log off option in the main
menu.
OPEN THE TASK WINDOW ON STARTUP.
Now I feel that being met with an empty screen is not the best way to greet
your user. Even though we now have a menu to select from, it would be
appropriate to take the user directly to the Task window, as that is where
they most likely to want to start working.
So open the event method for the Logon button in wLogon again, and add one
more line of code. Make room above the line that reads Do $cinst.$close()
and insert
Do $clib.$windows.wTask.$openonce('*')
The complete method will now read:
On evClick ;; Event Parameters - pRow( Itemreference )
Do tSession.$logon Returns #F
If flag true
Do $clib.$menus.mMain.$openonce('*')
OK message (Icon) {Successfully logged on to
[tSession.iSessionName]...}
Do method PopulateTreeList
Do $cwind.$objs.TabPane.$objs.Logon.$enabled.$assign(kFalse)
Do $cwind.$objs.TabPane.$objs.Logoff.$enabled.$assign(kTrue)
Do $cwind.$objs.TabPane.$enablepane(2,kTrue)
Do $clib.$windows.wTask.$openonce('*')
Do $cinst.$close()
Else
OK message {Logon failed.}
End If
As discussed above, we will probably not be using the tables/columns tree
list anymore, so the methods and objects for that can be removed if you
want.
THE TOOLBAR.
Having completed the logon, let us prepare for the next step. The rest of
today's issue we will spend on explaining the toolbar, and then next time we
will implement our "commands" toolbar and menu.
The toolbar is the row of icon buttons that you find on the top of many
applications. Originally a Windows interface gadget, it is now used on both
Windows and OS X. The toolbar consists of different controls, usually they
are icon buttons, but they may also be drop lists (of e.g. font sizes) and
other types of controls. If you look at the toolbars in e.g. MS Word, you
will see a lot of different controls. You have buttons for main functions
like New, Open, Save and Print, for Cut, Copy and Paste, drop downs for
style and font, toggle buttons for e.g. "show hidden characters", colour
palettes and more.
The look and feel of the toolbar controls depends on the platform. Old
Windows version had toolbar buttons with square button edges, today they
only show the icon, for a flatter look. Then the button edges is shown when
the pointer enters the button. This was inspired by web browsers. OS X
toolbars are a bit different, they usually show larger icons, and by default
display the text below. As usual, I recommend that you try to follow the
interface standard for your platform(s) of choice. You should look at other
applications that are created by the OS vendor to see how the standard is
implemented. Also, books and white papers on the subject are available.
A Studio toolbar is a class containing one or more controls. It is
instantiated in a docking area. The docking area can belong to either Omnis
or to a given window. It can be positioned on any of the edges, top, bottom,
right or left. While Omnis can have toolbars on multiple sides at once, a
window can only have the docking area in one position.
Now let us create a toolbar and look at the properties and options
available. You create a toolbar just like any other class, by dragging it
from the Component Store into the Class Browser. Name it tbMain. (I use the
prefix 'tb', to distinguish toolbars from table classes which use 't'.)
Double-click the new class to open the toolbar editor. This works much like
the window design mode, you add controls to the toolbar by dragging them in
from the Component Store, just like fields on a window. The main difference
is the position in a toolbar is fixed. Before we add any controls, let us
look at the properties of the toolbar class.
TOOLBAR PROPERTIES.
Open the Property manager for the toolbar class (click on the toolbar or
press F6/Cmd-6 to bring the Property Manager to the top). As usual you can
view the properties either with the editor window open, if no control is
selected) or when the class is selected in the class browser. As you can
see, the toolbar has few properties other than the common ones shared by all
types of classes. The special ones are:
* Allowdrag. This allows the user to drag the toolbar out of the docking
area, either to another docking area, or simply to leave it "floating" on
the screen as a so-called palette window.
* Allowresize. If true, the user may drag the bottom right corner of a
"floating" toolbar to resize it, e.g. from a horizontal to a vertical
layout.
* Initialdockingarea. Indicates which side of the screen the toolbar should
initially be installed if instantiated globally rather than inside a window.
The initial position can also be set to floating. If doing so, you probably
want to control the position by code.
* Enabled. Allows you to enable or disable the whole toolbar (all the
controls in the toolbar) in one go, both as a default state at design time,
and dynamically at runtime.
To install a toolbar globally, use:
Install toolbar {tbMain/*}
As usual the * asks Omnis to name the instance for us. You can add more
arguments, the docking area to use, the left and top position of a floating
toolbar, and parameters to its $construct.
You can of course also use notation:
Do $clib.$toolbars.tbMain.$open('*')
If the toolbar contains functions that belong to a specific window, you can
install the toolbar inside the window. To do that at design time, open the
window in design mode. You will find two relevant properties on the General
tab of the Property Manager:
* Toolbarpos, Indicates which side of the window, if any, should hold the
docking area for window toolbars.
* Toolbarnames. The name(s) of the toolbar(s) to display.
You can also install a toolbar in a window dynamically at runtime.
THE TOOLBAR CONTROLS.
Now it is time to add some controls to the toolbar. Simply drag them in from
the Component Store. You may want to drag in one of each initially, just to
get a feel for their look and feel and the properties of each. Some of them
resemble the equivalent window controls; these include:
* Command button: An ordinary icon button.
* Combo box.
* Drop list.
* Pop up list.
* Pop up menu
* Radio button
* Checkbox
Then some that were designed for use with report layouts, but may be used
for other purposes as well.
* Color picker: When clicked it displays the standard Omnis color picker,
allowing the user to select colours.
* Line style picker: lets the user select between solid and dotted lines or
line thickness.
* Pattern picker: Lets the user select the pattern of foreground and
background color.
* Font combo: Displays the installed report fonts in Omnis.
* Fontsize combo: Displays font sizes.
Finally you have the divider, which simply displays a vertical line to
separate controls visually into logical "groups".
Their properties are mostly very simple. An ordinary button control simply
takes an icon id for the icon to display, and then it has the $enabled
property to allow you to enable and disable the control. Some controls have
special properties:
* The list types take a list name and a calculation, plus they have the
defaultlines property which allows you to set up a static pick list without
having to define a list in memory to hold the lines.
* The color, pattern and line pickers have the runtime $contents property
which returns the selected value.
* The menu control has a menu name property for the menu to display when the
control is clicked.
Try them out. To test install a toolbar, right-click and select Install
Toolbar. To remove it, right-click and select Remove Toolbar. To remove a
control that you have added to a toolbar, right-click it in design mode and
select Remove Tool.
Note that the standard docking areas allow you to toggle the text for
toolbar controls on and off by right-clicking. You can of course implement
the same functionality for your own windows by creating a context menu.
That is all for now, next time we will design our toolbars and see how we
can add code to them and integrate them in our application.
Take care!
Geir :)
========================================
AmerOmnis 2002: the North American Omnis developer conference
17 DAYS TO GO..... starts March 10th.
Raining Data is delighted to lend support to the AmerOmnis developer
conference, taking place March 10 - 13, 2002, at the Ivey Spencer Conference
Centre in London, Ontario Canada, half way between Toronto and Detroit.
AmerOmnis is the North American counterpart of EurOmnis, the highly
successful European conference organized by Omnis developers for the Omnis
developer community. The organisers tell us that a few places remain, but
you must be quick to avoid disappointment. The cost of the conference is
US$1199 which includes all conference fees, meals, and hotel accommodation.
Full details of the conference, including detailed schedule of sessions,
speaker profiles, hotel information (it has a 10Mb/s fiber Internet
connection running 24/7), travel options and registration form, are
available from the AmerOmnis web site: www.ameromnis.com
========================================
Radio Button and Checkbox Strategies
By David Swain, Polymath Business Systems Inc
Email: dataguru@polymath-bus-sys.com
Web: www.polymath-bus-sys.com
------------------------------------------------------------------
My apologies for not covering the Icon Editor in this issue as promised last
time. I find myself on the road with only my Mac laptop along (without that
copy of Virtual PC I keep meaning to install!) and there are a number of
Windows-specific issues that need to be addressed in that article. So we'll
branch to a more "cross-platform" subject regarding the controls we
discussed two weeks ago.
Review of Basic Functionality
Radio buttons and checkboxes are two data entry mechanisms used for
assigning a value to an associated variable. In fact, these control types
are meaningless, or even useless, without an associated variable. Their
function is to provide that variable with a value. A closely related
secondary function is to visually represent the value contained in that
variable.
The range of values this variable can be assigned using this mechanism is
very strictly controlled. A checkbox can only be "On" or "Off". Radio
buttons can only generate values between zero and the number of buttons in
the group minus one. If the associated variable contains a value (acquired
through some other means) that falls outside these ranges, that value cannot
be properly displayed; so an interface rule has evolved (specifically for
radio buttons) that states we should provide enough buttons in a group to
display any possible value the associated variable might contain. This
relates specifically to the secondary function of display of stored values.
A common sense corollary for the primary function of assignment of values is
that we should not offer a control that would assign an invalid or
out-of-range value.
For these rules to be clear, we must first examine how these controls work
with various data types.
Checkbox Field
In Omnis Studio, a checkbox field generates a value of "1" when checked and
"0" when unchecked. This is so no matter what the data type of the
associated variable. Omnis then translates that value on assignment to the
associated variable, so a Boolean variable will see a "YES" when the control
is set to "checked" and a "NO" when it is deliberately "unchecked", a date
variable will see a "JAN 1 1901" or a "DEC 31 1900", etc. Notice that we are
dealing here with the result of a user action, not the simple display of a
value.
Omnis also translates from the value of the variable to the display state of
the control when redrawing a checkbox. The value itself is not affected by
the simple act of display. Any value that Omnis sees as equating to zero
sets the unselected state, while any value it sees as non-zero (including
negative values) sets the selected state. Empty and NULL values equate to
zero in the eyes of Omnis and are displayed as an unselected checkbox for
any data type. To be completely unambiguous, then, we should strive to only
store values (in variables destined to be represented by checkbox fields)
that can be properly and directly translated for display. This also affects
how we generate default values for data entry of new records.
Checkboxes have a Boolean nature and are best used with Boolean variables.
If we use a Component Store Wizard to create a window that must display a
Boolean variable or drag a Boolean variable from the Catalog to a window
class, Omnis Studio automatically gives us a checkbox field. However, any
variable that can distinguish between a "0" and a "1" will technically work.
The difficulty with using checkboxes to represent Booleans in Omnis Studio
(and many other databases) is the fact that there are actually four states
to such a value: "YES", "NO", empty and NULL. These have separate meanings,
but the last three are all represented by an empty checkbox field. If the
variable defaults to empty or NULL, it will take two clicks of the checkbox
to convert this to a "NO" - and the user will be unaware of this fact. So if
we want to be certain that a "NO" value is stored for an unselected checkbox
when a new record is accepted, we must either convert it as the record is
being accepted or generate a "NO" (or "YES") default value before data entry
begins. In either of these cases, the following code snippet will do the
job:
If not(booleanVar)
Calculate booleanVar as kFalse
End if
Radio Button Fields
Radio buttons are used where there are a small finite number of possible
values for a variable and where control over the consistency of the values
entered if important. For example, if we needed a variable to represent and
store either "wall", "floor" or "ceiling" (and these are the only possible
values for the variable), we are best off using a set of radio buttons for
this information rather than an entry field. This prevents our having to
deal with misspellings, synonyms, various permutations of upper and lower
case and other data entry anomalies that could haunt us as we attempt to
work with thousands or millions of records gathered over time.
In Omnis Studio, a radio button field generates an integer value for the
associated variable. This value is the offset from the field number of the
first field in the radio button group to that of the control that received
the click. So the first field in the group will generate a value of "0", the
next a value of "1", etc.
Some other languages (I'll use HTML here since most Omnis programmers will
have at least some familiarity with basic HTML forms) may allow the
programmer to assign the value (often a string) returned by a radio button,
but Omnis is more restrictive than this. We cannot create our own data types
and assign ranges of values to them that can then be directly represented by
radio buttons in Omnis Studio. For example, we can't create a data type
named "stringedInstruments" with values of "violin", "viola", "cello" and
"bass" and have Omnis Studio automatically create appropriately labeled
radio buttons for a variable of this data type. That's just not how Omnis
Studio works.
Since a non-negative integer value is generated and we would normally have
fewer than 256 radio buttons in a group, a "Short integer" variable is the
most appropriate type to use with radio buttons. We could also use other
numeric types, "Character", "Date", or even "Boolean" (for two-button
groups), but "Short integer" offers us the most flexibility with the least
amount of required storage (one byte). So using an integer variable also
allows us to minimize data storage for these values. A short integer only
requires one byte while the full string would have to be stored in seven
(for "ceiling" - the number varies with language). Certainly we could also
have a single character variable and an entry field (forced to upper case
for consistency) for the first letter of each choice, but then we have to
deal with disallowing invalid choices or have our users memorize special
letter codes when first letter is not unique among the valid choices, etc.
Radio buttons are just easier to manage than this alternative.
"Coded" Values
Values stored in a numeric variable as we do with radio buttons are often
referred to as "coded" values. That is, they must be "translated" back into
a meaningful form for display and reporting purposes. Abbreviations (and
even custom data types as described above) are also in this category.
Sometimes "translation tables" are included within the database, but for
simple radio button values this could become very cumbersome, so the
translation information is generally tucked away somewhere with the
application instead. When I work with native Omnis datafiles, I store this
information in the description area of the field definition in the File
Class. When I work with SQL databases, I put this information in either the
description area of the column definition of the Schema Class or in an
otherwise unused method in the associated Table Class.
In fact, Omnis provides a handy mechanism for this called a "lookup table".
(This topic was already discussed in my article on "Display Fields" in the
September 12, 2001 issue of Omnis Tech News, so I won't repeat it all here.)
For variables that will be represented by radio buttons, I put the
appropriate translation string in the description of the variable where it
is defined. This description takes a specific form, like this:
/0=Violin/1=Viola/2=Cello/3=Bass/
If I am more likely to use calculated fields in reports than to use the
lookup table functionality, I might store instead:
pick(instrumentType,'Violin','Viola','Cello','Bass')
I can then go to this description (directly from the Catalog context menu in
the case of File Class fields), copy my translation expression and paste it
where I need it.
Translating for Storage
Some developers object to the storage of coded data when the database itself
does not contain the translation table. This is not an issue when using the
native Omnis database, since the application and the database are tightly
integrated. But this is more of an issue (and a legitimate concern) with SQL
databases where an Omnis Studio application may be only one of several means
used to access the data. If this decision has been made, then the data must
be translated from numeric form into appropriate strings when it is passed
to the back end database - and it must also be encoded (converted to numeric
form) when read from the back end database on its way to being displayed
using radio buttons. The technique used will vary from case to case, so I
only bring this up here on a "heads up" basis (be aware that this can
happen).
This can evolve into a lot of work in a large system, and my vote is usually
to publish the translation tables for all (authorized) interested parties so
they can each deal with the translation in their own applications (often
ODBC-derived spreadsheets). On the other hand, I get paid strictly by the
hour, so if the client is convinced that the extra data storage and
(especially) the extra development time is worth the costs I have explained
in detail, who am I to argue?
In the rest of this article, I will assume that we are using integer
variables with our radio buttons and Boolean variables with checkboxes and
storing those values directly in our database.
"Split" Radio Button Groups
Usually, a group of radio buttons will be clustered together on a data entry
window. But there are situations where they are more appropriate visually
separated on a window. Since each Omnis Studio radio button is a separate
field, this gives us the flexibility to arrange radio buttons anywhere we
need them on a window, as long as they are still in sequential tab order.
Here is an example:
Suppose we have a Customer entry window with two sets of address information
for the customer. This would allow us to have both a mailing address (many
businesses use a post office box or other separate location for
correspondence) and a (physical) shipping address for each record. But some
people and businesses use the same address for both and some may even just
have an alternate address for seasonal purposes. It would be useful to store
a flag or two indicating which address should be used for mailing labels and
which should be used for shipping labels.
We can do this with two variables and two corresponding sets of radio
buttons as follows: name the variables "mailFlag" and "shipFlag" and make
them each short integer. (These must be in the data definition of the
record, not just instance variables of the window.) Now introduce a radio
button field at a location on the window that would indicate it "belongs"
with the first set of address fields (to the immediate right or left of the
group, for example). Assign "mailFlag" and the dataname value for this field
and "Mail" as the text value. Now drag-duplicate this field to a
corresponding position relative to the second group of address fields. The
second radio button will be in sequential field number order after the
first, so the two will work as a group since they have the same dataname
value - even though they are separated visually on the window.
Now add another radio button to the window aligned (just below or just to
the side of) with the first radio button. Label this one "Ship" and assign
"shipFlag" as the dataname value. Now drag-duplicate this newest radio
button to a position relative to the second one created earlier. At this
point, it will "look" like there are two radio button groupings with the
first button labeled "Mail" and the second labeled "Ship". I'll try to
simulate that here in this text-only article (will look best with a
monospaced font):
Primary Address
[Street address field ] [( ) Mail]
[City, etc. fields ] [( ) Ship]
Secondary Address
[Street address field ] [( ) Mail]
[City, etc. fields ] [( ) Ship]
In reality (and functionally), the two "Mail" buttons constitute one group
and the two "Ship" buttons make up the other. When a new record is created,
the "0" choice (Primary Address) will automatically be the default for both
mailing and shipping address (no need to set anything except to clear all
variable values in anticipation of the new record). But either address can
be flagged as serving either purpose.
In the report that prints mailing labels, the street address field content
(for example) will be calculated using an expression like this:
pick(mailFlag,primaryStreet,secondaryStreet)
A similar expression using "shipFlag" will be used for determining the
values for shipping labels, "Ship To" (as opposed to "Bill To") address
information on invoices, and so on.
The "Null" Choice
Even if we use an integer variable with our radio button fields, we can
still have a problem. NULL values are equated to zero values by the radio
buttons, so the first button in a group will appear to be selected if the
associated variable contains a NULL value.
NULL has the legitimate meaning that "no choice has yet been made". A radio
button just can't directly represent this value. So we have a decision to
make here: Is the unselected or NULL state a valid value for our database
(that is, do we want to keep track of this) or do we not want to allow such
a state? Let me expand on this:
In many cases of gathering information, there is "information" in the fact
that the respondent has deliberately chosen "not to choose". So in opinion
surveys, we must keep track of "No opinion" responses. With gender fields,
we may often need to keep track of "Decline to state" options. If this is
the case, then (according to our interface rules discussed earlier) we must
include this in the range of values the radio buttons can both generate and
display. Perhaps we will make this the zero choice, or perhaps we will make
it the choice with the highest numeric value (the other end of the valid
range). In either event, we want the user to be able to generate the value
with a radio button click and we need to be able to display that value with
our radio buttons for browsing purposes.
But now consider two cases for initial data entry of a new record: The case
where we don't want to prejudice a response by setting a default value ("No
choice" may be a legitimate choice here, but we don't wish to set it as a
default) and the case where the client wants to be certain that the user
deliberately clicked on a specific choice (one of which may be "Decline to
state" or something similar). In either of these cases, we would need to
begin the data entry process with no radio buttons selected.
At first glance, this would seem to violate our interface rules for radio
buttons that states that we should not allow data that can't be displayed by
one of the buttons. But this is just a rule for display and our data doesn't
actually exist yet. If we provided a radio button to act as a default (in
essence stating that "I haven't even physically selected 'Decline to state'
yet"), we are violating the "data entry" rule that states we should not
offer a radio button for an invalid or out-of-range value. Since we are
disallowing storage of the record until the user has made some choice (even
if it's the button for "No opinion") from the valid choices offered, the
"not physically clicked" state is out of range.
Usually this state should be represented by a NULL value, but this would be
displayed as the first button in the "selected" state. Furthermore, it would
take two mouse clicks to actually set the zero choice, since a click on a
selected radio button is a non-event. (So the user would have to click
another selection, then click the zero choice.) Since the user is completely
unaware of this, we must take other action.
My choice for dealing with these cases is to set the variable to an out of
range value that cannot be reselected by the user and to test for that value
when the user performs whatever action indicates they feel the record should
be stored (usually a click on the OK button or some equivalent). The
out-of-range value I usually use is "99" (a very old upper limit for integer
variables that has just stuck in my mind for this purpose), but "255" or
anything larger than the highest number of radio button we ever feel we
might allow for a group is fine. Some developers use -1, but then they must
use a "Long integer" variable (4 bytes of storage) or even a floating point
number (8 bytes of storage) instead of a Short integer (1 byte of storage),
which seems a waste of space to me.
Here is the code I would use to do this:
On evOK
If choice=99
OK message {You must choose an item from the radio buttons.}
Quit event handler (Discard event)
End if
In this way we disallow storage of an out-of-range value. We disallow the
user from setting that value by not providing a radio button for it. We will
never have to display it (since it is never stored), so there is no problem
in not providing a button for it.
Sub-Choices
In many database projects, when a checkbox is checked or certain radio
buttons within a group are selected, additional selection controls or entry
fields must be brought into play (with appropriate default values as
needed). When the checkbox is unchecked or different radio buttons are
selected, those auxiliary fields must disappear and the values they
represent must be appropriately cleared. Consider this example:
We must track information about the flooring in the many rooms of the many
buildings of our corporation. The accountants who need to monitor this
information have identified six flooring types and various subtypes: Carpet
(Rolled goods or Carpet tiles), Stone (Granite/Terrazzo/Marble), Vinyl
(Tile/Linoleum), Concrete (sealed or not), Wooden (type of wood) and Ceramic
(description). The items indicated in parentheses are sub-choices they wish
to have identified for each major choice. Sure, we could make this very
linear by making radio buttons for each choice/sub-choice combination
(Carpet, Carpet tiles, Granite, Terrazzo, etc.), but this is not in keeping
with the categorization needs of our clients (and it makes for a really
cluttered window). Instead, we create a separate variable for each
sub-category and a separate radio group, checkbox or entry field (as
appropriate) for each as well. We make the subcategory controls visible or
invisible as their main category is selected or deselected in the main radio
button group.
So for our example, we would have short integer variables named
"flooringType", "carpetType", "stoneType" and "vinylType", a Boolean
variable named "sealed" and two character variables named "woodType" and
"ceramicDescription". Our layout might look something like this:
[( ) Carpet] [( ) Rolled goods] [( ) Carpet tiles]
[( ) Stone] [( ) Granite] [( ) Terrazzo] [( ) Marble]
[( ) Vinyl] [( ) Tile] [( ) Linoleum]
[( ) Concrete] [[ ] Sealed]
[( ) Wooden] [<entry field>]
[( ) Ceramic] [<entry field>]
The main radio button group is vertical with subcategory groups extending
horizontally from the appropriate main category.
Since a click on a radio button means that choice has been selected, we can
control the display of the other fields from within the $event() method of
each main radio button field. For example, here is what we might include in
the second radio button labeled "Stone":
On evClick
Hide fields {Rolled goods,Carpet
tiles,Tile,Linoleum,Sealed,woodEntry,ceramicEntry}
Calculate carpetType as 0
Calculate stoneType as 0
Calculate vinylType as 0
Calculate sealed as kFalse
Calculate woodType as ''
Calculate ceramicDescription as ''
Show fields {Granite,Terrazzo,Marble}
Notice that we also clear other values and set a default value for the
associated subcategory variable.
The "Other" Option
No matter how hard we try, sometimes we just can't be certain we have taken
all possibilities into account when creating a set of radio buttons. In
these cases, it is good practice to include an "Other" option. We assume
that this option will be used only infrequently, so we put it at the bottom
of the radio button group. But to accurately track what "other" means for a
given record, we should also include an entry field and associated storage
variable for the user to enter some descriptive text. Yes, I know, this is
counter to the original purpose we had in setting up radio buttons in the
first place (to avoid data entry inconsistencies and anomalies), but if we
can't be certain we have accounted for all possible values we have no
choice. On the other hand, we have limited the possibility of such errors to
a very small minority of the records in our database - or we'll soon see
that our entire premise was wrong!
We track this "exception" data in the same way we did the descriptive
sub-choices above - when the "Other" radio button is selected, an entry
field appears with the cursor ready and waiting for input within that field.
If a different radio button is selected, the field is hidden and the value
is cleared from the variable. The trick here is how to display this content
on a report or other display-only device.
The "Other" choice is an extension of the main categories if we are printing
out a translated text version of the variable on our report. In this case,
we will use a pick() function to select what to print based on the value of
"flooringType". notice how we can simply include the value of "otherType" in
the list of category names:
pick(flooringType,'Carpet','Stone','Vinyl','Concrete','Wooden','Ceramic',oth
erType)
Displaying just this string result instead of trying to simulate the radio
button group takes up much less space on the report and may be more
meaningful to the people reading the report.
Reporting Options
But what if we did want to show the radio button groups on our report as
they appear on our data entry window. There are no checkbox or radio button
fields for reports because these are "controls" and reports aren't
interactive. We also don't have pushbuttons, tab panes, scrolling lists or
other controls in reports, either.
But this doesn't mean we can't make it look like we have checkboxes and
radio buttons if that is the image we want to present. Why not just use the
icons these controls use? Although I haven't figured out how to access the
multistate icon states (yet), we can use some other icons from the Omnispic
and Userpic icon stores that work just about as well.
Icon number 1806 in the 32x32 size is the icon used to indicate a checkbox
field in the component store and the "field list" window. This one has a 3D
look that may not print well, though. A little more digging leads us to the
Userpic icon number 404, which is a "flat" checked checkbox. Icon number 304
is used to represent a background rectangle in the component store. (It
doesn't have quite the square aspect ratio of the checkbox, but it's close.)
So how do we get the icon onto our report? There are a couple of ways, but
the most straightforward is to convert it to a picture value using the
$getpict() function of the OmnisIcn Library external component (which we
must have loaded in order to use). This function requires two parameters: an
icon ID (complete with the size constant) and the background color for the
resulting picture value (since many "modern" Omnis Studio icons are
transparent).
In our report, we then create a small picture field with the "noscale" and
"calculated" property values set to kTrue. The "text" property value will
look like:
OmnisIcn Library.$getpict(pick(<boolean variable or
expression>,304,404)+k32x32,kWhite)
This will act like a checkbox field and give us the look we want. (Notice
that we didn't even have to declare an instance variable of "picture" type!)
We can extend this to radio buttons (405 = "pushed", 301 = "cleared"),
Excel-style OK (checkmark = 109+k16x16 -- or 1066 or 1068 in color) and
Cancel ("x" = 110+k16x16 -- or 1067 or 1069 in color), arrows of many styles
and orientations, or any other icons in the icon libraries. We can also add
our own to the #ICONS table of our libraries.
And that takes us back to the Icon Editor, which I'll detail in the next
issue...
========================================
I hope you've found this issue of the Omnis Tech Newsletter both interesting
and informative. Please send me your comments and feedback, and include
suggestions for future articles if you like. We would like to hear from
you...
Regards,
--Andrew Smith.
Omnis Technical Newsletter Editor
Email: editor@omnis.net
========================================
No part of this newsletter may be reproduced, transmitted, stored in a
retrieval system or translated into any language in any form by any means
without the written permission of Raining Data.
(c) Copyright Raining Data, Inc., and its licensors 2002. All rights
reserved.
Omnis(r) is a registered trademark and Omnis 7(tm), and Omnis Studio are
trademarks of Raining Data UK Ltd. Other products mentioned are trademarks
or registered trademarks of their corporations.
========================================
To unsubscribe from this newsletter or change your subscription options,
please go to:
www.omnis.net/newsletter