Skip to main content

...

....

C# WPF Tutorial - Increasing a Window's Border [Beginner]


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. 

Source Files:

Comments

Popular posts from this blog

C# Snippet - Shuffling a Dictionary [Beginner]

Randomizing something can be a daunting task, especially with all the algorithms out there. However, sometimes you just need to shuffle things up, in a simple, yet effective manner. Today we are going to take a quick look at an easy and simple way to randomize a dictionary, which is most likely something that you may be using in a complex application. The tricky thing about ordering dictionaries is that...well they are not ordered to begin with. Typically they are a chaotic collection of key/value pairs. There is no first element or last element, just elements. This is why it is a little tricky to randomize them. Before we get started, we need to build a quick dictionary. For this tutorial, we will be doing an extremely simple string/int dictionary, but rest assured the steps we take can be used for any kind of dictionary you can come up with, no matter what object types you use. Dictionary < String , int > origin = new Dictionary < string , int >();

C# WPF Printing Part 2 - Pagination [Intermediate]

About two weeks ago, we had a tutorial here at SOTC on the basics of printing in WPF . It covered the standard stuff, like popping the print dialog, and what you needed to do to print visuals (both created in XAML and on the fly). But really, that's barely scratching the surface - any decent printing system in pretty much any application needs to be able to do a lot more than that. So today, we are going to take one more baby step forward into the world of printing - we are going to take a look at pagination. The main class that we will need to do pagination is the DocumentPaginator . I mentioned this class very briefly in the previous tutorial, but only in the context of the printing methods on PrintDialog , PrintVisual (which we focused on last time) and PrintDocument (which we will be focusing on today). This PrintDocument function takes a DocumentPaginator to print - and this is why we need to create one. Unfortunately, making a DocumentPaginator is not as easy as

C# WPF Tutorial - Implementing IScrollInfo [Advanced]

The ScrollViewer in WPF is pretty handy (and quite flexible) - especially when compared to what you had to work with in WinForms ( ScrollableControl ). 98% of the time, I can make the ScrollViewer do what I need it to for the given situation. Those other 2 percent, though, can get kind of hairy. Fortunately, WPF provides the IScrollInfo interface - which is what we will be talking about today. So what is IScrollInfo ? Well, it is a way to take over the logic behind scrolling, while still maintaining the look and feel of the standard ScrollViewer . Now, first off, why in the world would we want to do that? To answer that question, I'm going to take a an example from a tutorial that is over a year old now - Creating a Custom Panel Control . In that tutorial, we created our own custom WPF panel (that animated!). One of the issues with that panel though (and the WPF WrapPanel in general) is that you have to disable the horizontal scrollbar if you put the panel in a ScrollV