WPF Borderless Window Controls

WPFControlBox

The first challenge I tackled when writing Metro Stack was creating that Microsoft Zune player feel with no border while still supporting a draggable and resizable window. I have created an isolated project to demonstrate how you can do this.

Creating a borderless window that supports resizing is simple. Here is the code:

<Window
    ...
    ResizeMode="CanResizeWithGrip"
    WindowStyle="None"
    AllowsTransparency="True">
    ...
</Window>

Just three properties can get you a borderless window like you would expect. WindowStyle will remove the standard control box. AllowsTransparency will remove the border. ResizeMode will give you a drag option in the bottom right of the window.

When I initially thought of creating a custom control box for the minimize, maximize, and close buttons I started thinking how annoying it would be to find or create images for each function and possibly for each state of the button. I got less interested in making them the more I thought about it. Then I heard about people doing this using Wingdings and I thought that was a great idea! This would also allow me to handle different effects for MouseOver and Pressed events easier. Getting this going was rather simple. I just created a TextBlock, changed the font to Wingdings and entered the appropriate character for the symbol I am looking for. For example, the close button would look like this:

<TextBlock
    x:Name="CloseButton"
    Text="r"
    FontFamily="Webdings"
    Foreground="Gray"
    Margin="5,0,0,0"
    HorizontalAlignment="Right"
    VerticalAlignment="Top"
    MouseLeftButtonUp="CloseButtonMouseLeftButtonUp" />

I decided to go with traditional code behind because I find it appropriate that these controls are the concern of the view.

Accomplishing the window drag was not too bad either. For my application, I wanted just a portion of the window to be dragable. To do this I created a new grid to act as the window drag handle.

<Grid x:Name="LayoutRoot"
    Background="White">
    <Grid 
        x:Name="HeaderGrid" 
        Height="50" 
        VerticalAlignment="Top">
        <Grid 
            x:Name="DragableAreaGrid"
            Background="White"
            MouseDown="DragableGridMouseDown"/>
        ...
    </Grid>
</Grid>

There were a few tricks I had to play to get the dragable area working with the control box. Notice that the DragableAreaGrid has a background of white, which is really just on top of another grid that is white. This is done so that the user actually has some surface area of the DragableAreaGrid to grab on to. If this were a transparent background you would never be able to invoke the MouseDown event of the grid.

Here is the full XAML code for this sample:

<Window
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d"
    x:Class="Metro.CustomControlBox"
    x:Name="Window"
    ResizeMode="CanResizeWithGrip"
    WindowStyle="None"
    AllowsTransparency="True"
    Width="640"
    Height="480">
    <Grid
        x:Name="LayoutRoot"
        Background="White">
        <Grid 
            x:Name="HeaderGrid" 
            Height="50" 
            VerticalAlignment="Top">
            <Grid
                x:Name="DragableArea"
                Background="White"
                MouseDown="DragableGridMouseDown"/>
            <StackPanel
                Orientation="Horizontal"
                Margin="0,5,5,0"
                HorizontalAlignment="Right"
                VerticalAlignment="Top"
                Background="White">
                <TextBlock 
                    x:Name="ChangeViewButton"
                    Text="2"
                    FontFamily="Webdings"
                    Foreground="Gray"
                    Margin="0"
                    VerticalAlignment="Top"
                    HorizontalAlignment="Right"
                    MouseLeftButtonUp="ChangeViewButtonMouseLeftButtonUp" />
                <TextBlock
                    x:Name="MinimizeButton"
                    Text="0"
                    FontFamily="Webdings"
                    Foreground="Gray"
                    Margin="5,0,0,0"
                    HorizontalAlignment="Right"
                    VerticalAlignment="Top"
                    MouseLeftButtonUp="MinimizeButtonMouseLeftButtonUp" />
                <TextBlock
                    x:Name="MaximizeButton"
                    Text="1"
                    FontFamily="Webdings"
                    Foreground="Gray"
                    Margin="5,0,0,0"
                    HorizontalAlignment="Right"
                    VerticalAlignment="Top"
                    MouseLeftButtonUp="MaximizeButtonMouseLeftButtonUp" />
                <TextBlock
                    x:Name="CloseButton"
                    Text="r"
                    FontFamily="Webdings"
                    Foreground="Gray"
                    Margin="5,0,0,0"
                    HorizontalAlignment="Right"
                    VerticalAlignment="Top"
                    MouseLeftButtonUp="CloseButtonMouseLeftButtonUp" />
            </StackPanel>
        </Grid>
    </Grid>
</Window>

Here is the code behind for this XAML window:

using System.Windows;
using System.Windows.Input;

namespace Metro
{
    public partial class CustomControlBoxWindow : Window
    {
        public CustomControlBoxWindow()
        {
            InitializeComponent();
        }

        private void CloseButtonMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
        {
            Close();            
        }

        private void MaximizeButtonMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
        {
            WindowState = WindowState.Maximized;
        }

        private void ChangeViewButtonMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
        {
            WindowState = WindowState.Normal;           
        }

        private void MinimizeButtonMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
        {
            WindowState = WindowState.Minimized;            
        }

        private void DragableGridMouseDown(object sender, MouseButtonEventArgs e)
        {
            if (e.LeftButton == MouseButtonState.Pressed)
                DragMove();         
        }
    }
}

The code behind is just slap happy code, but really there is not much more to it. Notice the condition in the DragableGridMouseDown event. You will get an exception if you try and call DragMove() if the user was dragging with any other button other than the primary button. InvalidOperationException: Can only call DragMove when primary mouse button is down.

DragMoveException

Checking for the LeftButton is probably not the best idea but it demonstrates something to watch out for. I doubt this code would work with a left handed mouse as the input device may consider the RightButton to be the primary button.

Other tricky parts to watch out for is how your DragableAreaGrid interacts with your new control box. If the controls are within the DragableAreaGrid the grid will handle your mouse events for the controls. Because of this, I have added my controls side-by-side (rather than the controls in the dragable grid) with my DragableAreaGrid. You will also run into the same background problem with the control grid if your controls sit on top of your DragableAreaGrid. Unless you put a background color on your controls you will only get a click event on your controls if you hit it perfectly on the wingding character. Don't be sad though. There are tricks inside of tricks for that. If you truly do not want a color for your background but you need some surface area to be able to catch that mouse input you can set the alpha channel of the background to be 0% so you can't even see the background but it's there as a clickable space.