Creating a custom WPF window

… for real this time…

One might argue that WPF is a legacy technology, that has no meaningful future. Well… if you take a look at the current desktop development ecosystem and you target Windows, there aren’t many alternatives. Sure you can use Java, Electron, plain old win32, etc. But… if you are a .NET guy like me, like to get good performance and OS integration, WPF is a great way to do it.

Now, while WPF is great and offers an abundance of customization options, there is an aspect of it that has always been a pain in the butt for many, many developers out there.

A Custom window...

I certainly had to spend numerous hours of research, trial and error, combining various blog posts and read a ton of WinAPI documentation, before I managed to put something together, that comes as close as you can get without resorting to Win32 host for your WPF app.

So, without further ado, let’s get to it. It’ll be a long one...

Initial setup

If you are reading an article on custom WPF windows, you probably know how to create a project in VisualStudio, so let’s skip over that.

Overally, before we begin, you need to have a Solution with an empty Controls Library and a WPF project that references that library.

SolutionExplorer_Initial.png

Then, let’s create our new Window class in the Controls Library project.

    public partial class SWWindow : System.Windows.Window
    {
    }

Add a ResourceDictionary in the Themes folder for our styles, as well.

After that we need to change the base class of our MainWindow in the WPF project.

<sw:SWWindow x:Class="WPFCustomWIndow.MainWindow"
             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"
             xmlns:sw="clr-namespace:SourceWeave.Controls;assembly=SourceWeave.Controls"
             xmlns:local="clr-namespace:WPFCustomWIndow"
             mc:Ignorable="d"
             Title="MainWindow" Height="450" Width="800">

</sw:SWWindow>
public partial class MainWindow : SWWindow

Merge the created Styles dictionary in the App.xaml, and we should be ready for the “fun” stuff.

<Application.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <ResourceDictionary Source="pack://application:,,,/SourceWeave.Controls;component/Themes/SWStyles.xaml"/>
            </ResourceDictionary.MergedDictionaries>
        </ResourceDictionary>
    </Application.Resources>

Creating our Window “Content”

Ok. So far so good. At this point starting the application should display an empty “normal” window.

PlainWindow.png

Our aim, is to remove the default, boring header bar and borders and replace them with our own.

As a first step, we need to create a custom ControlTemplate for our new window. We add that to the SWStyles.xaml resource dictionary we created in the setup steps.

After that, we need to create a Style for our MainWindow and base it on the created style. For that we create a resource dictionary in our WPF project and merge it alongside the first one in the App.xaml file.

