WPF Wizards part 2 - Glass

See also WPF Wizards - Part 1

The big difference between the wizard from part 1 and how a system wizard looks, for me, is that the glass header doesn't extend down.   Fortunately there are lots of tutorials that can help with the mechanics of how it works so I'm going to simply present my own solution, which demonstrates some useful WPF tactics.

Defining a Style

Since you will obviously want to reuse this code, I’m going to define a style, we start with something pretty simple.

<Style x:Key="Win7Wizard" TargetType="{x:Type NavigationWindow}">
    <Setter Property="MinWidth" Value="400"/>
</Style>

 

I'm going to use Attached Properties to avoid any need to derive a GlassWindow class from Window. Since we are using a NavigationWindow  it really wouldn't make sense. There’s two parts you normally set, first a bool to turn it on and then you define how far it extends into the normal area. We simply need to extend down from the top, so we use this

<Setter Property="glass:GlassEffect.IsEnabled" Value="True" />
<Setter Property="glass:GlassEffect.Thickness">
    <Setter.Value>
        <Thickness Top="35"/>
    </Setter.Value>
</Setter>        

 

Implementing the properties

We can implement the attached properties just like this, notice the extra handler definition for when IsEnabled is modified;

public static readonly DependencyProperty IsEnabledProperty =
       DependencyProperty.RegisterAttached("IsEnabled",
       typeof(Boolean), typeof(GlassEffect),
       new FrameworkPropertyMetadata(OnIsEnabledChanged));

 

The actual backing values are applied in the usual manner for Dependency Properties

[DebuggerStepThrough]
public static void SetIsEnabled(DependencyObject element, Boolean value)
{
    element.SetValue(IsEnabledProperty, value);
}

[DebuggerStepThrough]
public static Boolean GetIsEnabled(DependencyObject element)
{
    return (Boolean)element.GetValue(IsEnabledProperty);
}

And not forgetting the extra handler for IsEnabled which prevents Windows 7 losing the glass.

[DebuggerStepThrough]
      public static void OnIsEnabledChanged(DependencyObject obj, 
                                                DependencyPropertyChangedEventArgs args)
      {
          if ((bool)args.NewValue == true)
          {
              try
              {
                  Window wnd = (Window)obj;
                  wnd.Activated += new EventHandler(wnd_Activated);
                  wnd.Loaded += new RoutedEventHandler(wnd_Loaded);
                  wnd.Deactivated += new EventHandler(wnd_Deactivated);
              }
              catch (Exception)
              {
                  //Oh well, we tried
              }
          }
          else
          {
              try
              {
                  Window wnd = (Window)obj;
                  wnd.Activated -= new EventHandler(wnd_Activated);
                  wnd.Loaded -= new RoutedEventHandler(wnd_Loaded);
                  wnd.Deactivated -= new EventHandler(wnd_Deactivated);
              }
              catch (Exception)
              {                    
              }
          }
      }
 

Those handlers are all very similar but I’ve chosen to implement separately for simplicities sake

[DebuggerStepThrough]
  static void wnd_Deactivated(object sender, EventArgs e)
  {
      ApplyGlass((Window)sender);
  }

  [DebuggerStepThrough]
  static void wnd_Activated(object sender, EventArgs e)
  {            
      ApplyGlass((Window)sender);
  }

  [DebuggerStepThrough]
  static void wnd_Loaded(object sender, RoutedEventArgs e)
  {            
      ApplyGlass((Window)sender);
  }

 

Making glass

The code to actually turn on the glass is slightly more involved. Although this is based on lots of other web based examples, note the use of DPI calculation so it still works correctly as soon as you move to 125% DPI Windows installation. I’ve also got another property which defines the colour that should be painted in the glass sections to correctly match the window when we are on XP or if glass isn’t turned on in Vista/7 so that we handle the switch between active and inactive windows.

