Click here to Skip to main content
15,867,594 members
Articles / Desktop Programming / WPF
Article

Multiple Window Interface for WPF

Rate me:
Please Sign up or sign in to vote.
4.88/5 (51 votes)
18 Jan 2008CPOL12 min read 373.7K   19.7K   134   64
An article on the creation of multiple attachable/detachable windows inside of a WPF control.
Image 1

Table of Contents

Introduction

Multiple Document Interfaces (MDI) have come and gone in .NET and I am sure will someday come back again. In the meantime, I decided to write my very own implementation of MDI in WPF. Instead of multiple documents, I decided to extend my design to house multiple windows. The Multiple Window Interface contains many of the features of MDI with the addition of attachable windows.

Multiple Document Interface (MDI) Design

A very basic MDI should contain a parent container which can display multiple child windows (documents). The children, being windows, can be minimized, maximized, closed and resized and are confined within the bounds of the parent container. More advanced features include the ability to rearrange children within the parent. Cascade, Tile, Horizontal Tiling and Vertical Tiling are all common ways to arrange MDI children.

The Children

I wanted the child windows in my interface to act and look like windows. Since I could not add a window to a control in WPF (at least not the way I wanted to), I created a custom control which was styled to look like a window. The MwiChild class is templated to look and feel like a window. It is this object type that I used for the children of the parent container.

The MwiChild class was templated to contain a DockPanel within a Border. The DockPanel contains another DockPanel that holds the Icon, DisplayTitle and window Buttons. I added an additional ItemsControl to contain user-created Buttons. The ItemsControl makes use of the MwiChild property TitleBarButtons to add new Buttons.

XML
<Border x:Name="MwiBorder"
           Background="{TemplateBinding Background}"  
           BorderBrush="{TemplateBinding BorderBrush}"
           BorderThickness="3,1,3,3" 
           CornerRadius="2,2,2,2"                     
      >
      <AdornerDecorator>
        <DockPanel x:Name="DockPanelWindow" >
          <DockPanel x:Name="PanelTitle" DockPanel.Dock="Top" 
                     Style="{StaticResource MyDockPanelStyle}" 
                     MinHeight="{Binding Path=MinSize.Height}" 
                     local:Controls.MwiChild.IsDraggable="true" 
                     local:Controls.MwiChild.IsActive="{TemplateBinding IsSelected}"
                     local:Controls.MwiChild.Minimized="{TemplateBinding IsMinimized}" >
            <StackPanel x:Name="PanelButtons" Orientation="Horizontal"
                     DockPanel.Dock="Right" HorizontalAlignment="Right"
                     Visibility="{TemplateBinding PanelButtonsVisiblity}" >
              <ItemsControl x:Name="MyItemsControl1"
                     ItemsSource="{Binding TitleBarButtons}"
                     ItemsPanel="{StaticResource ButtonItemsPanel}" />

              <StackPanel Orientation="Horizontal" HorizontalAlignment="Right" >
              ...
              </StackPanel>
            <StackPanel x:Name="PanelTitleIcon" Orientation="Horizontal"
                     DockPanel.Dock="Left" local:Controls.MwiChild.IsDraggable="true" >
              <Image x:Name="ImageIcon" Source="{Binding Icon}" Width="16"
                     Height="16" Stretch="Uniform"
                     local:Controls.MwiChild.IsDraggable="true" Margin="1,0,1,0" />
              <TextBlock x:Name="TextBlockTitle" Text="{Binding DisplayTitle}"
                     Foreground="White" VerticalAlignment="Center"
                     local:Controls.MwiChild.IsDraggable="true" />
            </StackPanel>

          </DockPanel>

I added three attached properties to MwiChild: IsDraggable, IsActive and Minimized. IsDraggable is used on a control that can be clicked to drag on the underlying Canvas. The IsActive property is used to invoke a state change in a UIElement. Depending on the value, the UIElement template can be altered. Finally, Minimized is used to denote another status change. This one is denoting whether or not the UIElement is minimized. Again, it is used in conjunction with a template alteration.

In order to get the coloring that I wanted for each window, I created a converter class called SolidBrushToColorConverter. The converter is, as named, a converter between a SolidColorBrush and the Color that it contains. To facilitate color matching throughout the control, I added a parameter to the converter. The parameter takes an int and uses that value to add an offset to the resulting color. Therefore, if the parameter is a positive integer, the color will be lighter. Similarly, if it is a negative integer, the color will become darker. This way I can always get a variation of the original SolidColorBrush color.