SWStyles.xaml
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                    xmlns:fa="http://schemas.fontawesome.io/icons/"
                    xmlns:local="clr-namespace:SourceWeave.Controls">
    <Style TargetType="{x:Type Button}" x:Key="WindowButtonStyle">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type ButtonBase}">
                    <Border
                            x:Name="Chrome"
                            BorderBrush="{TemplateBinding BorderBrush}"
                            Margin="0"
                            Background="{TemplateBinding Background}"
                            SnapsToDevicePixels="True">
                        <ContentPresenter
                                ContentTemplate="{TemplateBinding ContentTemplate}"
                                Content="{TemplateBinding Content}"
                                ContentStringFormat="{TemplateBinding ContentStringFormat}"
                                HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
                                Margin="{TemplateBinding Padding}"
                                RecognizesAccessKey="True"
                                SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"
                                VerticalAlignment="{TemplateBinding VerticalContentAlignment}" />
                    </Border>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
        <Setter Property="Background" Value="Transparent"/>
        <Setter Property="FontFamily" Value="Webdings"/>
        <Setter Property="FontSize" Value="13.333" />
        <Setter Property="Foreground" Value="Black" />
        <Setter Property="Margin" Value="0,2,3,0"/>
        <Style.Triggers>
            <Trigger Property="IsMouseOver" Value="True">
                <Setter Property="Foreground" Value="Gray" />
            </Trigger>
        </Style.Triggers>
    </Style>

    <Style TargetType="local:SWWindow" x:Key="SWWindowStyle">
        <Setter Property="Background" Value="White"/>
        <Setter Property="BorderBrush" Value="Black"/>
        <Setter Property="MinHeight" Value="320"/>
        <Setter Property="MinWidth" Value="480"/>
        <Setter Property="RenderOptions.BitmapScalingMode" Value="HighQuality"/>
        <Setter Property="Title" Value="{Binding Title}"/>
       
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type local:SWWindow}">

                    <Grid Background="Transparent" x:Name="WindowRoot">

                        <Grid x:Name="LayoutRoot"
                              Background="{TemplateBinding Background}">
                            <Grid.RowDefinitions>
                                <RowDefinition Height="36"/>
                                <RowDefinition Height="*"/>
                            </Grid.RowDefinitions>

                            <!--TitleBar-->
                            <Grid x:Name="PART_HeaderBar">
                                <Grid.ColumnDefinitions>
                                    <ColumnDefinition Width="Auto"/>
                                    <ColumnDefinition Width="*"/>
                                    <ColumnDefinition Width="Auto"/>
                                </Grid.ColumnDefinitions>

                                <TextBlock Text="{TemplateBinding Title}" 
                                           Grid.Column="0"
                                           Grid.ColumnSpan="3"
                                           TextTrimming="CharacterEllipsis"
                                           HorizontalAlignment="Stretch" 
                                           FontSize="13"
                                           TextAlignment="Center"
                                           VerticalAlignment="Center"
                                           Width="Auto"
                                           Padding="200 0 200 0"
                                           Foreground="Black"
                                           Panel.ZIndex="0"
                                           IsEnabled="{TemplateBinding IsActive}"/>

                                <Grid x:Name="WindowControlsGrid" Grid.Column="2" Background="White">
                                    <Grid.ColumnDefinitions>
                                        <ColumnDefinition Width="36"/>
                                        <ColumnDefinition Width="36"/>
                                        <ColumnDefinition Width="36"/>
                                    </Grid.ColumnDefinitions>

                                    <Button x:Name="MinimizeButton" Style="{StaticResource WindowButtonStyle}" 
                                            fa:Awesome.Content="WindowMinimize" 
                                            TextElement.FontFamily="pack://application:,,,/FontAwesome.WPF;component/#FontAwesome"
                                            Grid.Column="0"/>
                                    <Button x:Name="MaximizeButton" Style="{StaticResource WindowButtonStyle}" 
                                            fa:Awesome.Content="WindowMaximize" 
                                            TextElement.FontFamily="pack://application:,,,/FontAwesome.WPF;component/#FontAwesome"
                                            Grid.Column="1"/>

                                    <Button x:Name="RestoreButton" Style="{StaticResource WindowButtonStyle}" 
                                            fa:Awesome.Content="WindowRestore"
                                            Visibility="Collapsed"
                                            TextElement.FontFamily="pack://application:,,,/FontAwesome.WPF;component/#FontAwesome"
                                            Grid.Column="1"/>

                                    <Button x:Name="CloseButton" Style="{StaticResource WindowButtonStyle}" 
                                            fa:Awesome.Content="Times" 
                                            TextElement.FontFamily="pack://application:,,,/FontAwesome.WPF;component/#FontAwesome"
                                            TextElement.FontSize="24"
                                            Grid.Column="2"/>
                                </Grid>
                            </Grid>

                            <Grid x:Name="PART_MainContentGrid"
                                  Grid.Row="1"
                                  Panel.ZIndex="10">
                                <ContentPresenter x:Name="PART_MainContentPresenter" Grid.Row="1"/>
                            </Grid>
                        </Grid>
                    </Grid>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</ResourceDictionary>
WPF Project -> Styles.xaml
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                    xmlns:local="clr-namespace:WPFCustomWindowSample">

    <Style TargetType="local:MainWindow" BasedOn="{StaticResource SWWindowStyle}"/>
