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
Post a Comment