Home
Foreword
Preface
Class Idioms
Collections
Implements
Constructors
Terminate
Forms
On Error
Frameworks
F.A.Q.
Value-added
FSMs
Constants
GOTO
Hungarian
Nothing
Properties
Big EXEs

When it comes to designing the user interface, Visual Basic often frustrates our intentions because it doesn't implement all the window-management features we see in other Microsoft applications. This chapter focuses on the design of a window management scheme to show how we can use classes to add value to our forms and implement some of the features we need.

The most striking feature of most commercial Visual Basic applications is the set of windows they create and manipulate, the way the user navigates these windows and the ways in which the windows interact with each other. Often, a window management scheme is something that simply evolves during implementation: we may have no clear idea about how each window will behave with respect to other windows, and the window-management features built into Visual Basic may be the factor that most influences how things turn out. This isn't so much a poor design strategy as a lack of strategy, and the resulting problems can be anything from poor usability to insidious bugs.

Visual Basic has a mixed bag of window-management tricks, the simplest of which are MsgBox and InputBox. These are modal dialogs, so we must deal with them before the program will take them away. There isn't much to say about MsgBox and InputBox except that they are inflexible. In particular, we can't change the button captions and we don't have complete control over the size and position of the dialogs. For added flexibility we can, of course, write Visual Basic functions called MsgBox and InputBox to override the Visual Basic ones. Interestingly, doing this allows us to make non-modal message boxes, the consequences of which will become clear later.

Visual Basic also has features to support MDI ('Multiple Document Interface') applications, and the decision to build with MDI will have a major influence on the way our application works. There are some advantages to MDI –  chiefly that it is well-defined and that Visual Basic will implement some of the features for us (menu management, window arrangements and so on). On the other hand, adopting MDI for these reasons alone is futile if the application we want to build doesn't fit the rigid MDI model. MDI supports a document model, and usually makes sense only if our application is going to work with multiple instances of things which are somehow document-like. 'Document-like' qualities include serialisation (binding to permanent storage) and size-independence, which gives meaning to familiar MDI window-arrangement functions Tile and Cascade .

On the other side of the fence from MDI is SDI ('Single Document Interface'), although as Visual Basic has no specific functions to support SDI a more accurate description is 'not MDI'. We have much more flexibility when building non-MDI applications, but we lose out on some of the free functionality such as window arrangement, child window menu handling and CTRL-TAB to switch between child windows. On the other hand we have more control over our application's appearance, and we can choose whether to make our child forms modal or modeless. At the time of writing, the current version of Visual Basic is 6.0, which pre-dated Microsoft's adoption of the MTI ('Multiple Top-level Interface') model. Thus VB has no explicit support for that model either.

Finally, we can build hybrid applications that borrow some features from MDI (or MTI) without actually using built-in functionality. We can, for example, create multiple instances of an ordinary form (forms behave just like classes in many ways), and we can even create MDI parent forms dynamically within a non-MDI application. It's important that we look at these issues in advance and plan a window-management scheme appropriate to the application we want to build.

Modal or Modeless?

Whether we choose MDI, MTI, SDI or our own brand of DIY-DI, we'll need to think about modality. Modality is probably one of the most important issues in the design of our window management scheme, since it can significantly affect program complexity. Using modal forms wherever possible helps to keep a lid on complexity but it can also get in the way by imposing artificial restrictions on our users. Modality is one of the more contentious issues of user interface design, but the evidence in favour of radical modeless design is far from conclusive. Suffice it to say that we're more concerned here with the implications of modality on our implementation than with the psychology of interface design.

When we show a form, Visual Basic lets us specify whether we want to show it modally or non-modally, using the constants vbModal and vbModeless. However, this isn't a very flexible way of implementing modes: a vbModal form is task-modal, which means it locks out all user input from the rest of the application. This is really only suitable for pop-up dialogs, and the only way we can show two forms together is if they have a parent-child relationship. This imposes a particular set of design restrictions on our application, and it may prevent us from doing what we want. It's also impossible to display a non-modal form from a modal form, another potentially intolerable situation.

Consider the following example program:


An example of function modality: form A is not accessible here