</ResourceDictionary>
WPF Project -> App.xaml
<Application.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <ResourceDictionary Source="pack://application:,,,/SourceWeave.Controls;component/Themes/SWStyles.xaml"/>
                <ResourceDictionary Source="Styles.xaml"/>
            </ResourceDictionary.MergedDictionaries>
        </ResourceDictionary>
    </Application.Resources>

Ok. Let’s take a look at SWStyles.xaml

The first style is a basic button style for our Window control buttons.

The fun stuff starts in the second style. We have a pretty basic and standard Control template with a Header bar and a Content presenter.

Oh…

One more bonus thing we will learn in this article - how to use FontAwesome in WPF. :)

Just invoke this in your PackageManager console, for both projects and you’re all set.

PM> Install-Package FontAwesome.WPF

We use it for cool window control icons, but there is a lot more you can do with it. Just visit their github page

At this point starting the project should look like:

NestedWindow.png

The buttons on the custom header still don’t work and we’ll need them after we remove the default header. Let’s hook them up.

    public partial class SWWindow : Window
    {
        public Grid WindowRoot { get; private set; }
        public Grid LayoutRoot { get; private set; }
        public Button MinimizeButton { get; private set; }
        public Button MaximizeButton { get; private set; }
        public Button RestoreButton { get; private set; }
        public Button CloseButton { get; private set; }
        public Grid HeaderBar { get; private set; }

        public T GetRequiredTemplateChild<T>(string childName) where T : DependencyObject
        {
            return (T)base.GetTemplateChild(childName);
        }

        public override void OnApplyTemplate()
        {
            this.WindowRoot = this.GetRequiredTemplateChild<Grid>("WindowRoot");
            this.LayoutRoot = this.GetRequiredTemplateChild<Grid>("LayoutRoot");
            this.MinimizeButton = this.GetRequiredTemplateChild<System.Windows.Controls.Button>("MinimizeButton");
            this.MaximizeButton = this.GetRequiredTemplateChild<System.Windows.Controls.Button>("MaximizeButton");
            this.RestoreButton = this.GetRequiredTemplateChild<System.Windows.Controls.Button>("RestoreButton");
            this.CloseButton = this.GetRequiredTemplateChild<System.Windows.Controls.Button>("CloseButton");
            this.HeaderBar = this.GetRequiredTemplateChild<Grid>("PART_HeaderBar");

            if (this.CloseButton != null)
            {
                this.CloseButton.Click += CloseButton_Click;
            }

            if (this.MinimizeButton != null)
            {
                this.MinimizeButton.Click += MinimizeButton_Click;
            }

            if (this.RestoreButton != null)
            {
                this.RestoreButton.Click += RestoreButton_Click;
            }

            if (this.MaximizeButton != null)
            {
                this.MaximizeButton.Click += MaximizeButton_Click;
            }

            base.OnApplyTemplate();
        }

        protected void ToggleWindowState()
        {
            if (base.WindowState != WindowState.Maximized)
            {
                base.WindowState = WindowState.Maximized;
            }
            else
            {
                base.WindowState = WindowState.Normal;
            }
        }

        private void MaximizeButton_Click(object sender, RoutedEventArgs e)
        {
            this.ToggleWindowState();
        }

        private void RestoreButton_Click(object sender, RoutedEventArgs e)
        {
            this.ToggleWindowState();
        }

        private void MinimizeButton_Click(object sender, RoutedEventArgs e)
        {
            this.WindowState = WindowState.Minimized;
        }

        private void CloseButton_Click(object sender, RoutedEventArgs e)
        {
            this.Close();
        }
    }

Great!

Now that the buttons are hooked and they work, it’s time to remove that dreaded Windows border.

Removing the Window Chrome

