Mastering Dialog Windows in Xbase++ Part 2 - Techniques for MDI Style Applications Copyright © 2002-2008 Clayton Jones |
by Clayton
Jones Revised November 19, 2008 |
Introduction |
The
material presented in this article was demonstrated at the first US Xbase++ Developers
Conference in New Hampshire in October, 2001. The
presentation was well received and I decided to expand the material into a
series of web page articles. The intent of this article is to help
new Xbase++ users get productive more quickly by clarifying
a subject that is not well documented and by providing some good
programming tips and techniques for working with dialog windows.
In Part 1 we established the terminology and definitions for the different window types and how they are used. Here we will look at the pros and cons of using these different types to achieve control of multiple layered windows and other designs. I am assuming that the reader is familiar with the how and why of running dialog windows in separate threads. If not, I recommend first reading the series of two articles on that subject, "Dialog Windows in Recycled Threads" and "Data Entry Dialogs in Event Driven Applications". Both can be found on the same Articles page as this series. |
The Central Challenge |
The most
fundamental window construct uses the three primary window
types: Main, Child and Modal. Typically the Main window has a menu
from which Child windows are called. If another window is needed
below a Child, then a Modal is called. More than one child can be
open at once, but only one Modal can be open at any time, because a Modal
disables the rest of the application and prevents any other activity until
it is closed. This diagram shows a Modal, called from Child 1, as
the only active window. All others are disabled: Main / | \ Child 1 Child 2 Child 3 | Modal In a document-centered application (MDI means Multiple Document Interface), such as a word processor, this architecture is adequate. Child windows display the documents, and Modals are used for pop up message boxes and various utility windows. Database applications, however, are an entirely different matter, and more levels of windows are often needed. In DOS it was just a matter of drawing another box on the screen, but here everything has to be a window object, subject to MDI rules. Suddenly we come up against the fundamental challenge: How to have more than three levels? In addition, we must be able to have multiple levels under more than one Child window at once. There are a variety of solutions for multiple levels, such as calling another Modal from the first one, but many have undesirable side effects. I will first present the technique which I find to be both the easiest and most powerful, and then discuss the pros and cons of others. |
Multiple Event Loops and Threads |
There
has been much debate about whether it is advisable to have more than one
event loop in an application. Some people adhere to the one-loop philosophy and predict dire consequences for
using more.
The one-loop idea, however, is rooted in the document-centered
architecture which we have already found to be unsuitable for our needs. Using multiple event loops is perfectly safe as long as one
understands how they work. Not only are they safe and easy to use,
but they are a necessary part of using threaded windows and
multiple layers. Here are the guiding principles: a) It is desirable to use threaded windows because it is the easiest and most trouble-free way to avoid record pointer/alias conflicts when opening data entry windows more than once (assuming that data files are opened each time the window is opened). Each thread has its own Work Space and encapsulated, protected Work Areas. b) Each thread has its own event queue, and therefore requires its own event loop. So any window opened in a new thread must have an event loop. c) There are no event loop problems as long as there is only one active window with event loop in a thread. The key word here is "active". Problems occur when more than one window with an event loop is active in the same thread. |
Procedural Behavior and Loopless Windows |
The
term "procedural" is generally used in the phrase "procedural
programming", to describe a program architecture where only one
window can be open at a time (often in emulation of DOS programming style). I am using it here in a different
sense to describe
"procedural behavior", when a routine in a window must call a function
which opens another window. We want the program flow to stop
where the function is called, and resume after the function closes its
window and Returns: | executable code - done | executable code - done > WindowFunction() - in progress (program flow stops here until window closes and Returns) executable code - executable code - There is a problem if we use the common dialog window model which has no event loop, in which the function sets up the window, gets everything working, and then Returns. This model is based on the idea that there is a single event loop which handles all events for all windows open in its thread. This is fine in a document-centered application, but creates havoc in a database program where we need procedural behavior within a data entry form and its subroutines. In the above diagram, the WindowFunction() would Return and the following lines of code would be executed while the window is still open. The only way around this is to put an event loop in the subroutine window. This keeps program control in the function until it destroys its window and Returns. But now we have two active windows with event loops in the same thread - that means trouble. We could open the second window in another thread but then the subroutine couldn't access the data tables. We could pass the work areas into the next thread via the Zero Space, but that's a lot of extra work and complexity. The solution needs to be simple and easy. What to do? The answer is to de-activate the first window. There are several ways to accomplish this. The easiest is to simply disable the calling window, and re-enable it when the subroutine Returns (the other techniques will be discussed later). Here is a typical example which could be in a keystroke handler for the first window: ELSEIF nKey == xbeK_F5 oDlg:disable() CallSecondWindowWithEventLoop() oDlg:enable() This works beautifully - the first window is disabled while the second one is active. In addition, if a third window is required, the same thing can be repeated - the second window is disabled while the third one is open. This technique is part of the answer to our question of how to have multiple layers. As long as all of these windows are in the same thread, and each one has an event loop with procedural behavior, any number of layers may be created. |
Streams |
I call this
structure a Stream, where the first window opens in a new thread, and
a stream of sub-windows emanates from it, each one disabling itself and
calling the next. The only active window in a Stream is the
most recent one. The Main window is always active and a new Stream
can be opened at any time. The first window in a Stream is a Child and is
called from the main menu. Any number of Streams can exist at once,
each in its own thread. Note that there are 10 open windows in
this diagram, each with an event loop, but only 4 are active - one for each thread: Main (Thread 1) / | \ Stream (Thread 2) Stream (Thread 3) Stream (Thread 4) Layer 1 - Child Layer 1 - Child Layer 1 - Child Layer 2 - ??? Layer 2 - ??? Layer 2 - ??? Layer 3 - ??? Layer 3 - ??? Layer 3 - ??? This is a very powerful architecture. Imagine the following scenario: You have a Customer window open (Stream 1) and are processing an order that came in over the Internet. You are down in the 3rd layer (Customer/Invoice/Inventory) and are about half way through the order. Suddenly the phone rings and it's the Big Boss demanding to know ASAP how many Blue Widgets are in stock and what is the discount price schedule. You go to the main menu and open an Inventory window (Stream 2) and drill down several layers to get all the information (the Inventory table is now open a 2nd time, but there are no conflicts). While you're doing that someone from accounting rushes in from the next cubicle needing you to change the credit rating for a certain customer. You go to the main menu and open another customer window (Stream 3), locate the customer record and make the change (you are now editing two different customer records at the same time). Then you Close Stream 3, phone Big Boss with the Blue Widget information, Close Stream 2, and go back to work right where you left off. I'm sure you can think of other scenarios. This design is extremely flexible and can be used in a wide variety of ways. Control over what Stream can be opened, or how many and how often, can be maintained by disabling and enabling menu items. Note that the sub-layer windows should not be Modals. If a Modal is opened in any Stream, it disables Main and any open children downstream from Main (in effect, the entire application). It is the only active window. Everything else stops until the Modal closes: Main (Thread 1) / | \ Stream (Thread 2) Stream (Thread 3) Stream (Thread 4) Layer 1 - Child Layer 1 - Child Layer 1 - Child Layer 2 - ??? Layer 2 - ??? Layer 2 - ??? Layer 3 - Modal Layer 3 - ??? Layer 3 - ??? Using a Modal for one of the working layers would prevent us from working in, or opening, another Stream. Therefore, Modals should be reserved for windows that are open for brief periods of time, such as pop up message boxes and picklists, or any purpose for which it is necessary to prevent other activity. The only remaining question, then, is what window type to use for the sub layers? |
Layered Siblings |
We have already
ruled out Modals. Looking at our chart of window types in Part 1, we
are left with StepChild, SubMain, and Layered Sibling .
StepChild and SubMain, discussed further below, have behavior characteristics that are undesirable
for this purpose. Layered Siblings, on the other hand, behave
exactly as needed. Note: Child and Layered Sibling windows are technically the same and have identical behavior. The term "Child", however, along with its technical definition, is universally understood to mean a window called from Main (or other top-level window), either with or without an event loop. So we use "Layered Sibling" (or just Sibling for short) specifically to mean a Child window with an event loop that is called from within an existing Child window that is disabling itself. It is a term which becomes synonymous with the technique itself. Siblings are ordinary Child windows in every respect. They have title bars with system menus, icons and buttons. They can be moved, resized, minimized and maximized. They respond predictably to all the different ways we have of modifying their appearance and behavior. When one is closed, focus automatically goes to the next one up-Stream. If a Stream is closed, focus goes to another Stream. In short, they behave in all the predictable ways which we are used to and expect. There are no strange surprises. So here we have our completed model - any number of layers in a Stream, any number of Streams. Complete control with predictable behavior - and it's the easiest of all the techniques. Main (Thread 1) / | \ Stream (Thread 2) Stream (Thread 3) Stream (Thread 4) Layer 1 - Child Layer 1 - Child Layer 1 - Child Layer 2 - Sibling Layer 2 - Sibling Layer 2 - Sibling Layer 3 - Sibling Layer 3 - Sibling Layer 3 - Sibling In so many areas of life things that produce the best results usually require more work. How nice to find this happy convergence, where the easiest way is also the most powerful and flexible. |
Using StepChild Windows |
Can we use a
StepChild for these layers? Yes, but it is not recommended. There are several problems, all
caused by its parent being AppDeskTop(): 1) If there is a Child active in another Stream, the StepChild will cover the Child, even when the Child has focus. So in order to use StepChilds, they must all be Stepchilds, so they can compete equally. 2) When a StepChild has focus, the Main title bar and menu bar are grayed. Main looks disabled, just like it does when a Modal is open. This can be disconcerting to the user, who may think that the main menu is not usable. 3) If a Stream made of StepChilds is closed while another Stream is open, focus returns to Main, not to the other stream. This is a relatively minor quibble, but it is unexpected behavior and can be annoying. 4) A keystroke handler attached to a Stepchild window will not receive any number or letter keys. Only "system" keystrokes are passed to it by its event handler (Esc, Enter, Tab, Backspace, etc). There is a clever workaround for this, but it requires extra work and care. On a complex form it can add an extra layer of difficulty to the control logic. 5) If a Modal, such as a message box, is called from a StepChild, the StepChild is not disabled (because the StepChild is not a child of Main). That leaves two active windows with event loops in the same thread, so the StepChild must be disabled. There are basically three ways to do this: a) Disable/enable the StepChild when calling the modal, just like our technique of calling layers. This can present a problem when calling popups from in-line expressions such as in validation codeblocks, or deep within lookup routines - it can be a lot more work. b) Send the StepChild into SetAppWindow() so that it becomes the owner of the Modal and gets disabled. This has the same problem as a), and, even worse, Main (and the rest of the application) is not disabled, defeating the purpose of the Modal. There is potential for trouble here. As stated in Part 1, it is a general principle in GUI applications not to pass other windows into SetAppWindow() because it is expected this will always return a reference to Main. In this StepChild scenario, Main is still active while the Modal is open. The user could start a routine from the main menu which calls SetAppWindow(), and it would receive the StepChild instead of Main. This is a poor programming practice and should be discouraged. There are better ways to do things. Passing windows to SetAppWindow() should never be done in a GUI app. c) Pass the StepChild into the Modal routine as a parameter to override the default owner. This avoids the difficulty of a) and doesn't change SetAppWindow(), but it still has the problem of not disabling Main. Note: There are legitimate situations (discussed below) which call for specifying alternate owners for Modals, and using a parameter has proven to be the easiest way to do it. For that purpose, all Modal routines in Top-Down Library have an oOwner parameter. Unfortunately, the Xbase++ function MsgBox() does not have one. Confirmbox() does, though, so obviously the author was thinking about this. Why it was omitted in Msgbox() is a mystery. For anyone who would like to have a message box with the parameter, I have written a generic message box function called Gmsgbox(), which can serve as a replacement. So the best solutions are a) and disable the StepChild yourself, or c) and disable Main yourself - these are kludgy at best. Clearly, StepChilds are not good general purpose windows in MDI applications. They simply aren't worth the trouble. What, then, are they good for? They seem to be especially made for two purposes: 1) Applications that have small Main windows with StepChilds floating around them. These tend to be special purpose utility programs. A perfect example is the Xbase++ Form Designer. The Delphi IDE also uses this design. Note, however, that while sometimes used, this design is not absolutely necessary for these types of applications - it really is a matter of personal preference. The Visual dBase and Visual Objects IDEs both use full screen Mains with Child windows, and I chose that design for the Top-Down Form Designer. I find that seeing background objects between the application's working windows to be extremely distracting. 2) Applications with floating toolbar windows. When the :tasklist attribute is False, a StepChild window will have the narrow title bar and small "x" button commonly seen on toolbar windows: The best example I have seen of this is Adobe Photoshop. There is a resizable Main window and four small floating StepChild tool windows. Images are displayed in Child windows, so a tool window can never be covered by an image. The user is free to resize Main to suit the need. It is an excellent design. |
Using SubMain Windows |
Can
we call a SubMain as a layer? Yes, but there are several things
which make it unfit for this purpose. As a top-level window it
competes equally with Main. If it is a smaller window and you click
anywhere on Main, Main comes to front and the SubMain disappears behind it.
It can be regained by clicking on its taskbar button, but it is annoying
to have to be so careful about where to click. The disappearing can
be prevented by setting its StayOnTop attribute, but then it covers all
windows in other Streams and dominates the entire desktop as well,
including other applications. In addition, because its parent is
AppDeskTop() it has the same keystroke handling limitation as a StepChild.
Clearly, it is not worth the trouble. So what are some good uses for
it? SubMains are excellent for special purpose windows which need to be full screen, such as a Print Preview. A full screen preview window offers the largest possible view, and at this size, clicking on Main is not an issue. An example of this can be seen in the Print Preview window in the Top-Down Demo. Another good use is for having separate modules within a large application. I once converted a large accounting program into Xbase++ using this design. Each of the major modules, such as General Ledger, Accounts Receivable, Payroll, Inventory, etc., has its own full screen window with menu bar and task bar button. Each SubMain is running in its own thread with its own event loop, just as if it was a Main window in another application. To the user, each module seems like a separate application. It allows working in more than one module without a cluttered screen. Main is programmed not to close until all SubMain module windows are closed (Top-Down Library has a template for creating these module windows). One of the primary reasons I chose this design is because of the program's extensive menu system. Each module has its own menu choices, some two or three levels deep. When they were all on the Main menu, it required drilling down two to four levels for every item needed. Initial tests showed it to be tiring and annoying to the user. A separate menu bar for each module made it much easier to use. In these modules the Layered Sibling and Modal techniques work just the same as they do in Main, but with one essential difference: all parent/owner references to Main must now become SubMain. Every Stream window called in a module must have the SubMain:drawingarea specified as the parent and owner, and every Modal must have the SubMain specified as the owner. All of my window functions have parameters for overriding these default settings, so it's just a matter of passing in the correct values. I created a Get/Set function in each module to make it easy to obtain a reference to the SubMain. So it just requires a small amount of extra typing whenever a window function is called. In this case, SubMains are an excellent solution, and well worth the extra bit of effort. |
Calling Modals From Modals |
There are situations
which could require calling a Modal from within an existing Modal.
Sometimes a work window legitimately needs to prevent other
activity while it is open. An example of this could be a dialog in
which application-wide settings are adjusted, and a Modal window is
appropriate for this sort of task. If a message box or other Modal
popup needs to be called from this window, and an ordinary Modal is used, two
problems occur: 1) Modal 2 disables Main (which is already disabled), and leaves Modal 1 active. Not only can Modal 1 still be used, but there are now two active windows with event loops in the same thread. 2) When Modal 2 closes, it enables Main. Now we are back in Modal 1, but Main and the entire application are now active again. Bad news in both cases. The correct way is to specify Modal 1 as the owner of Modal 2. Modal 1 then becomes disabled. When Modal 2 closes, it re-enables Modal 1, and Main remains disabled. As we have already seen, sending a parameter into the Modal routine is
the best way to specify an alternate owner. |
De-Activating Windows In Layers |
There are three ways to de-activate windows when using
Layered Siblings. The first has already been mentioned, but I will
summarize it again here to make it easy to compare all three. They
are all equally effective. Which one to use is only a matter of
preference for the appearance and behavior of the deactivated window. |
Conclusion |
Most developers will agree that we spend more time on interface code
than on business logic. In Windows, especially for someone just getting started,
we can spend an inordinate amount of time just trying to get the windows
to work the way we want. This should not be the case. The
window architecture should be a minor issue, not an obstacle to
productivity. I hope this information will smooth that part of
the learning curve by giving a better understanding of the window hierarchy and
how it works. It doesn't have to be a mysterious dark art. |
Part 2 - The End |
Copyright © 2002-2008 Clayton Jones |
Return to: Articles Page |Top-Down Page | Software And Services
| Home