WPF Tutorial – Increasing a Window’s Border

Extending the window’s frame is a design trend that is becoming popular in modern applications – especially browsers. Every major browser today (Chrome, Firefox, Internet Explorer, and Opera) uses this technique to increase the visual quality of their applications.

Today we’re going to see how to duplicate this look in WPF. The approach we’re going to take relies on the OS supporting Aero, which means only Vista and Windows 7 are supported. For Windows XP, you’re going to have to find another solution.

Perhaps in future versions of WPF Microsoft will simply add a property that can be used to set the window’s frame, but unfortunately that doesn’t exist today. We’re going to have to get our hands dirty inside the Windows API to get this done.

The example I’m going to build today gets its inspiration from modern browsers. I’m going to build an application that puts a search box up in the window’s frame.

Let’s start with a basic window definition in XAML. There’s very little we have to do in XAML to support extending the frame, so this is very basic markup for creating the application pictured above.

<!– Normal window.  The only required addition is setting
    the Background to transparent. –>
<Window x:Class="WindowBorder.MainWindow"
       xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
       xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
       Title="MainWindow"
       Height="300"
       Width="400"
       Background="Transparent">
  <Grid>
    <Grid.RowDefinitions>
      <!– 30 is how much margin was added to the window frame. –>
      <RowDefinition Height="30" />
      <RowDefinition Height="*" />
    </Grid.RowDefinitions>
   
    <!– Stack panel to layout the search box and the Go button. –>
    <StackPanel Orientation="Horizontal"
               HorizontalAlignment="Right"
               VerticalAlignment="Center">
     
      <!– Search box. –>
      <TextBox Width="150"
              VerticalAlignment="Center"
              Text="Search" />
     
      <!– Go button. –>
      <Button Content="Go"
             VerticalAlignment="Center"
             Margin="5,0,0,0" />
    </StackPanel>
   
    <!– This is where the rest of the content would go. –>
    <Grid Background="White"
         Grid.Row="1">

    </Grid>
  </Grid>
</Window>

The only important pieces in this code are the Window’s background, which must be set to transparent, and the magic row height of 30. I’ve extended the frame of this window by 30, so this row will hold items that are on top of the window’s frame – like the search box and Go button.

All right, now on to some meat. The first thing we need to do is provide access to the DwmExtendFrameIntoClientArea API function. This function requires a MARGINS structure, which we will also have to define.

using System;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Interop;
using System.Windows.Media;
/// <summary>
/// Structure to hold the new window frame.
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public struct MARGINS
{
  public int cxLeftWidth;
  public int cxRightWidth;
  public int cxTopHeight;
  public int cxBottomHeight;
}

/// <summary>
/// Extends the window’s frame into the client area.
/// </summary>
/// <param name="hWnd">Handle of the window to extend.</param>
/// <param name="pMarInset">Amount to extend.</param>
/// <returns>0 on success or error code.</returns>
[DllImport("dwmapi.dll")]
public static extern int DwmExtendFrameIntoClientArea(
  IntPtr hWnd, ref MARGINS pMarInset);

The structure and the function definition come straight from MSDN’s documentation. Nothing fancy here.

Everything required to adjust the frame happens in the window’s Loaded event. We have to wait until the window is loaded because the handle isn’t valid until that happens.

void OnLoaded(object sender, RoutedEventArgs e)
{
  // Get the handle for this window.
  IntPtr windowHandle = new WindowInteropHelper(this).Handle;

  // Get the Win32 window that hosts the WPF content.
  HwndSource window = HwndSource.FromHwnd(windowHandle);

  // Get the visual manager and set its background to transparent.
  window.CompositionTarget.BackgroundColor = Colors.Transparent;

  // Set the desired margins.
  MARGINS margins = new MARGINS();
  margins.cxTopHeight = 30;

  // WPF is DPI independent.  Simply passing 30 into the
  // Windows API does not take into account differences in the
  // user’s DPI settings.  We must adjust the margins to
  // reflect different settings.
  margins = AdjustForDPISettings(margins, windowHandle);

  // Call into the windows API to extend the frame.
  // Only supported on OS versions with Aero (Vista, 7).
  // Throws an exception on non-supported operating systems.
  int result = DwmExtendFrameIntoClientArea(windowHandle, ref margins);
}

The Windows API controls windows using their handles, so the first thing we need to do is get this window’s handle. .NET provides a nice helper class for that called WindowInteropHelper. Next we have to get an HwndSource from that handle. These objects represent a low-level Win32 window that will host WPF content. The only thing we need that object for is to set the background to transparent. If you don’t, you’ll see a nasty black rectangle where the new frame is supposed to be. Next we create a MARGINS structure that holds how much we’d like to adjust our window frame – in this case I’m adding 30 to the top. Because WPF is resolution independent, we have to adjust our margin based on the current DPI setting of the computer. The very last thing is to simply call the API function to adjust the frame.

Let’s take a look at the AdjustForDPISettings function.