Ok. Most of the articles you can find on the web, will tell you to set the Window Style to None. While it’s true that this will take care of the dreaded window border, you lose a lot of the window functionality in the process. Things like docking the window with mouse drag, using key combinations to minimize, dock, etc. won’t work. Another “cool” side efect is that when you maximize the window, it will cover the taskbar as well. Oh, and if you are a stickler for visuals - the window shadow and animations are M.I.A.

I have a better way for you. Ready?

SWStyles.xaml -> SWWindowStyle

<Setter Property="WindowChrome.WindowChrome">
  <Setter.Value>
    <WindowChrome GlassFrameThickness="1" 
                  ResizeBorderThickness="4"
                  CaptionHeight="0"/>
  </Setter.Value>
</Setter>

Starting the app this way you get the custom window you have always dream of… almost.

Chromeless.png

There are still some things we have to do. First and foremost - the window isn’t draggable. Let’s fix that.

        //SWWindow.cs
        public override void OnApplyTemplate()
        {
            // ...
            this.HeaderBar = this.GetRequiredTemplateChild<Grid>("PART_HeaderBar");
            // ...

            if (this.HeaderBar != null)
            {
                this.HeaderBar.AddHandler(Grid.MouseLeftButtonDownEvent, new MouseButtonEventHandler(this.OnHeaderBarMouseLeftButtonDown));
            }

            base.OnApplyTemplate();
        }

        protected virtual void OnHeaderBarMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
        {
            System.Windows.Point position = e.GetPosition(this);
            int headerBarHeight = 36;
            int leftmostClickableOffset = 50;

            if (position.X - this.LayoutRoot.Margin.Left <= leftmostClickableOffset && position.Y <= headerBarHeight)
            {
                if (e.ClickCount != 2)
                {
                    // this.OpenSystemContextMenu(e);
                }
                else
                {
                    base.Close();
                }
                e.Handled = true;
                return;
            }

            if (e.ClickCount == 2 && base.ResizeMode == ResizeMode.CanResize)
            {
                this.ToggleWindowState();
                return;
            }

            if (base.WindowState == WindowState.Maximized)
            {
                this.isMouseButtonDown = true;
                this.mouseDownPosition = position;
            }
            else
            {
                try
                {
                    this.positionBeforeDrag = new System.Windows.Point(base.Left, base.Top);
                    base.DragMove();
                }
                catch
                {
                }
            }
        }

Now, there is a lot going on here, but, the highlight is: the window moves, maximizes and closes as a normal window would with HeaderBar interaction. There is a commented out clause there, but we’ll deal with that a bit later.

This can be enough for you at this stage, as this is a fully functional window. But… you might have noticed some wierd stuff.

In some cases, maximizing the window, will cut off a part of the frame. If you have a dual monitor setup, you might even see where the cut part sticks out on the adjacent monitor.

To deal with that… we have to get… creative.

Polishing the behavior

Now, bear with me here. The following magic s the result of a week-long research and testing on different DPIs, but, I found a way to solve that issue. For this, you will need to add two additional references to the Controls Library project.

references_Highlight.png

… and create a System helper to get some OS configuration values.

    internal static class SystemHelper
    {
        public static int GetCurrentDPI()
        {
            return (int)typeof(SystemParameters).GetProperty("Dpi", BindingFlags.Static | BindingFlags.NonPublic).GetValue(null, null);
        }

        public static double GetCurrentDPIScaleFactor()
        {
            return (double)SystemHelper.GetCurrentDPI() / 96;
        }

        public static Point GetMousePositionWindowsForms()
        {
            System.Drawing.Point point = Control.MousePosition;
            return new Point(point.X, point.Y);
        }
    }