private static void ApplyGlass(Window window)
   {
       try
       {
           // Obtain the window handle for WPF application
           IntPtr mainWindowPtr = new WindowInteropHelper(window).Handle;
           HwndSource mainWindowSrc = HwndSource.FromHwnd(mainWindowPtr);

           // Get System Dpi
           System.Drawing.Graphics desktop = System.Drawing.Graphics.FromHwnd(mainWindowPtr);
           float DesktopDpiX = desktop.DpiX;
           float DesktopDpiY = desktop.DpiY;

           // Set Margins
           GlassEffect.MARGINS margins = new GlassEffect.MARGINS();
           Thickness thickness = GetThickness(window);//new Thickness();

           // Extend glass frame into client area
           // Note that the default desktop Dpi is 96dpi. The  margins are
           // adjusted for the system Dpi.
           margins.cxLeftWidth = Convert.ToInt32((thickness.Left*DesktopDpiX/96)+0.5);
           margins.cxRightWidth = Convert.ToInt32((thickness.Right*DesktopDpiX/96)+0.5);
           margins.cyTopHeight = Convert.ToInt32((thickness.Top*DesktopDpiX/96)+0.5);
           margins.cyBottomHeight = Convert.ToInt32((thickness.Bottom*DesktopDpiX/96)+0.5);

           int hr = GlassEffect.DwmExtendFrameIntoClientArea(
                        mainWindowSrc.Handle, 
                        ref margins);
           if (hr < 0)
           {
               //DwmExtendFrameIntoClientArea Failed      
               if(window.IsActive)
                   SetGlassBackground(window, SystemColors.GradientActiveCaptionBrush);
               else
                   SetGlassBackground(window, SystemColors.GradientInactiveCaptionBrush);
           }
           else
           {
               mainWindowSrc.CompositionTarget.BackgroundColor = Color.FromArgb(0, 0, 0, 0);
               SetGlassBackground(window, Brushes.Transparent);
           }


       }
       // If not Vista, paint background as control.
       catch (DllNotFoundException)
       {
           SetGlassBackground(window, SystemColors.ControlBrush);
       }
   }

The only downside of this code is that we rely on the System.Drawing dll which isn’t referenced by default for WPF. You simply have to remember to add the reference or else you will get.

The type or namespace name 'Drawing' does not exist in the 
namespace 'System' (are you missing an assembly reference?)

Putting it all together

The final step is simply to turn on the glass by applying the style to the window, first since I’ve defined the style in a ResourceDictionary, you can add that once to your App.xaml by adding the highlighted bit below.

<Application x:Class="BlogWPFWizard.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             StartupUri="MainWindow.xaml">
    <Application.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <ResourceDictionary Source="Style\Win7wizard.xaml" />
            </ResourceDictionary.MergedDictionaries>
        </ResourceDictionary>
    </Application.Resources>
</Application>

And then we apply the style to the Window

<NavigationWindow x:Class="BlogWPFWizard.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525"
                  Style="{StaticResource Win7Wizard}"
        >
  
</NavigationWindow>

 

And there we have the glass, the only problem is you can’t see it, because the Navigation window is in the way.  If you look really closely you can see the missing black line on either side of the Navigation header.

image

Building the glass layout

One quick and dirty way round this is to start to quickly define an empty template for the Navigation Window. I’m going to define a section for the top, which we will bind the BackGround colour as generated above, and also define a ContentPresenter so we can inject the pages into.

<Setter Property="Template">
   <Setter.Value>
      <ControlTemplate>

         <DockPanel x:Name="mainDock"  LastChildFill="True" >
         <!-- The border is used to compute the rendered height with margins.
             topBar contents will be displayed on the extended glass frame.—>
             <Border DockPanel.Dock="Top" 
                Background="{TemplateBinding glass:GlassEffect.GlassBackground}" 
                x:Name="glassPanel" Height="36">
                <!-- Wizard controls will go here-->
             </Border>
                        
             <Border 
                BorderThickness="{TemplateBinding Border.BorderThickness}" 
                BorderBrush="{TemplateBinding Border.BorderBrush}" 
                Background="{TemplateBinding Panel.Background}">                            
                <!-- Content area ie where Pages go -->
                <AdornerDecorator >                                    
                   <ContentPresenter                                 
                      Content="{TemplateBinding ContentControl.Content}" 
                      ContentTemplate="{TemplateBinding ContentControl.ContentTemplate}" 
                      ContentStringFormat="{TemplateBinding 
                                               ContentControl.ContentStringFormat}"
                      Name="PART_NavWinCP" ClipToBounds="True" />                                                                          
                </AdornerDecorator>                                
             </Border>
         </DockPanel>

      </ControlTemplate>
   </Setter.Value>
</Setter>

Running this proves our glass integration has worked, but also removes the chrome that makes the wizard work.image

It does leave a perfect starting point for the next post.

Comments (1) -

steve
steve
1/16/2012 11:57:47 AM #

thanks for this

Comments are closed