This shows a non-MDI application with several distinct  functions invoked from a main menu. Perhaps it's a database maintenance program, and we would like to be able to refer to display functions whilst we're using update functions. At the instant shown, two functions are executing at the same time, and we can consider forms A and C as 'parent' forms for each function. Parent A is also displaying a child form.

Although the forms shown here are relatively simple, it's quite likely that we want form A to display form B modally, or, more specifically, for form A to be inaccessible for as long as form B is on the screen. The conventional way to code this is for form A to do FormB.Show vbModal, but this locks all user input from any form except form B – including the main menu. Hence it wouldn't be possible to reach the situation we see here. The alternative, FormB.Show vbModeless, doesn't prevent us from accessing multiple functions at the same time, but it interferes with the design of each function and greatly increases the complexity of the program. Clearly we need to find something in between.

If Visual Basic's built-in support for modal forms is geared towards simple pop-up dialogs, that doesn't stop us from building modes by other means. Forms have an 'enabled' property which, when set to False, mimics very effectively what happens to a parent form when it shows a vbModal child. Now that we're in control, however, we are free to enable and disable forms at will, without the restrictions imposed by vbModal.

Returning to our example, all we need to do is arrange to disable Form A when Form B  loads, and re-enable it when Form B unloads (or possibly on Show and Hide instead of Load and Unload). This implements a new kind of mode that's more appropriate to our requirements; we might call it 'function modality', since we're creating an architecture where it's permissible to hop back and forth between functions, yet where each function is effectively a modal cascade of forms. This, of course, is only one possible architecture and we can conceive of others, such as this one:

A less orthodox architecture: the Create and Review forms swap
with each other
 

Here we have another database application, one that's used to keep records of technical publications. Users can choose an existing entry from the list and edit it using the Review form, or enter a new publication by calling up New. Notice that the New screen has a 'Review' button, and the Review screen has a 'New' button –  this could imply multiple instances of each screen, but let's say that the design calls for screens to be swapped when these buttons are used. For example, we call up the New screen to enter the details for a new publication, and then we press the Review button to move immediately to the Review screen to enter a review for it. As the New screen loads, it replaces the Review screen, which is unloaded. As before, the Selector screen is disabled when either the Review screen or the New screen is displayed.

There is no elegant way to implement this architecture using Visual Basic's standard modality features; to do so we would have to somehow defer our request for the Review form to be displayed until the New form was unloaded. We could make it work, but it would be tricky and it's ugly. It would be much better if we could devise a general mechanism to support the kind of modes we want to enforce.

Building a Form Base Class

Forms are really classes in disguise (see Chapter 1), but because Visual Basic doesn't support inheritance at the source code level, we can't build value-added form classes; the best we can do is to build value-added form instances by adding custom properties and methods to our forms. We can do this by exploiting the class-like nature of forms and writing a form 'base class' that contains extra properties and methods we'd like to see on every form. This works very well in practice, although it relies on us adding some standard code to every form we create. To see how it works, let's build some methods to save and restore a form's position when it loads and unloads.

The first thing we need to do is define a class, which we'll call CFormAttributes. We'll create a Public instance of this class in every form we create, and this will appear as a property of the form. When we come to store the form positions with SaveSetting it would be nice to use the form name as a key, but unfortunately there isn't any way for an instance of a Visual Basic class to refer to the object that owns it. This means we'll need to define the owner as a property in our CFormAttributes class and arrange to set it when we create the instance. Here's the class:

Private frmPiSelf As Form

Public Sub SavePosition()
    SaveSetting App.Title, "Form Positions", _
                       frmPiSelf.Name & "-top", frmPiSelf.Top
    ' ...etc
End Sub

Public Sub RestorePosition()
    ...
End Sub

Public Sub LoadActions(ByVal frmiMe As Form)
    Set frmPiSelf = frmiMe
    RestorePosition frmPiSelf
End Sub

Public Sub UnloadActions()
    SavePosition frmPiSelf
End Sub

Notice that we've also defined LoadActions and UnloadActions methods. These make the class more general for when we add to it later. To add our new properties to a form, we need to adopt certain conventions. Firstly we need to define an instance of the class as a form-level variable:

