Custom Windows in WPF

In my spare time I have been working on a photoviewer application. The main reason for spending time on this project was to check out the functionality offered in Composite WPF and WPF in general. This is sort of a way for me to discover what is missing in Composite WPF. But the main reason for having this application is because I needed some way to manage my photos. There are other applications that might do a better job, but the idea that you have reached your goal of creating a cool WPF application made it well worth the effort. And a geek has to have something do during his vacation right?

In this article I will showcase one feature of the photoviewer application, which is the custom window chrome I have been adding the last few days. It is far from finished and the look of the application needs some tweaks to really make it stand out. However it is a great example on how you can modify WPF applications in such a way that even the windows look cooler.

To give you an idea of how it looks when you replace the window chrome I have included a picture of the photoviewer application below:

Replacing the windows chrome

The first step in creating a custom window chrome is removing the windows chrome. This is done by setting the border style of the window to none. After you have done this you end up with just the contents of the window. The next step is to define a style and controltemplate for the window, which will define the overall look and feel of the window. As you can see in the screenshot I made my application to look purple/blueish with round corners.

One of the key parts of replacing the window chrome is to not only remove the standard window border, but also emulate every single part of the original window chrome provided by windows. For this to work you will need to subclass the Window class from WPF and implement some custom logic.

Emulating the window chrome buttons

Since you can no longer use the minimize, maximize and close button provided by the default win32 implementation of the Window you will need to implement this yourself. For this I have added the buttons to my control template and named them. Naming the buttons with x_Name="…" is important because otherwise it's not possible to look them up in the codebehind file. You can look the implementation of the chrome buttons up in the source of the photoviewer application. For now I will only show how I have bound the click event of the close button.

In the OnApplyTemplate override I added the following piece of code:

 

public override void OnApplyTemplate()
{
base.OnApplyTemplate();

Button minimizeButton = (Button)GetTemplateChild(MinimizeButtonPart);
Button maximizeButton = (Button)GetTemplateChild(MaximizeButtonPart);
Button closeButton = (Button)GetTemplateChild(CloseButtonPart);

Control titlebar = (Control)GetTemplateChild(TitleBarPart);

titlebar.MouseDown += OnTitleBarMouseDown;
titlebar.MouseDoubleClick += OnTitleBarDoubleClick;

closeButton.Click += OnCloseButtonClick;
minimizeButton.Click += OnMinimizeClick;
maximizeButton.Click += OnMaximizeClick;

AttachResizeRegions();
}

 

If the user now clicks on the close button, the window will close, piece of cake. The rest of the buttons is just as simple.

Dragging the window

A normal window can be moved around when you 'grab' it at the titlebar and drag it around. To emulate this behavior I have added a Control to the titlebar region of my window and hooked up an eventhandler for the MouseDown event. The implementation of this event is again very simple:

this.DragMove();

Now the user can move the window around by just grabbing it at the titlebar. I choose to make it possible to move the window around by grabbing it anywhere in the top region over the full width of the window, but you can of course make it smaller if you like. It's just a matter of defining a smaller control.

General tips and tricks

As simple as the previous two operations may seem, I still had some minor issues that were preventing my window from working they way I expected it to work. Here are some general tips:

  • Always set IsTabStop and IsFocusable to false for all parts that are derived from control in the control template of the window.
  • Specify at least Background="Transparent" if you want a rectangle to be clickable to the user. Otherwise the user clicks 'through' the rectangle.
  • Set AllowTransparency="True" on the window if you are going to create a window with round corners. Otherwise the corners will contain black triangles.

Resizing the window

At this point the window is displaying correctly and the window state can be modified. However it is not resizable, as again windows does no longer provide that functionality. To get this functionality back I needed to write my own resize logic.

The trick to get resizing of the window working is to abuse the grid layout panel. The basic layout panel of my window is a grid. This allows me to pull some rather cool tricks. The first thing I did was add the normal layout controls to the grid so that the window looks like the screenshot at the beginning of this article. Next I created a few rectangles that I layed out at the edges and corners of the window. The rectangles are transparent, so you won't notice them. However as you move the mouse over them the cursor will change to a resize cursor. This is actually not really special. I just specified the cursor for the rectangles in XAML.