After that, we will need to handle some of the resizing and state change events of the window.

        // SWWindow.Sizing.cs
        public SWWindow()
        {
            double currentDPIScaleFactor = (double)SystemHelper.GetCurrentDPIScaleFactor();
            Screen screen = Screen.FromHandle((new WindowInteropHelper(this)).Handle);
            base.SizeChanged += new SizeChangedEventHandler(this.OnSizeChanged);
            base.StateChanged += new EventHandler(this.OnStateChanged);
            base.Loaded += new RoutedEventHandler(this.OnLoaded);
            Rectangle workingArea = screen.WorkingArea;
            base.MaxHeight = (double)(workingArea.Height + 16) / currentDPIScaleFactor;
            SystemEvents.DisplaySettingsChanged += new EventHandler(this.SystemEvents_DisplaySettingsChanged);
            this.AddHandler(Window.MouseLeftButtonUpEvent, new MouseButtonEventHandler(this.OnMouseButtonUp), true);
            this.AddHandler(Window.MouseMoveEvent, new System.Windows.Input.MouseEventHandler(this.OnMouseMove));
        }

        protected virtual Thickness GetDefaultMarginForDpi()
        {
            int currentDPI = SystemHelper.GetCurrentDPI();
            Thickness thickness = new Thickness(8, 8, 8, 8);
            if (currentDPI == 120)
            {
                thickness = new Thickness(7, 7, 4, 5);
            }
            else if (currentDPI == 144)
            {
                thickness = new Thickness(7, 7, 3, 1);
            }
            else if (currentDPI == 168)
            {
                thickness = new Thickness(6, 6, 2, 0);
            }
            else if (currentDPI == 192)
            {
                thickness = new Thickness(6, 6, 0, 0);
            }
            else if (currentDPI == 240)
            {
                thickness = new Thickness(6, 6, 0, 0);
            }
            return thickness;
        }

        protected virtual Thickness GetFromMinimizedMarginForDpi()
        {
            int currentDPI = SystemHelper.GetCurrentDPI();
            Thickness thickness = new Thickness(7, 7, 5, 7);
            if (currentDPI == 120)
            {
                thickness = new Thickness(6, 6, 4, 6);
            }
            else if (currentDPI == 144)
            {
                thickness = new Thickness(7, 7, 4, 4);
            }
            else if (currentDPI == 168)
            {
                thickness = new Thickness(6, 6, 2, 2);
            }
            else if (currentDPI == 192)
            {
                thickness = new Thickness(6, 6, 2, 2);
            }
            else if (currentDPI == 240)
            {
                thickness = new Thickness(6, 6, 0, 0);
            }
            return thickness;
        }

        private void OnLoaded(object sender, RoutedEventArgs e)
        {
            Screen screen = Screen.FromHandle((new WindowInteropHelper(this)).Handle);
            double width = (double)screen.WorkingArea.Width;
            Rectangle workingArea = screen.WorkingArea;
            this.previousScreenBounds = new System.Windows.Point(width, (double)workingArea.Height);
        }

        private void SystemEvents_DisplaySettingsChanged(object sender, EventArgs e)
        {
            Screen screen = Screen.FromHandle((new WindowInteropHelper(this)).Handle);
            double width = (double)screen.WorkingArea.Width;
            Rectangle workingArea = screen.WorkingArea;
            this.previousScreenBounds = new System.Windows.Point(width, (double)workingArea.Height);
            this.RefreshWindowState();
        }

        private void OnSizeChanged(object sender, SizeChangedEventArgs e)
        {
            if (base.WindowState == WindowState.Normal)
            {
                this.HeightBeforeMaximize = base.ActualHeight;
                this.WidthBeforeMaximize = base.ActualWidth;
                return;
            }
            if (base.WindowState == WindowState.Maximized)
            {
                Screen screen = Screen.FromHandle((new WindowInteropHelper(this)).Handle);
                if (this.previousScreenBounds.X != (double)screen.WorkingArea.Width || this.previousScreenBounds.Y != (double)screen.WorkingArea.Height)
                {
                    double width = (double)screen.WorkingArea.Width;
                    Rectangle workingArea = screen.WorkingArea;
                    this.previousScreenBounds = new System.Windows.Point(width, (double)workingArea.Height);
                    this.RefreshWindowState();
                }
            }
        }

        private void OnStateChanged(object sender, EventArgs e)
        {
            Screen screen = Screen.FromHandle((new WindowInteropHelper(this)).Handle);
            Thickness thickness = new Thickness(0);
            if (this.WindowState != WindowState.Maximized)
            {

                double currentDPIScaleFactor = (double)SystemHelper.GetCurrentDPIScaleFactor();
                Rectangle workingArea = screen.WorkingArea;
                this.MaxHeight = (double)(workingArea.Height + 16) / currentDPIScaleFactor;
                this.MaxWidth = double.PositiveInfinity;

                if (this.WindowState != WindowState.Maximized)
                {
                    this.SetMaximizeButtonsVisibility(Visibility.Visible, Visibility.Collapsed);
                }
            }
            else
            {

                thickness = this.GetDefaultMarginForDpi();
                if (this.PreviousState == WindowState.Minimized || this.Left == this.positionBeforeDrag.X && this.Top == this.positionBeforeDrag.Y)
                {
                    thickness = this.GetFromMinimizedMarginForDpi();
                }

                this.SetMaximizeButtonsVisibility(Visibility.Collapsed, Visibility.Visible);
            }

            this.LayoutRoot.Margin = thickness;
            this.PreviousState = this.WindowState;
        }

        private void OnMouseMove(object sender, System.Windows.Input.MouseEventArgs e)
        {
            if (!this.isMouseButtonDown)
            {
                return;
            }

            double currentDPIScaleFactor = (double)SystemHelper.GetCurrentDPIScaleFactor();
            System.Windows.Point position = e.GetPosition(this);
            System.Diagnostics.Debug.WriteLine(position);
            System.Windows.Point screen = base.PointToScreen(position);
            double x = this.mouseDownPosition.X - position.X;
            double y = this.mouseDownPosition.Y - position.Y;
            if (Math.Sqrt(Math.Pow(x, 2) + Math.Pow(y, 2)) > 1)
            {
                double actualWidth = this.mouseDownPosition.X;

                if (this.mouseDownPosition.X <= 0)
                {
                    actualWidth = 0;
                }
                else if (this.mouseDownPosition.X >= base.ActualWidth)
                {
                    actualWidth = this.WidthBeforeMaximize;
                }

                if (base.WindowState == WindowState.Maximized)
                {
                    this.ToggleWindowState();
                    this.Top = (screen.Y - position.Y) / currentDPIScaleFactor;
                    this.Left = (screen.X - actualWidth) / currentDPIScaleFactor;
                    this.CaptureMouse();
                }

                this.isManualDrag = true;

                this.Top = (screen.Y - this.mouseDownPosition.Y) / currentDPIScaleFactor;
                this.Left = (screen.X - actualWidth) / currentDPIScaleFactor;
            }
        }


        private void OnMouseButtonUp(object sender, MouseButtonEventArgs e)
        {
            this.isMouseButtonDown = false;
            this.isManualDrag = false;
            this.ReleaseMouseCapture();
        }

        private void RefreshWindowState()
        {
            if (base.WindowState == WindowState.Maximized)
            {
                this.ToggleWindowState();
                this.ToggleWindowState();
            }
        }

