MDI Applications

Multiple document interface (MDI) applications permit more than one document to be open at a time. This is in contrast to single document interface (SDI) applications, which can manipulate only one document at a time. Visual Studio .NET is an example of an MDI application—many source files and design views can be open at once. In contrast, Notepad is an example of an SDI application—opening a document closes any previously opened document.

There is more to MDI applications than their ability to have multiple files open at once. The Microsoft Windows platform SDK specifies several UI behaviors that MDI applications should implement. The Windows operating system provides support for these behaviors, and this support is exposed through Windows Forms as well.

Parent and Child Forms

MDI applications consist of a main form, which does not itself display any data, and one or more child forms, which appear only within the main form and are used for displaying documents. The main form is called the MDI parent, and the child forms are called the MDI children.

The Form class has two properties that control whether a given form is an MDI parent, MDI child, or neither. The Boolean IsMdiContainer property determines whether a form behaves as an MDI parent. The MdiParent property (which is of type Form) controls whether a form behaves as an MDI child. Setting the MdiParent property of a form to reference the application’s MDI parent form makes the form an MDI child form. Example 4-6 shows the minimum amount of code required to display an MDI parent form containing a single MDI child form.

Example 4-6. A minimal MDI application

Imports System
Imports System.Windows.Forms

Public Module AppModule
   Public Sub Main(  )
      Application.Run(New MainForm(  ))
   End Sub
End Module

Public Class MainForm
   Inherits Form
   
   Public Sub New(  )
      ' Set the main window caption.
      Text = "My MDI Application"
      ' Set this to be an MDI parent form.
      IsMdiContainer = True
      ' Create a child form.
      Dim myChild As New DocumentForm("My Document", Me)
      myChild.Show
   End Sub

End Class

Public Class DocumentForm
   Inherits Form
   
   Public Sub New(ByVal name As String, ByVal parent As Form)
      ' Set the document window caption.
      Text = name
      ' Set this to be an MDI child form.
      MdiParent = parent
   End Sub
   
End Class

Assuming that the code in Example 4-6 is saved in a file named MyApp.vb, it can be compiled from the command line with this command:

vbc MyApp.vb /r:System.dll,System.Windows.Forms.dll

Running the resulting executable produces the display shown in Figure 4-7.

A minimal MDI application (the output of the code in Example 4-6)

Figure 4-7. A minimal MDI application (the output of the code in Example 4-6)

The Form class has two read-only properties related to MDI behavior. The IsMdiChild property returns a Boolean value that indicates whether the form is an MDI child. The MdiChildren property of a parent form contains a collection of references to the form’s child forms. The IsMdiChild and MdiChildren properties are both automatically maintained in response to setting the child forms’ MdiParent properties.

Creating a Window Menu

MDI applications usually have a main-menu item called Window. On this menu appear standard items for cascading, tiling, and activating child windows and arranging the icons of minimized child windows. Figure 4-8 shows a typical example.

A typical Window menu

Figure 4-8. A typical Window menu

Such a menu is easy to create using the support in Windows Forms. Assuming that you were to do it programmatically, Example 4-7 shows a revised version of Example 4-6 that has been modified to include a Window menu; the added code is shown in boldface. For details on how to work with menus from the Visual Studio IDE, as well as programmatically, see Section 5.5 in Chapter 5.

Example 4-7. An MDI application with a Window menu

Imports System
Imports System.Windows.Forms

Public Module AppModule
   Public Sub Main(  )
      Application.Run(New MainForm(  ))
   End Sub
End Module

Public Class MainForm
   Inherits Form

   ' Declare MainForm's main menu
   Private myMainMenu As MainMenu
   
   ' Declare Windows menu
   Protected WithEvents mnuWindow As MenuItem
   Protected WithEvents mnuTileHoriz As MenuItem
   Protected WithEvents mnuCascade As MenuItem
   Protected WithEvents mnuTileVert As MenuItem
   Protected WithEvents mnuArrangeAll As MenuItem

   Public Sub New(  )
      ' Set the main window caption.
      Text = "My MDI Application"
      ' Set this to be an MDI parent form.
      IsMdiContainer = True
      ' Create main menu
      MyMainMenu = New MainMenu(  )
      ' Define menu items
      mnuWindow = New MenuItem(  )
      mnuTileHoriz = New MenuItem(  )
      mnuTileVert = New MenuItem(  )
      mnuCascade = New MenuItem(  )
      mnuArrangeAll = New MenuItem(  )
      ' Set menu properties
      mnuWindow.Text = "&Window"
      mnuWindow.MdiList = True
      mnuTileHoriz.Text = "Tile Horizontally"
      mnuTileVert.Text = "Tile Vertically"
      mnuCascade.Text = "Cascade"
      mnuArrangeAll.Text = "Arrange Icons"
      ' Add items to menu
      MyMainMenu.MenuItems.Add(mnuWindow)
      mnuWindow.MenuItems.Add(mnuCascade)
      mnuWindow.MenuItems.Add(mnuTileHoriz)
      mnuWindow.MenuItems.Add(mnuTileVert)
      mnuWindow.MenuItems.Add(mnuArrangeAll)
      ' Assign menu to form
      Me.Menu = MyMainMenu
      ' Create a child form.
      Dim myChild As New DocumentForm("My Document", Me)
      myChild.Show
   End Sub

   Public Sub mnuCascade_Click(o As Object, e As EventArgs) _
              Handles mnuCascade.Click
      LayoutMdi(MdiLayout.Cascade)   
   End Sub

   Public Sub mnuTileHoriz_Click(o As Object, e As EventArgs) _
              Handles mnuTileHoriz.Click
      LayoutMdi(MdiLayout.TileHorizontal)   
   End Sub

   Public Sub mnuTileVert_Click(o As Object, e As EventArgs) _
              Handles mnuTileVert.Click
      LayoutMdi(MdiLayout.TileVertical)   
   End Sub

   Public Sub mnuArrangeAll_Click(o As Object, e As EventArgs) _
              Handles mnuArrangeAll.Click
      LayoutMdi(MdiLayout.ArrangeIcons)   
   End Sub

