Introduction
By setting WindowStyle="None"
in WPF, you can completely customize a window, however, this removes all of the functionality that you would expect a window to have. Some of the lost features are:
- Maximizing to the working area of the screen (the useable space other than the taskbar)
- Remembering the window size and location when restoring down the window
- Window resizing by dragging the window edges
- Restoring down by dragging the window header
- Maximizing to the correct monitor in a multi-screen setup
For this article, I have created a few example windows which have blue or red borders to illustrate where the resizing elements are. The base example has a standard header bar and there are also two other examples available in the project, one which is a browser style window and another which has a left aligned navigation column.
Working Example
The gif below has been created from the code available in this article, it is styled on the Opera browser. I've chosen this example to highlight the potential use of what is normally the non-client area.
Credit
This code makes use of WpfScreenHelper by micdenny to find display size and boundary information without introducing dependencies on Windows Forms. It is available on GitHub
or on NuGet
with the package command:
Install-Package WpfScreenHelper -Version 0.3.0
Using the Code
- Extract the files within to your desired location
- Import the
ExampleBaseWindow
, Screen Finder and WindowStateHelper
into your project - Search ExampleBaseWindow.xaml for "Window resize behaviour" which will take you to the rectangles that have a blue or red "
Fill
", set these to "Transparent
" - Edit the rest of the "
MainGrid
" to fit the design that you wish to use
Understanding the Code
Other than the code in the window and WpfScreenHelper
, there are two static
classes I have created to regain the behaviour we would expect from a normal window.
1. Window Code
a) Resizing Effects
To start, we need to add the resizing effects to the window which requires HwndSource
to be included in the application and add our window to its presentation source. This is done in the SourceInitialized
event on the window. We also need an enum
with the values for each direction to be passed to the method which handles the resizing effect by using SendMessage
in Win32.
using System;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Input;
using System.Windows.Interop;
using System.Windows.Shapes;
using CustomWindowWpf.Classes;
namespace CustomWindowWpf.Windows
{
public partial class MainWindow : Window
{
private HwndSource _hwndSource;
public MainWindow()
{
InitializeComponent();
ButtonWindowStateNormal.Visibility = Visibility.Collapsed;
}
#region WindowResizing
private void Window_OnSourceInitialized(object sender, EventArgs e)
{
_hwndSource = (HwndSource)PresentationSource.FromVisual(this);
}
[DllImport("user32.dll", CharSet = CharSet.Auto)]
private static extern IntPtr SendMessage
(IntPtr hWnd, UInt32 msg, IntPtr wParam, IntPtr lParam);
private void ResizeWindow(ResizeDirection direction)
{
SendMessage(_hwndSource.Handle, 0x112, (IntPtr)(61440 + direction), IntPtr.Zero);
}
private enum ResizeDirection
{
Left = 1,
Right = 2,
Top = 3,
TopLeft = 4,
TopRight = 5,
Bottom = 6,
BottomLeft = 7,
BottomRight = 8,
}
In the example, each corner rectangle is marked in red with the line borders marked in blue, these have all engaged the PreviewMouseDown
event and MouseMove
events to handle cursor changes when the mouse moves into these regions. WindowStateHelper.IsMaximized
is our own boolean value, which will be discussed later in the article.
private void WindowResize_OnPreviewMouseDown(object sender, MouseButtonEventArgs e)
{
var rectangle = (Rectangle)sender;
if (rectangle == null) return;
if (WindowStateHelper.IsMaximized) return;
switch (rectangle.Name)
{
case "WindowResizeTop":
Cursor = Cursors.SizeNS;
ResizeWindow(ResizeDirection.Top);
break;
case "WindowResizeBottom":
Cursor = Cursors.SizeNS;
ResizeWindow(ResizeDirection.Bottom);
break;
case "WindowResizeLeft":
Cursor = Cursors.SizeWE;
ResizeWindow(ResizeDirection.Left);
break;
case "WindowResizeRight":
Cursor = Cursors.SizeWE;
ResizeWindow(ResizeDirection.Right);
break;
case "WindowResizeTopLeft":
Cursor = Cursors.SizeNWSE;
ResizeWindow(ResizeDirection.TopLeft);
break;
case "WindowResizeTopRight":
Cursor = Cursors.SizeNESW;
ResizeWindow(ResizeDirection.TopRight);
break;
case "WindowResizeBottomLeft":
Cursor = Cursors.SizeNESW;
ResizeWindow(ResizeDirection.BottomLeft);
break;
case "WindowResizeBottomRight":
Cursor = Cursors.SizeNWSE;
ResizeWindow(ResizeDirection.BottomRight);
break;
}
}
private void WindowResize_OnMouseMove(object sender, MouseEventArgs e)
{
var rectangle = (Rectangle)sender;
if (rectangle == null) return;
if (WindowStateHelper.IsMaximized) return;
switch (rectangle.Name)
{
case "WindowResizeTop":
Cursor = Cursors.SizeNS;
break;
case "WindowResizeBottom":
Cursor = Cursors.SizeNS;
break;
case "WindowResizeLeft":
Cursor = Cursors.SizeWE;
break;
case "WindowResizeRight":
Cursor = Cursors.SizeWE;
break;
case "WindowResizeTopLeft":
Cursor = Cursors.SizeNWSE;
break;
case "WindowResizeTopRight":
Cursor = Cursors.SizeNESW;
break;
case "WindowResizeBottomLeft":
Cursor = Cursors.SizeNESW;
break;
case "WindowResizeBottomRight":
Cursor = Cursors.SizeNWSE;
break;
}
}
private void Window_OnPreviewMouseMove(object sender, MouseEventArgs e)
{
if (e.LeftButton != MouseButtonState.Pressed)
Cursor = Cursors.Arrow;
}
b) Window Buttons: Minimize, Restore Down, Maximize and Close
After creating the buttons in XAML, we need to hook up the click events to these and create two methods for showing or hiding maximize window or restore down based on what the window state has changed to.
private void ShowRestoreDownButton()
{
ButtonMaximize.Visibility = Visibility.Collapsed;
ButtonWindowStateNormal.Visibility = Visibility.Visible;
}
private void ShowMaximumWindowButton()
{
ButtonWindowStateNormal.Visibility = Visibility.Collapsed;
ButtonMaximize.Visibility = Visibility.Visible;
}
private void ButtonClose_OnClick(object sender, RoutedEventArgs e)
{
Close();
}
private void ButtonMinimize_OnClick(object sender, RoutedEventArgs e)
{
WindowState = WindowState.Minimized;
}
private void ButtonRestoreDown_OnClick(object sender, RoutedEventArgs e)
{
ShowMaximumWindowButton();
WindowStateHelper.SetWindowSizeToNormal(this);
}
private void ButtonMaximize_OnClick(object sender, RoutedEventArgs e)
{
WindowState = WindowState.Maximized;
ShowRestoreDownButton();
}
2. WindowStateHelper
As the last two methods in the window focus heavily on WindowStateHelper
and ScreenFinder
, we will discuss these static
classes before showing those last two methods from the window.
The WindowStateHelper
stores the last known top, left, width and height of the window while it was in a "normal" state. There are also two more properties in this class:
IsMaximized
- We need to set our own property for maximized instead of WindowState.Maximized
because setting via WindowState
will make your window the size of the whole screen (over the task bar) instead of the working area (excluding the task bar). BlockStateChange
- We are updating the size and location of the normal window any time it is dragged around on the screen, so this prevents the event from updating the last known normal size to maximum when you click the maximize button.
using System.Windows;
using WpfScreenHelper;
namespace CustomWindowWpf.Classes
{
public static class WindowStateHelper
{
private static double Top { get; set; }
private static double Left { get; set; }
private static double Width { get; set; }
private static double Height { get; set; }
public static bool IsMaximized { get; private set; }
public static bool BlockStateChange { get; set; }
private static void SetWindowTop(Window window)
{
BlockStateChange = true;
window.Top = Top;
}
private static void SetWindowLeft(Window window)
{
BlockStateChange = true;
window.Left = Left;
}
private static void SetWindowWidth(Window window)
{
BlockStateChange = true;
window.Width = Width;
}
private static void SetWindowHeight(Window window)
{
BlockStateChange = true;
window.Height = Height;
}
public static void UpdateLastKnownLocation(double top, double left)
{
Top = top;
Left = left;
}
public static void UpdateLastKnownNormalSize(double width, double height)
{
Width = width;
Height = height;
}
public static void SetWindowMaximized(Window window)
{
IsMaximized = true;
window.WindowState = WindowState.Normal;
}
When setting the window size to normal
, the state change must be blocked for each property, otherwise the window will treat the last known location as 0, 0
(where it was when it was maximized).
We also check how far the mouse is from the left of the window when setting the window size to normal
. This allows us to create a smooth dragging effect away from maximized which is when useMouseLocation = true
, below.
private static double MousePercentageFromLeft(Window window)
{
var mouseMinusZeroToLeft = MouseHelper.MousePosition.X - window.Left;
var percentage = mouseMinusZeroToLeft / window.Width;
return percentage;
}
public static void SetWindowSizeToNormal(Window window, bool useMouseLocation = false)
{
IsMaximized = false;
var percentage = MousePercentageFromLeft(window);
SetWindowWidth(window);
SetWindowHeight(window);
if (useMouseLocation)
{
Top = MouseHelper.MousePosition.Y;
var valueOnNewSize = percentage * Width;
Left = MouseHelper.MousePosition.X - valueOnNewSize;
}
SetWindowTop(window);
SetWindowLeft(window);
}
3. Screen Finder
Next is the ScreenFinder
class which is used to determine the screen the window should maximize to in a multi-screen setup. It does so by checking in this order:
- Is the whole window inside the boundaries of a single screen? If so, return that screen.
- Is the screen between two screens (in a side-by-side orientation), If so, measure how much of the window is in each screen and return the largest result.
- Return the primary screen if the first two conditions are not met.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using WpfScreenHelper;
namespace CustomWindowWpf.Classes
{
public static class ScreenFinder
{
public static Screen FindAppropriateScreen(Window window)
{
var windowRight = window.Left + window.Width;
var windowBottom = window.Top + window.Height;
var allScreens = Screen.AllScreens.ToList();
var screenInsideAllBounds = allScreens.Find(x => window.Top >= x.Bounds.Top
&& window.Left >= x.Bounds.Left
&& windowRight <= x.Bounds.Right
&& windowBottom <= x.Bounds.Bottom);
if (screenInsideAllBounds != null)
{
return screenInsideAllBounds;
}
var screensInBounds = allScreens.FindAll(x => window.Top >= x.Bounds.Top
&& windowBottom <= x.Bounds.Bottom);
if (screensInBounds.Count > 0)
{
var values = new List<Tuple<double, Screen>>();
foreach (var screen in screensInBounds.OrderBy(x => x.Bounds.Left))
{
double amountInScreen;
if (screen.Bounds.Left == 0)
{
var rightOfWindow = window.Left + window.Width;
var outsideRightBoundary = rightOfWindow - screen.Bounds.Right;
amountInScreen = window.Width - outsideRightBoundary;
values.Add(new Tuple<double, Screen>(amountInScreen, screen));
}
else
{
var outsideLeftBoundary = screen.Bounds.Left - window.Left;
amountInScreen = window.Width - outsideLeftBoundary;
values.Add(new Tuple<double, Screen>(amountInScreen, screen));
}
}
values = values.OrderByDescending(x => x.Item1).ToList();
if (values.Count > 0)
{
return values[0].Item2;
}
}
return Screen.PrimaryScreen;
}
}
}
4. Back to the Window
Now that we've seen WindowStateHelper
and ScreenFinder
, we can return to the window to look at the last two methods:
PreviewMouseDown
(on the draggable area) SizeChanged
(on the whole window)
private void WindowDraggableArea_OnPreviewMouseDown(object sender, MouseButtonEventArgs e)
{
if (e.LeftButton != MouseButtonState.Pressed)
return;
if (WindowStateHelper.IsMaximized)
{
WindowStateHelper.SetWindowSizeToNormal(this, true);
ShowMaximumWindowButton();
DragMove();
}
else
{
DragMove();
}
WindowStateHelper.UpdateLastKnownLocation(Top, Left);
}
private void Window_OnSizeChanged(object sender, SizeChangedEventArgs e)
{
if (WindowState == WindowState.Maximized)
{
WindowStateHelper.SetWindowMaximized(this);
WindowStateHelper.BlockStateChange = true;
var screen = ScreenFinder.FindAppropriateScreen(this);
if (screen != null)
{
Top = screen.WorkingArea.Top;
Left = screen.WorkingArea.Left;
Width = screen.WorkingArea.Width;
Height = screen.WorkingArea.Height;
}
ShowRestoreDownButton();
}
else
{
if (WindowStateHelper.BlockStateChange)
{
WindowStateHelper.BlockStateChange = false;
return;
}
WindowStateHelper.UpdateLastKnownNormalSize(Width, Height);
WindowStateHelper.UpdateLastKnownLocation(Top, Left);
}
}
History
- May 2018: First published
- July 2018: Fixed 'expression is always true' bug in
ScreenFinder
- July 2018: Added GIF of window created using this code