Do I know how this looks? Oh, yeah!

Is it pretty? Hell no!

But…

About 80% of the time, it works all the time! Which is good enough for most custom window applications with WPF. Plus, if you take a look behind the scenes of one of the commonly used IDEs for WPF (VisualStudio, like anyone would use anything else for that…) You will find a lot of the same, and worse. Don’t believe me? Just decompile devenv.exe, and take a look ;)

Of course, a lot of the code can be better architectured, abstracted, etc. However, this is not the point of the post. Do what you will with the information and approaches you have seen.

Now, I promised to take a look at the commented out section in the HeaderBar MouseDown handler. Here is where it gets hardcore.

Displaying the system’s context menu

This is something I just couldn’t find a way to do without using interop services. The only other way would be to implement every single functionality manually, but that’s just… bonkers. So…

First we need a “bridge” class to call native functions.

    internal static class NativeUtils
    {
        internal static uint TPM_LEFTALIGN;

        internal static uint TPM_RETURNCMD;

        static NativeUtils()
        {
            NativeUtils.TPM_LEFTALIGN = 0;
            NativeUtils.TPM_RETURNCMD = 256;
        }

        [DllImport("user32.dll", CharSet = CharSet.None, ExactSpelling = false)]
        internal static extern IntPtr PostMessage(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam);

        [DllImport("user32.dll", CharSet = CharSet.Auto, ExactSpelling = false, SetLastError = true)]
        internal static extern IntPtr GetSystemMenu(IntPtr hWnd, bool bRevert);

        [DllImport("user32.dll", CharSet = CharSet.None, ExactSpelling = false)]
        internal static extern bool EnableMenuItem(IntPtr hMenu, uint uIDEnableItem, uint uEnable);

        [DllImport("user32.dll", CharSet = CharSet.None, ExactSpelling = false)]
        internal static extern int TrackPopupMenuEx(IntPtr hmenu, uint fuFlags, int x, int y, IntPtr hwnd, IntPtr lptpm);
    }

