Click here to Skip to main content
15,867,686 members
Articles / Desktop Programming / WPF

Custom WPF Window with Windows System Functionality Restored

Rate me:
Please Sign up or sign in to vote.
4.58/5 (6 votes)
15 Jun 2018CPOL4 min read 32.7K   1.3K   25   12
A fully custom WPF window, with all the functionality you would expect

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.

Image 1

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.

C#
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)
        {
            // Call for resizing effects
            _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.

C#
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;

    // ReSharper disable once SwitchStatementMissingSomeCases
    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.

C#
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.
C#
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; }

        // Required because using WindowState.Maximized will not respect 
        // the WorkingArea of the screen in a fully custom window
        public static bool IsMaximized { get; private set; }
        // Blocks the window from running OnSizeChanged when resizing the window
        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.

C#
// Returns a percentage which is how far the mouse pointer is from the left of the window
private static double MousePercentageFromLeft(Window window)
{
    var mouseMinusZeroToLeft = MouseHelper.MousePosition.X - window.Left;
    var percentage = mouseMinusZeroToLeft / window.Width;
    return percentage;
}

// Returns the window to its last known size and location before it was maximized.
// When useMouseLocation = true (dragging away from maximized) then the location is below the
// mouse pointer, respecting the percentage the pointer is from the left of the window
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:

  1. Is the whole window inside the boundaries of a single screen? If so, return that screen.
  2. 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.
  3. Return the primary screen if the first two conditions are not met.
C#
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();

            // If the window is inside all of a single screen boundaries, maximize to that
            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;
            }

            // Failing the above (between two screens in side-by-side configuration)
            // Measure if the window is between the top and bottom of any screens.
            // Then measure the percentage it is within each screen and pick a winner
            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>>(); 
                // Determine the amount of width inside each screen
                foreach (var screen in screensInBounds.OrderBy(x => x.Bounds.Left))
                {
                    // This has only been tested in a two screen, side-by-side setup.
                    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;
                }
            }

            // Failing all else
            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)
C#
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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer
United Kingdom United Kingdom
I am a software developer for a firm of accountants with a background in IT support. I believe that user experience comes before all else - without user engagement your best work will never be appreciated.

Comments and Discussions

 
QuestionTask bar is unavailable when maximized Pin
dotnetkurt10-Feb-20 9:42
dotnetkurt10-Feb-20 9:42 
GeneralMy vote of 4 Pin
webmaster4428-Jun-18 2:48
webmaster4428-Jun-18 2:48 
GeneralRe: My vote of 4 Pin
SirGrowns11-Jun-18 1:18
professionalSirGrowns11-Jun-18 1:18 
QuestionHave you memory profiled this Pin
Sacha Barber8-Jun-18 0:37
Sacha Barber8-Jun-18 0:37 
AnswerRe: Have you memory profiled this Pin
SirGrowns11-Jun-18 1:19
professionalSirGrowns11-Jun-18 1:19 
PraiseRe: Have you memory profiled this Pin
SirGrowns14-Jun-18 4:51
professionalSirGrowns14-Jun-18 4:51 
SuggestionGood, but... Pin
Alaa Ben Fatma6-Jun-18 11:49
professionalAlaa Ben Fatma6-Jun-18 11:49 
AnswerRe: Good, but... Pin
SirGrowns15-Jun-18 5:02
professionalSirGrowns15-Jun-18 5:02 
QuestionSource download link Pin
descartes27-May-18 22:16
descartes27-May-18 22:16 
AnswerRe: Source download link Pin
SirGrowns28-May-18 2:02
professionalSirGrowns28-May-18 2:02 
GeneralRe: Source download link Pin
Member 1134863730-May-18 12:51
Member 1134863730-May-18 12:51 
Source download link fails.
GeneralRe: Source download link Pin
SirGrowns30-May-18 20:40
professionalSirGrowns30-May-18 20:40 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.