Each of the resize rectangles gets bound in the codebehind of the window to a set of eventhandlers. The first to be bound is the mousedown event. When the user presses the mouse I want to set the _isResizing flag to true and capture the mouse movement, so that the user can drag and I still get the mouse events on the rectangle even if the mouse is actually leaving the rectangle. The code for this looks like this:

 

void OnResizeRectMouseDown(object sender, MouseButtonEventArgs e)
{
if (e.LeftButton == MouseButtonState.Pressed)
{
isResizing = true;

Rectangle senderRectangle = sender as Rectangle;
senderRectangle.CaptureMouse();
}
}

 

The second event to bind is the mousemove event. When the user moves the mouse when the _isResizing flag is active, the window will need to be resized. The logic for this is kind of complicated, but the following snippet should suffice.

 


void OnResizeRectMouseMove(object sender, MouseEventArgs e)
{
Rectangle senderRectangle = (Rectangle)sender;

if (!isResizing) return;
if (senderRectangle == null) return;

switch (senderRectangle.Name)
{
case ResizeLeftPart:
ResizeFromLeft(senderRectangle, e);
break;
case ResizeRightPart:
ResizeFromRight(senderRectangle, e);
break;
case ResizeBottomPart:
ResizeFromBottom(senderRectangle, e);
break;
case ResizeTopPart:
ResizeFromTop(senderRectangle, e);
break;
case ResizeBottomRightPart:
ResizeFromBottomRight(senderRectangle, e);
break;
}
}

 

 

void ResizeFromTop(Rectangle sender, MouseEventArgs e)
{
Point mousePosition = e.GetPosition(this);

sender.CaptureMouse();

double newHeight = Height - mousePosition.Y;

if (newHeight <= MaxHeight && newHeight > MinHeight)
{
Point absoluteMousePosition = PointToScreen(mousePosition);

Height = newHeight;
Top = absoluteMousePosition.Y;
}
}

 

The idea behind the mouse capture earlier is that I can accurately determine where the mouse is in relation to the rectangle. The effect of which is that I can actually see if the user is making the window region larger or smaller depending on the fact that I have either mouse coordinates that are below zero or are higher than the with and height of the rectangle.

The last event to bind is the mouseup event. When the user releases the mouse the window will release the mouse capture from the rectangle, so we no longer receive the mouse events on the rectangle. Instead the mouse will now act as normal. Also in this eventhandler I will reset the _isResizing flag so that if the mouse moves of the resize rectangle it will no longer try to resize the window. The code for the mouse up event looks like this:

 

void OnResizeRectMouseUp(object sender, MouseButtonEventArgs e)
{
Rectangle senderRectangle = (Rectangle)sender;
senderRectangle.ReleaseMouseCapture();

isResizing = false;
}

 

Limiting the resize actions

To keep the normal functionality of WPF working for all application windows I had to apply some limitations to the resize logic. The following limitations were applied either through triggers or in custom code:

  • When resizing the window, the size of the window is limited to the minimum and maximum size specified in XAML. Otherwise the whole application will look really weird and that's clearly not what I want.
  • When ResizeMode=None is specified the resize rectangles are no longer "visible" so you will not get the resize cursor and the eventhandlers will not activate. Actually, the rectangles aren't there anymore. This may sound complex, but it isn't. I specified a trigger in my control template that simply sets the Visibility property of the rectangles to collapsed.

Sources

For those of you who would like to take a peek at the photoviewer application and what I did exactly in the sourcecode, check out the project at codeplex: http://www.codeplex.com/photoviewer The code can be found in the Controls project and is part of the theming branch of the project. The trunk version is there because I also wanted to keep a stable theme-free version of the application. If you want the application to look 'right' download the sources of the trunk version and compile that. Better yet, download the beta release and you don't even have to compile 🙂