After that it’s pretty straightforward. Just uncomment that section of the Header MouseLeftButtonDown handler, and add the following method.

        private void OpenSystemContextMenu(MouseButtonEventArgs e)
        {
            System.Windows.Point position = e.GetPosition(this);
            System.Windows.Point screen = this.PointToScreen(position);
            int num = 36;
            if (position.Y < (double)num)
            {
                IntPtr handle = (new WindowInteropHelper(this)).Handle;
                IntPtr systemMenu = NativeUtils.GetSystemMenu(handle, false);
                if (base.WindowState != WindowState.Maximized)
                {
                    NativeUtils.EnableMenuItem(systemMenu, 61488, 0);
                }
                else
                {
                    NativeUtils.EnableMenuItem(systemMenu, 61488, 1);
                }
                int num1 = NativeUtils.TrackPopupMenuEx(systemMenu, NativeUtils.TPM_LEFTALIGN | NativeUtils.TPM_RETURNCMD, Convert.ToInt32(screen.X + 2), Convert.ToInt32(screen.Y + 2), handle, IntPtr.Zero);
                if (num1 == 0)
                {
                    return;
                }

                NativeUtils.PostMessage(handle, 274, new IntPtr(num1), IntPtr.Zero);
            }
        }

That, I admit, is copy-pasted. Can’t remember which of the thousand articles it is from, but it works.

ContextMenu.png

Populate

Now just for the fun of it, let’s add some content to our Main window. You know, to see that it actually works.

<sw:SWWindow x:Class="WPFCustomWindow.MainWindow"
             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"
             xmlns:sw="clr-namespace:SourceWeave.Controls;assembly=SourceWeave.Controls"
             xmlns:local="clr-namespace:WPFCustomWindow"
             mc:Ignorable="d"
             Title="MagicMainWindow" Height="450" Width="800">
    <Grid>
        <Button Content="Click me to see some magic!" Click="Button_Click"/>
    </Grid>
</sw:SWWindow>
    public partial class MainWindow : SWWindow
    {
        public MainWindow()
        {
            InitializeComponent();
        }
        private void Button_Click(object sender, RoutedEventArgs e)
        {
            MessageBox.Show("Some Magic");
        }
    }
Content.png

Wrap up

Ok so… We learned How to:

  1. Inherit from the System Window

  2. Customize our Window’s content template

  3. Remove the Window Chrome

  4. Make the Chromeless Window, actually behave as we would expect it to

  5. Display the default Window context menu on our custom window.

You can find the code in my github. You can use it as you see fit. I sure would have taken advantage of such an example when I had to do it.

Let me know if you know of a better way to create custom windows in WPF.