/// <summary>
/// Adjusts the margins based on the users DPI settings.
/// </summary>
/// <param name="input">The unadjusted margin.</param>
/// <param name="hWnd">The window handle.</param>
/// <returns>Adjusted margins.</returns>
private MARGINS AdjustForDPISettings(MARGINS input, IntPtr hWnd)
{
  MARGINS adjusted = new MARGINS();

  // Gets the graphic object from the window handle
  // so we can get the current DPI settings.
  var graphics = System.Drawing.Graphics.FromHwnd(hWnd);

  // The default DPI is 96.  This creates a ratio that
  // will be applied to the incoming values to adjust them
  // based on whatever the current DPI setting is.
  float dpiRatioX = graphics.DpiX / 96;
  float dpiRatioY = graphics.DpiY / 96;

  // Adjust settings.
  adjusted.cxLeftWidth = (int)(input.cxLeftWidth * dpiRatioX);
  adjusted.cxRightWidth = (int)(input.cxRightWidth * dpiRatioX);
  adjusted.cxTopHeight = (int)(input.cxTopHeight * dpiRatioY);
  adjusted.cxBottomHeight = (int)(input.cxBottomHeight * dpiRatioY);

  return adjusted;
}

All this is doing is multiplying the original margin by an adjustment factor. The adjustment factor is simply the current setting divided by the default value (96).

That’s actually all there is to it. If you run the application now, you’d see a nice large top application frame with a search box and button overlaying it. The code isn’t all that long, so here it is again in one big chunk.

using System;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Interop;
using System.Windows.Media;

namespace WindowBorder
{
  /// <summary>
  /// Interaction logic for MainWindow.xaml
  /// </summary>
  public partial class MainWindow : Window
  {
    public MainWindow()
    {
      InitializeComponent();

      // Has to be done in the Loaded event because the window
      // handle is not valid until the window has loaded.
      this.Loaded += OnLoaded;
    }

    void OnLoaded(object sender, RoutedEventArgs e)
    {
      // Get the handle for this window.
      IntPtr windowHandle = new WindowInteropHelper(this).Handle;

      // Get the Win32 window that hosts the WPF content.
      HwndSource window = HwndSource.FromHwnd(windowHandle);

      // Get the visual manager and set its background to transparent.
      window.CompositionTarget.BackgroundColor = Colors.Transparent;

      // Set the desired margins.
      MARGINS margins = new MARGINS();
      margins.cxTopHeight = 30;

      // WPF is DPI independent.  Simply passing 30 into the
      // Windows API does not take into account differences in the
      // user’s DPI settings.  We must adjust the margins to
      // reflect different settings.
      margins = AdjustForDPISettings(margins, windowHandle);

      // Call into the windows API to extend the frame.
      // Only supported on OS versions with Aero (Vista, 7).
      // Throws an exception on non-supported operating systems.
      int result = DwmExtendFrameIntoClientArea(windowHandle, ref margins);
    }

    /// <summary>
    /// Adjusts the margins based on the users DPI settings.
    /// </summary>
    /// <param name="input">The unadjusted margin.</param>
    /// <param name="hWnd">The window handle.</param>
    /// <returns>Adjusted margins.</returns>
    private MARGINS AdjustForDPISettings(MARGINS input, IntPtr hWnd)
    {
      MARGINS adjusted = new MARGINS();

      // Gets the graphic object from the window handle
      // so we can get the current DPI settings.
      var graphics = System.Drawing.Graphics.FromHwnd(hWnd);

      // The default DPI is 96.  This creates a ratio that
      // will be applied to the incoming values to adjust them
      // based on whatever the current DPI setting is.
      float dpiRatioX = graphics.DpiX / 96;
      float dpiRatioY = graphics.DpiY / 96;

      // Adjust settings.
      adjusted.cxLeftWidth = (int)(input.cxLeftWidth * dpiRatioX);
      adjusted.cxRightWidth = (int)(input.cxRightWidth * dpiRatioX);
      adjusted.cxTopHeight = (int)(input.cxTopHeight * dpiRatioY);
      adjusted.cxBottomHeight = (int)(input.cxBottomHeight * dpiRatioY);

      return adjusted;
    }

    /// <summary>
    /// Structure to hold the new window frame.
    /// </summary>
    [StructLayout(LayoutKind.Sequential)]
    public struct MARGINS
    {
      public int cxLeftWidth;
      public int cxRightWidth;
      public int cxTopHeight;
      public int cxBottomHeight;
    }

    /// <summary>
    /// Extends the window’s frame into the client area.
    /// </summary>
    /// <param name="hWnd">Handle of the window to extend.</param>
    /// <param name="pMarInset">Amount to extend.</param>
    /// <returns>0 on success or error code.</returns>
    [DllImport("dwmapi.dll")]
    public static extern int DwmExtendFrameIntoClientArea(
      IntPtr hWnd, ref MARGINS pMarInset);
  }
}

Hopefully Microsoft will make doing this a little easier in future versions of WPF – especially since the design has become so popular. For now, however, we get to use the long way. If you’ve got any questions or comments, feel free to leave them below.

Leave a Reply

Your email address will not be published. Required fields are marked *