End Class

Public Class DocumentForm
   Inherits Form
   
   Public Sub New(ByVal name As String, ByVal parent As Form)
      ' Set the document window caption.
      Text = name
      ' Set this to be an MDI child form.
      MdiParent = parent
   End Sub
   
End Class

To add a Window menu to the parent form of an MDI application, perform the following steps. First, add a menu item to the MDI parent form’s main menu, setting its Text property to anything desired (usually Window) and its MdiList property to True. It is the MdiList property that makes the Window menu a Window menu. Setting the MdiList property to True causes the Windows Forms framework to add and delete menu items to and from this menu item as necessary. This in turn will always display the current list of MDI child windows in the menu.

Next, add menu items for Cascade, Tile Horizontally, Tile Vertically, and Arrange Icons. In the Click event handler for each of these menu items, call the Form class’s LayoutMdi method, passing the appropriate parameter value for the desired action.

The syntax of the LayoutMdi method is:

Public Sub LayoutMdi(ByVal 
value As MdiLayout)

The method’s single argument must be a value from the MdiLayout enumeration (defined in the System.Windows.Forms namespace). The values in this enumeration are:

ArrangeIcons

Indicates that the icons for the minimized MDI child windows should be neatly arranged.

Cascade

Indicates that the MDI child windows should be cascaded (displayed overlapping each other).

TileHorizontal

Indicates that the MDI child windows should be tiled (displayed without overlapping), with each child window filling the width of the MDI parent.

TileVertical

Indicates that the MDI child windows should be tiled, with each child window filling the height of the MDI parent.

Merging Menus

Often, the items that should appear on an MDI application’s main menu are dependent on the type of document being displayed or on whether any document is displayed at all. Of course, this effect could be achieved in code by dynamically adding and removing menu items each time a child window is activated. However, the Windows Forms framework provides an easier way.

If an MDI child form has a main menu of its own, it and the MDI parent form’s main menu are merged to produce the menu that is shown to the user when the child form is displayed. Two properties of the MenuItem class affect how the menu items are merged. First, the MergeOrder property determines the order in which the menu items are displayed. This property can be set to any Integer value, and the values don’t have to be contiguous. The menu items from the two menus are sorted on this value to determine the order in which the menu items are displayed on screen.

For example, consider an MDI parent form that has a main menu with three menu items representing File, Window, and Help menus. Further, say that the MergeOrder properties of these menu items are 10, 20, and 30, respectively. Now, if an MDI child form is displayed and its main menu has, for example, an Edit item with a MergeOrder property value of 15, the menu displayed to the user will have four items: File, Edit, Window, and Help, in that order. Example 4-8 shows a revised version of Example 4-6 that contains the code necessary to create such a menu; lines shown in boldface have been added to define the main menu and its menu items.

Example 4-8. An MDI application with merged menus

Imports System
Imports System.Windows.Forms

Public Module AppModule
   Public Sub Main(  )
      Application.Run(New MainForm(  ))
   End Sub
End Module

Public Class MainForm
   Inherits Form

   ' Declare MainForm's main menu.
   Private myMainMenu As MainMenu
   
   ' Declare the Window menu.
   Protected WithEvents mnuFile As MenuItem
   Protected WithEvents mnuWindow As MenuItem
   Protected WithEvents mnuHelp As MenuItem

   Public Sub New(  )
      ' Set the main window caption.
      Text = "My MDI Application"
      ' Set this to be an MDI parent form.
      IsMdiContainer = True
      ' Create main menu
      MyMainMenu = New MainMenu(  )
      ' Define menu items
      mnuFile = New MenuItem(  )
      mnuWindow = New MenuItem(  )
      mnuHelp = New MenuItem(  )
      ' Set menu properties
      mnuFile.Text = "&File"
      mnuFile.MergeOrder = 10
      mnuWindow.Text = "&Window"
      mnuWindow.MergeOrder = 20
      mnuWindow.MdiList = True
      mnuHelp.Text = "&Help"
      mnuHelp.MergeOrder = 30
      ' Add items to menu
      MyMainMenu.MenuItems.Add(mnuFile)
      MyMainMenu.MenuItems.Add(mnuWindow)
      MyMainMenu.MenuItems.Add(mnuHelp)
      ' Assign menu to form
      Me.Menu = MyMainMenu
      ' Create a child form.
      Dim myChild As New DocumentForm("My Document", Me)
      myChild.Show
   End Sub