C#
Color cResult = Colors.Black;
int add = 0;            
try
{
    SolidColorBrush brush = (SolidColorBrush)value;
    if (parameter != null)
        add = int.Parse(parameter.ToString());

    cResult = Color.FromArgb(255, (byte)Math.Max(Math.Min(brush.Color.R + add, 255), 0),
                                  (byte)Math.Max(Math.Min(brush.Color.G + add, 255), 0),
                                  (byte)Math.Max(Math.Min(brush.Color.B + add, 255), 0));
                 
}
catch (Exception ex) { }

return cResult;

A MwiChild contains the three standard window buttons -- minimize, maximize and close -- and an additional button for detaching an MwiChild from its parent container.

WPFMwiWindows2.JPG

In order to detach a child from its parent and maintain the integrity of its content, a new window-like container object was needed. As a result, the AttachableWindow class was created.

Attachable Windows

The AttachableWindow class inherits from Window and provides a container for the content of a MwiChild object when it is detached from its parent. An AttachableWindow takes advantage of styling in WPF to make the window look and feel like the MwiChild by which it was created. In order to style a Window, the WindowStyle and AllowsTransparency properties need to be set to None and True, respectively.

XML
<Window x:Class="WPFMwiWindows.Controls.AttachableWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Height="{Binding Path=Child.Height, Mode=TwoWay, FallbackValue=300}"
    Width="{Binding Path=Child.Width, Mode=TwoWay, FallbackValue=300}" 
    WindowStyle="None" AllowsTransparency="True" Background="Black" BorderThickness="2.0"
    xmlns:controls="clr-namespace:WPFMwiWindows.Controls" x:Name="MyWindow" >

Once those properties are set, AttachableWindow can be templated in the same way that the MwiChild was. The big difference between the template of an MwiChild and that of an AttachableWindow is the addition of a ContentPresenter within the AttachableWindow template. The ContentPresenter is used to display the content of a detached MwiChild.

XML
<Border x:Name="BorderWindow"          
        Background="{TemplateBinding BorderBrush}" 
        BorderBrush="{TemplateBinding BorderBrush}" 
        BorderThickness="3,1,3,3" 
        CornerRadius="2,2,2,2" >
    <DockPanel x:Name="DockPanelWindow" >
        ...
        <ContentPresenter Content="{Binding Child.Content}" DockPanel.Dock="Top" />
    </DockPanel>
</Border>

At this point, the AttachableWindow looks very much like the MwiChild that spawned it. The AttachableWindow contains the same three standard window buttons as the MwiChild and an additional attach button. This button is used to reattach the MwiChild to its parent container.

WPFMwiWindows3.JPG

Since the AttachableWindow was given a WindowStyle of None, it can no longer be dragged or resized. In order to allow dragging of the AttachableWindow, a few lines of code needed to be added to the class. The window should only be draggable when the left mouse button is down on the titlebar (not including the titlebar buttons). To ensure this, a PreviewMouseLeftButtonDown event needed to be handled on the StackPanel that contained the appropriate portion of the titlebar.

XML
<StackPanel x:Name="PanelTitle" Orientation="Horizontal" Background="Transparent" 
               PreviewMouseLeftButtonDown="DragWindow" DockPanel.Dock="Left" >
    <Image x:Name="ImageIcon" Source="{TemplateBinding Icon}" 
              Width="16" Height="16" Stretch="Uniform" Margin="1,0,1,0" />
    <TextBlock x:Name="TextBlockTitle" Text="{Binding Path=Child.DisplayTitle}"
                  Foreground="White" VerticalAlignment="Center" />
</StackPanel>

Once the left mouse button event is caught, a simple call to DragMove takes care of all the window dragging.

C#
protected void DragWindow(object sender, MouseEventArgs e)
{
    this.DragMove();
}

Unfortunately, resizing an AttachableWindow is not as simple (at least, not the way I wanted resizing to work). In order to get the result I desired, I created a ResizeAdorner to assist in resizing both an AttachableWindow and an MwiChild.

ResizeAdorner

The ResizeAdorner inherits from Adorner and is responsible for allowing the user to resize an AttachableWindow or MwiChild. The adorner draws a transparent border around the object capable of being resized. The border consists of twelve points of contact (two for each of the four corners and one for each of the four sides). These twelve points are represented by an Enum named HotSpot. The adorner tracks mouse movements and clicks. Depending on the hot spot that the mouse is over, the cursor changes accordingly (hovering over the top side results in a SizeNS cursor). If a hot spot is clicked and held and then the mouse is moved, the adorner will run through a series of calculations to determine if and how the adorned object should be resized. The MwiChild contains a property for the minimum size of an instance. Amongst its resize calculations, the ResizeAdorner checks to see if the resized height or width is smaller than these minimum values. Since an AttachableWindow contains a property for the MwiChild which spawned it, the adorner can retrieve the same minimum values when resizing a detached child.