Public My As New CFormAttributes

We've called the variable 'My' because it's pretty close to 'Me', and semantically the two are very similar. For example, we can now refer to My.UnloadActions. The only other thing we need to do is make sure the LoadActions and UnloadActions routines are called:

Private Sub Form_Load()
    My.LoadActions Me
End Sub

Private Sub Form_Unload()
    My.UnloadActions
End Sub

Note that we have to pass the owner form reference to LoadActions to initialise our class's Self property.

Towards a General Modality Class

We can create and manipulate value-added forms by building a CFormAttributes class and adding our function modality mechanism to it. The central requirement for such a mechanism is to associate a parent with each form we create. We can do this by adding a Parent property to the CFormAttributes class:

Public Parent As Form

Now we have somewhere to store a reference to the parent form, so we need to arrange for it to be set when the form is loaded. Since we can't pass parameters to a form's Show method (or our CFormAttributes instance) we need to do this manually from outside the CFormAttributes class. We want to be able to do something like this:

Public Sub ShowChild Child:=frmReview, Parent:=Me

We could make this a global procedure and give it its own .BAS file but we can keep all the code in one place by making ShowChild a method of the CFormAttributes class. Obviously this means we can't invoke ShowChild to display the first form in a hierarchy, but the only implication of this is that we need to make sure that the CFormAttributes class recognises that it has no parent when it is destroyed. We can also dispense with the Parent parameter, since we already have a Self reference in the CFormAttributes class. Here's the method, which we'll actually call NewChild:

Public Sub NewChild(ByVal frmiChild As Form)
    frmiChild.Show
    Set frmiChild.My.Parent = frmPiSelf
    frmPiSelf.Enabled = False
End Sub

The last statement is the significant one, because this is the one that disables the parent form and creates a new mode. We need a reciprocal action to re-enable the parent when the form unloads, so we define another method:

Public Sub EnableParent()
    If Not Me.Parent Is Nothing Then Me.Parent.Enabled = True
End Sub

Unfortunately there's no elegant way to bind this to a form unload; we must ensure that we call this method from each Form_Unload event:

Private Sub Form_Unload()
    My.EnableParent
End Sub

