Visual Basic

Extending the CFormAttributes 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 look at how you can add support for pseudo-MDI minimize and restore behavior. Because all document windows in an MDI application are contained within the client area of the parent window, minimizing that window naturally takes away all of the children too. This is convenient since it instantly clears the application off the desktop (without closing it, of course).

The MDI window feature in Visual Basic gives you this minimize behavior for free. With an SDI or a DIY-DI application, however, you have no such luxury. Because a Visual Basic form has no Minimize event, you must write code that plugs into the Resize event and decide for yourself when a form is minimized or restored by investigating the WindowState property. The behavior we're going to construct will watch for transitions from normal to minimized and from minimized back to normal. (This second operation is usually called "restore.") We'll write the code as a new method of the CFormAttributes class and then simply add a call to it from appropriate Resize event handlers.

Trapping the event, of course, is only half the story-you also 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 you end up with lots of task bar buttons (or icons in earlier versions of Microsoft Windows NT). It's quicker and visually more effective to hide all the other forms when you trap a minimize event and to restore them when you 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 you'll 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 behavior, you need to choose which forms will trigger it and call PropagateMinMaxEvents from their Resize event handlers. The publication editing program referred to in Figure 13-2 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 behavior to the example application shown in Figure 13-1, you would 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 of the definite parent form.

Visual Basic 6 has another trick that you could use here, which is to add custom Minimize and Restore events to your forms through the CFormAttributes class. You can do this very simply by making a small modification to the PropagateMinMaxEvents method shown here.

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, calls to HideAllForms and UnhideAllForms have been replaced with calls to the Visual Basic procedure RaiseEvent. This diminutive keyword is very powerful, and you'll see other examples of it later in the chapter. When you define the CFormAttributes instance on a form, a new object, My, appears in the code window's Object drop-down list box, and when you choose it, you'll see Minimize and Restore events in the Procedure drop-down list box. These events work in exactly the same way as normal events do, so selecting Minimize inserts an empty procedure named My_Minimize into the code. One caveat is that the syntax for defining the CFormAttributes instance is slightly different if you want to see the events:

Public WithEvents My As CFormAttributes

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

Private Sub Form_Load()
      Set My = New CFormAttributes
      My.LoadActions Me
  End Sub
Forms Are Classes Too

Forms are really classes in disguise. Once you realize this fact, you can start using it to your advantage. The similarity isn't obvious because you don't have to define instances of forms before you can use them. However, you can use a form's Name property to create new instances of the form at run time, just as if it were a class. What's a little confusing is that if you don't create any instances at run time, you always get one for free-and it has the same name as the class. Thus, referring to Form1 at run time means different things in different contexts:

Form1.Caption = "My Form"         ' Form1 is an object name.
  Dim frmAnotherForm As New Form1   ' Form1 is a class name.

The fact that forms are really classes is why defining public variables at the module level in a form appears not to work-trying to assign to these variables causes "Variable not defined" errors. In fact, you're defining properties of the form, and these work in exactly the same way as class properties do. To refer to such properties in code, you need to qualify them with the object name, which, you'll recall, is usually the same as the class name. (This is confusing if you do actually create multiple instances.) Even more interesting is that you can also define Property Let and Property Get procedures, Public methods, and even Friend functions in forms, just as you can in classes.

Because Visual Basic doesn't support inheritance at the source code level, you can't build value-added form classes; the best you can do is to build value-added form instances by adding custom properties and methods to your forms. You can do this by exploiting the classlike nature of forms and writing a form base class that contains extra properties and methods you'd like to see on every form. This works very well in practice, although it relies on you adding some standard code to every form you create. To see how this works, let's build some methods to save and restore a form's position when it loads and unloads.

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

Private frmPiSelf As Form
  Public Sub SavePosition()
      SaveSetting App.Title, "Form Positions", _
                         frmPiSelf.Name & "-top", frmPiSelf.Top
      .
      .
      .
  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 the LoadActions and UnloadActions methods are also defined. These make the class more general for when you add to it later. To add new properties to a form, you need to adopt certain conventions. First you need to define an instance of the class as a form-level variable:

Public My As New CFormAttributes

The variable is named My because it's pretty close to Me, and semantically the two are similar. For example, you can now refer to My.UnloadActions. The only other thing you need to do is to 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

You do have to pass the owner form reference to LoadActions to initialize the class's Self property.