The Parent

The MwiWindow class inherits from UserControl and is responsible for maintaining the MwiChild instances created. A MwiWindow maintains the current list of all children whether they are minimized, detached or attached (in a non-minimized state). It does so through a series of collections: Children, MinChildren, DetachedChildren and AttachedChildren. This way, all of the MwiChild objects created are stored in an appropriate collection that can be displayed as needed. To display the children, two ItemsControl elements are used: one for the minimized children and another for the non-minimized attached children.

XML
<DockPanel Background="Gray" x:Name="MwiGrid" >
    <ItemsControl Background="Transparent" ItemsSource="{Binding Path=MinChildren}"
                     ItemsPanel="{StaticResource MyMinItemsPanelTemplate}" 
                     DockPanel.Dock="Bottom" />
    <ItemsControl Background="Transparent" ItemsSource="{Binding Path=Children}"
                     ItemsPanel="{StaticResource MyItemsPanelTemplate}" 
                     DockPanel.Dock="Top" />
</DockPanel>

The ItemsPanel for the minimized children is a modified WrapPanel. The LayoutTransform of the WrapPanel is set so that the y-axis is flipped. This way, the panel is populated from bottom to top.

XML
<ItemsPanelTemplate x:Key="MyMinItemsPanelTemplate" >
  <WrapPanel Orientation="Horizontal" Background="Transparent" VerticalAlignment="Top" >
    <WrapPanel.LayoutTransform>
        <ScaleTransform ScaleX="1" ScaleY="-1" />

    </WrapPanel.LayoutTransform>        
  </WrapPanel>      
</ItemsPanelTemplate>

However, this is not enough. Now the objects within the panel will render upside down. To resolve this, each item in the panel needed its own LayoutTransform (once again flipping the y-axis). For the MwiChild, this was done within the template when it it is in a minimized state.

XML
<DataTrigger Binding="{Binding IsMinimized}" Value="True" >
     <Setter Property="LayoutTransform" >
       <Setter.Value>
           <ScaleTransform ScaleX="1" ScaleY="-1" />
       </Setter.Value>
     </Setter>

The non-minimized attached children needed a container of their own, one which would allow them to be dragged within the bounds of the MwiWindow. I accomplished this by implementing a modified DragCanvas. I got the DragCanvas idea from an article by Josh Smith.

DragCanvas