(In fact the sample code has a generic UnloadActions method, which takes the place of EnableParent, but it's clearer if we continue to refer to an EnableParent method.)

That takes care of modal child forms, as long as we invoke them with My.NewChild and include the appropriate reciprocal call in the Form_Unload. We can now build on this to extend the mechanism. To cope with the swapping in our second program, for example, we need to do a couple of extra things: pass on the outgoing form's parent reference to the new form, and then prevent the parent from being re-enabled when the old form unloads. We can do this by adding a new method and modifying the EnableParent method slightly so the two communicate through a module-level flag:

Private bPiKeepParentDisabled As Boolean

Public Sub SwapMe(ByVal frmiNewChild As Form)
    frmiNewChild.Show
    If frmiNewChild.Enabled Then
        Set frmiNewChild.My.Parent = Parent
        bPiKeepParentDisabled = True
    End If
    Unload frmPiSelf
End Sub

Public Sub EnableParent()
    If Not bPiKeepParentDisabled Then
        If Not Parent Is Nothing Then Parent.Enabled = True
    End If
End Sub

Notice that we check whether the form we're trying to swap to is enabled. If it isn't, it must already have been loaded, in which case we'll just leave the Parent property alone. This is an ad-hoc test that works in the simple examples we're looking at here, but it may not be general and we'd need to extend the mechanism to cope with other situations. For example, our mechanism as it stands won't prevent us from trying to swap to a form that's in the middle of a modal cascade –  in fact this would orphan any child forms in the cascade. With a little thought we should be able to extend the mechanism to allow swapping to remove child forms of the form we're trying to swap to, to prevent swapping between forms belonging to other functions in a 'function modal' situation, or to support any other flavours of modality we care to invent.

Extending the FormAttributes Class

The beauty of a value-added form class is that it's a simple matter to add new features retrospectively. As an example let's see how we can add support for pseudo-MDI minimize and restore behaviour. Because all document windows in an MDI application are contained within the Client area of the parent window, minimizing that window naturally takes all of the children away too. This is convenient, since it instantly clears the application off the desktop (without closing it, of course).

Programming with MDI windows in Visual Basic gives us this minimize behaviour for free, but with an SDI, MTI or DIY-DI application we have no such luxury. Since a Visual Basic form has no Minimize event we must write code that plugs into the Resize event and decide for ourselves when a form is minimized or restored by investigating the WindowState property. The behaviour we're going to construct will watch for transitions from normal to minimized and from minimized back to normal (this operation is usually called 'restore'). We'll write the code as a new method of our CFormAttributes class and then we can simply add a call to it from appropriate Resize event handlers.

Trapping the event, of course, is only half the story because then we need to do something to take away the rest of the forms. One possibility is to set the WindowState to follow the window containing the trap, but in practice that looks messy because Windows animates zoom boxes all over the place and we end up with lots of Task Bar buttons (or icons for earlier versions of Windows NT). It's quicker and visually more effective to hide all the other forms when we trap a Minimize event, and restore them when we trap a restore event. The only tricky part is to remember the prevailing state of each form before hiding it, just in case any were hidden already. Here's the code we need:

Public PreviouslyVisible As Boolean
Private nPiPrevWindowState As Integer

Public Sub PropagateMinMaxEvents ()
    If (frmPiSelf.WindowState = vbMinimized) _
            And nPiPrevWindowState = vbNormal Then
        Call HideAllForms
    ElseIf (frmPiSelf.WindowState = vbNormal) _
            And nPiPrevWindowState = vbMinimized Then
        Call UnhideAllForms
    End If
    nPiPrevWindowState = frmPiSelf.WindowState
End Sub

Private Sub HideAllForms()
    Dim frmForm As Form
    For Each frmForm In Forms
        If Not frmForm Is frmPiSelf Then
            frmForm.My.PreviouslyVisible = frmForm.Visible
            frmForm.Visible = False
        End If
    Next frmForm
End Sub

Private Sub UnhideAllForms()
    ' This is just the opposite of HideAllForms
End Sub

To activate the new behaviour we need to choose which forms will trigger it and call PropagateMinMaxEvents from their Resize event handlers. Our publication-editing program has this call coded in the Resize events of all the forms, so minimizing any form hides all the others and shows a single button on the Task Bar. Restoring from that button restores each form to its previous state. To add minimize behaviour to our original example application we'd code a single call to PropagateMinMaxEvents in the Resize event of the main form (the one carrying the menu bar). This mimics the MDI paradigm more closely because we have a definite 'parent' form.

Since VB5 we can add custom Minimize and Restore events to our forms through the CFormAttributes class. We can do this very simply by making a small modification to our PropagateMinMaxEvents method:

Event Minimize()
Event Restore()

Public Sub PropagateMinMaxEvents ()
    If (frmPiSelf.WindowState = vbMinimized) _
            And nPiPrevWindowState = vbNormal Then
        RaiseEvent Minimize
    ElseIf (frmPiSelf.WindowState = vbNormal) _
            And nPiPrevWindowState = vbMinimized Then
        RaiseEvent Restore
    End If
    nPiPrevWindowState = frmPiSelf.WindowState
End Sub

In case you didn't spot it, we've replaced calls to HideAllForms and UnhideAllForms with calls to RaiseEvent. When we define our CFormAttributes instance on a form, a new object, My, appears in the control list combo box, and when we choose it we see Minimize and Restore events in the event combo. These work in exactly the same way as normal events, so selecting Minimize inserts an empty procedure called My_Minimize into the code. One caveat is that the syntax for defining our CFormAttributes instance is slightly different if we want to see the events:

Public WithEvents My As CFormAttributes

Unfortunately, the New keyword is not allowed in combination with WithEvents, so we also need to add a line to the Form_Load event:

Private Sub Form_Load()
    Set My = New CFormProperties
    My.LoadActions Me
End Sub

 

Key Spinner

© 1998 - 2009 Mark Hurst. All rights reserved.   Updated March 01, 2009