End Class

Public Class DocumentForm
   Inherits Form

   ' Declare menu
   Private mdiMenu As New MainMenu
   ' Declare menu items
   Protected WithEvents mnuEdit As MenuItem

   Public Sub New(ByVal name As String, ByVal parent As Form)
      ' Set the document window caption.
      Text = name
      ' Set this to be an MDI child form.
      MdiParent = parent
      ' Instantiate menu and menu items
      mdiMenu = New MainMenu(  )
      mnuEdit = New MenuItem(  )
      ' Set menu properties
      mnuEdit.Text = "&Edit"
      mnuEdit.MergeOrder = 15
      ' Add item to main menu
      mdiMenu.MenuItems.Add(mnuEdit)
      ' Add menu to child window
      Me.Menu = mdiMenu
   End Sub
   
End Class

If a menu item in the MDI child form menu has the same MergeOrder value as a menu item in the MDI parent form menu, a second property comes into play. The MergeType property of both MenuItem objects is examined, and the behavior is determined by the combination of their values. The MergeType property is of type MenuMerge (an enumeration defined in the System.Windows.Forms namespace) and can have one of the following values:

Add

The menu item appears as a separate item in the target menu, regardless of the setting of the other menu item’s MergeType property.

MergeItems

If the other menu item’s MergeType property is also set to MergeItems, the two menu items are merged into a single item in the target menu. Merging is then recursively applied to the subitems of the source menus, using their MergeOrder and MergeType properties.

If the other menu item’s MergeType property is set to Add, both menu items appear in the target menu (just as though both had specified Add).

If the other menu item’s MergeType property is set to Remove, only this menu item appears in the target menu (again, the same as specifying Add for this menu item).

If the other menu item’s MergeType property is set to Replace, only the child form’s menu item is displayed, regardless of which one is set to MergeItems and which one is set to Replace. (This seems like inconsistent behavior and may be a bug.)

Remove

The menu item isn’t shown in the target menu, regardless of the setting of the other menu item’s MergeType property.

Replace

If the other menu item’s MergeType property is set to Add, both menu items appear in the target menu (just as though both had specified Add).

If the other menu item’s MergeType property is set to MergeItems or Replace, only the child form’s menu item is shown. (This seems like inconsistent behavior and may be a bug.)

If the other menu item’s MergeType property is also set to Replace, only the child form’s menu item is shown.

Detecting MDI Child Window Activation

Code in the MDI parent form class can be notified when an MDI child form becomes active inside an MDI parent form. (“Active” means that the child form receives the input focus after another MDI child form or the MDI parent form had the input focus.) To receive such notification, the MDI parent form must override the OnMdiChildActivate method (defined in the Form class). For example:

' Place this within the class definition of the MDI parent form.
Protected Overrides Sub OnMdiChildActivate(ByVal e As EventArgs)
   MyBase.OnMdiChildActivate(e) ' Important
   ' ...
End Sub

It is important to call the base-class implementation of OnMdiChildActivate within the overriding function, so that any necessary base-class processing (including raising of the MdiChildActivate event) can occur.

The e parameter carries no information. To find out which MDI child form became active, read the ActiveMdiChild property of the MDI parent form. This property is of type Form. Convert it to the MDI child form’s type to gain access to any public members that are specific to that type. For example:

Protected Overrides Sub OnMdiChildActivate(ByVal e As EventArgs)
   MyBase.OnMdiChildActivate(e)
   ' Assumes that SomeFormType is defined elsewhere and inherits
   ' from Form. Also assumes that the MDI child forms in the
   ' application are always of this type.
   Dim childForm As SomeFormType = _
      CType(ActiveMdiChild, SomeFormType)
   ' Do something with childForm here.
   ' ...
End Sub

To have code outside of the MDI parent form class notified when an MDI child form becomes active, write a handler for the MDI parent form’s MdiChildActivate event. This event is defined in the Form class as:

Public Event MdiChildActivate( _
   ByVal sender As Object, _
   ByVal e As EventArgs _
)

The sender parameter is the MDI parent form, not the MDI child form that has been activated. The e parameter does not contain any additional information about the event. As when overriding the OnMdiChildActivate method, read the MDI parent form’s ActiveMdiChild property to discover which MDI child form has been activated.

Get Programming Visual Basic .NET now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.