The DragCanvas inherits from Canvas and provides a container for MwiChild objects to be dragged around. The class basically handles mouse clicks, checking if a UIElement was clicked. If one was clicked (and it wasn't the DragCanvas), then the mouse is captured and the UIElement is dragged until the mouse is released. I modified the DragCanvas a little by adding a check for an attachable property named IsDraggable. When a UIElement is clicked, then it is hit-tested until an element with the IsDraggable property is found or the test returns no hits.

C#
protected HitTestFilterBehavior MyHitTestFilter(DependencyObject obj)
{
    isdrag = (bool)obj.GetValue(MwiChild.IsDraggableProperty);

    if (isdrag)
        return HitTestFilterBehavior.ContinueSkipSelf;
    else
        return HitTestFilterBehavior.Continue;
}

If a UIElement is draggable, then the visual tree is searched for the element's corresponding MwiChild. Once found, the DragCanvas captures the mouse as described above and allows the user the ability to drag the MwiChild object.

C#
protected override void OnPreviewMouseLeftButtonDown(MouseButtonEventArgs e)
{
    ...
        if (isdrag)
        {
            if (e.Source.GetType().Equals(typeof(MwiChild)))
                originalElement = (UIElement)e.Source;
            else
                originalElement = (UIElement)VisualTreeSearch.FindByParentType(
                    (UIElement)e.Source, typeof(MwiChild));
                 //(UIElement)e.Source;
                 //(UIElement)SearchForParentType((UIElement)e.Source, typeof(MwiChild));
            if (originalElement != null)
            {

Arrangement

Another common feature of MDI's is the ability to arrange child windows. In the MwiWindow class, I implement four methods that can be used to rearrange child windows.

Cascade

The cascade arrangement places a child window starting at the top left corner. Each additional window is then placed at an offset to the right and down. This continues until the next child cannot fit within the bounds of the parent and is placed in the top left corner where the offset begins anew.

WPFMwiWindows4.JPG

Tile

The tiling algorithm I used finds the smallest X by X grid which would fit all of the children. Then it subtracts from each column, starting at the left and moving right, until the desired number of children are placed. For example, if there are five children, then the largest X by X grid that will fit them is 3 by 3. Since there are nine possible slots for these five children, one slot is subtracted starting from the left-most column and moving right. So you remove one from the first column (leaving 8 slots), then one from the second and third (leaving 7 then 6 slots). Finally you loop back to the first column, removing another slot and leaving five slots, which is the desired number children that need to be placed (giving a final layout of 1 - 2 - 2).

WPFMwiWindows5.JPG

Tile Vertically

This simple arrangement positions the children from left to right, side by side. The children are resized to fit the height of the screen and given a width that cannot be smaller than the minimum width of a child window. Any excess children are repositioned starting back at the left of the screen.

WPFMwiWindows6.JPG

Tile Horizontally

This simple arrangement positions the children from top to bottom, one after the other. The children are resized to fit the width of the screen and given a height that cannot be smaller than the minimum height of a child window. Any excess children are repositioned starting back at the top of the screen.

WPFMwiWindows7.JPG

Using the Code

For the demo, I created a simple user interface which consists of a Menu and an MwiWindow. The "File" MenuItem allows for the creation of new child windows and the closing of the application. The "View" MenuItem contains items used to rearrange and select child windows.

WPFMwiWindows8.JPG

Each of the arranged items and the "New" MenuItem make use of the Command property. In order to function properly, the commands being used need to be registered in the code-behind.

C#
cmdBinding = new CommandBinding(ApplicationCommands.New);
cmdBinding.PreviewExecuted += new ExecutedRoutedEventHandler(delegate(object sender,
      ExecutedRoutedEventArgs e) 
    { this.gMwiWindow.CreateNewMwiChild(); e.Handled = true; });
cmdBinding.CanExecute += new CanExecuteRoutedEventHandler(delegate(object sender,
      CanExecuteRoutedEventArgs e) 
    { e.CanExecute = true; });
cmdBinding.PreviewCanExecute += new CanExecuteRoutedEventHandler(delegate(object sender,
      CanExecuteRoutedEventArgs e) 
    { e.CanExecute = true; });
CommandBindings.Add(cmdBinding);
...            
cmdBinding = new CommandBinding(MwiWindow.Cascade);
cmdBinding.PreviewExecuted += new ExecutedRoutedEventHandler(delegate(object sender,
      ExecutedRoutedEventArgs e) 
    { this.gMwiWindow.CascadeChildren(); e.Handled = true; });
cmdBinding.CanExecute += new CanExecuteRoutedEventHandler(delegate(object sender,
      CanExecuteRoutedEventArgs e) 
    { if (this.gMwiWindow.AttachedChildren.Count > 0) { e.CanExecute = true; } });
cmdBinding.PreviewCanExecute += new CanExecuteRoutedEventHandler(delegate(object sender,
      CanExecuteRoutedEventArgs e) 
    { if (this.gMwiWindow.AttachedChildren.Count > 0) { e.CanExecute = true; } }); 
CommandBindings.Add(cmdBinding);

I wanted to make sure that the Cascade command was only enabled when one or more child windows were attached. So I added a check for the number of AttachedChildren in the CanExecute and PreviewCanExecute delegates. I did this similarly for all of the arrange commands. For the New and Close commands, I set them up so that they are always enabled (as they should be).

Following the arrange items is a list of currently attached child windows. The IsChecked property of the list is bound to the IsSelected property of an MwiChild. So if a child is selected it is checked and vice-versa. The list of windows is generated by a custom control that I created, named MenuItems, and will be discussed in detail in a forthcoming article.

XML
<con:MenuItems IsCheckable="True" HeaderBindingPath="ShortTitle"
                  IsCheckedBindingPath="IsSelected" 
                  ItemsSource="{Binding ElementName=gMwiWindow, Path=AttachedChildren}"
                  TopSeparatorVisibility="Visible" />

Below the Menu, I created an MwiWindow and gave it two children. The first child contains a Grid and a TextBox. The TextBox is bound to the DisplayTitle of the MwiChild. The second child makes use of another custom control I am working on that mimics a database-like grid. It is important to note that each child must have a Canvas.Left and Canvas.Top. Otherwise it will not be draggable within the DragCanavas.

XML
<Control:MwiWindow x:Name="gMwiWindow" 
                      Width="{Binding ElementName=MyCanvas, Path=ActualWidth}" 
                      Height="{Binding ElementName=MyCanvas, Path=ActualHeight}"
                      Background="Gray" >
    <Control:MwiWindow.Children>
    <Control:MwiChild Background="DarkGray" BorderBrush="Black"
                         Icon="pack://application:,,,/Resources/app.ico" 
                         Width="300" Height="300" Title="Hello World 1" 
                         Canvas.Left="0" Canvas.Top="0" >
        <Grid Background="Blue" >
            <TextBox Text="{Binding DisplayTitle}" Width="100" Background="Yellow"
                         Height="24" IsEnabled="True" />
        </Grid>

    </Control:MwiChild>
    <Control:MwiChild Background="Green" BorderBrush="Green"
                         Icon="pack://application:,,,/Resources/app.ico"  
                         Width="300" Height="300" Title="Hello World 2" 
                         Canvas.Left="200" Canvas.Top="200" >
        <con:WPFSQLGrid />
    </Control:MwiChild>            
</Control:MwiWindow.Children>

Issues/Features

  • The arrangement of child windows is not maintained. Once you arrange the windows, if you then resize the MwiWindow the children are not similarly resized or rearranged. Also, under certain arrangements, it may be possible to temporarily resize a child too small, despite the minimum size restrictions. In situations like this, it would be nice if the arrangement applied an overlapping offset in order to maintain the proper size restrictions.

  • Currently the application doesn't automatically close any detached windows when it is closed.

  • When adding a child window in XAML, the MwiChild and its children cannot contain Name properties. This makes it somewhat difficult to add bindings, but there are plenty of workarounds to be found.

  • I would like to add a "new child template" that is used to generate the content of a child when added dynamically to the MwiWindow. This way you could create, for example, a grid control that is applied to all new child windows by wiring up a template into the MwiWindow (perhaps through a NewItemContentTemplate property).

  • I am unhappy with the DisplayTitle code and would like to see some improvement there to get the result looking more like windows.

  • A new method could be added in the MwiWindow class to arrange child windows using side-by-side comparison. The method would take two children and arrange them so that they were horizontally tiled, hiding any other child windows. This would most likely be accompanied by a dialog that allowed a user to select children to compare.

  • New properties to manage the visibility of the minimize, maximize and close buttons on MwiChild and AttachableWindow objects.

Updates

Version A (January 14th, 2008)

I finally got around to revisiting the child and detached window display titles. After a little trial and error, I came up with a simple solution to the problem. TextBlock has two properties, TextWrapping and TextTrimming, which are used to determine the look of text when the control is smaller than its content. I didn't want the title text to wrap, so I chose NoWrap for TextWrapping. I wanted the title to look and feel like a standard window title when it was resized, so I set TextTrimming to CharacterEllipsis. This, however, only provided me with half the solution. The TextBlock was not resizing within the confines of a StackPanel. So, I replaced the StackPanel with a Grid.

XML
<Grid x:Name="PanelTitleIcon" DockPanel.Dock="Left" 
          local:Controls.MwiChild.IsDraggable="true" >
  <Grid.ColumnDefinitions>
    <ColumnDefinition Width="Auto" />
    <ColumnDefinition Width="*" />
  </Grid.ColumnDefinitions>
  <!--StackPanel x:Name="PanelTitleIcon" Orientation="Horizontal" 
             DockPanel.Dock="Left" local:Controls.MwiChild.IsDraggable="true" -->
  <Image x:Name="ImageIcon" Source="{Binding Icon}" Width="16" Height="16" 
             Grid.Column="0" Stretch="Uniform" local:Controls.MwiChild.IsDraggable="true"
             Margin="1,0,1,0" />
  <TextBlock x:Name="TextBlockTitle" Text="{Binding Title}" 
             Foreground="White" Grid.Column="1" 
             TextWrapping="NoWrap" TextTrimming="CharacterEllipsis" 
             HorizontalAlignment="Left"   
             VerticalAlignment="Center" local:Controls.MwiChild.IsDraggable="true" />
  <!--/StackPanel-->
</Grid>

History

Initial Version (December 2007)

Version A (January 14th, 2008) - Fixed a problem with the background color that occurred when a child did not contain content and was then detached. Revisited the DisplayTitle issues and developed a much more simple solution.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Unknown
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionTab navigation in the MDIChild Pin
DaniloCodelines20-Sep-12 11:31
DaniloCodelines20-Sep-12 11:31 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.