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

Creating a Custom WPF Window

Rate me:
Please Sign up or sign in to vote.
5.00/5 (15 votes)
16 Nov 2018CPOL6 min read 23.3K   21   6
Often, when WPF developers have to write a custom window, they find themselves drowning in countless articles, blog posts, and StackOverflow threads each depicting a different approach to the problem.

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.

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 Visual Studio, so let’s skip over that.

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

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

Image 1

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

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

XML
<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.

Image 2

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.

XML
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>
XML
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>
XML
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:

Image 3

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.

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

XML
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 dreamed of… almost.

Image 4

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

C#
//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.

Image 5

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

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

C#
        // 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.

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

C#
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 a copy-paste. Can’t remember which of the thousand articles it is from, but it works.

Image 6

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.

C#
<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>
C#
public partial class MainWindow : SWWindow
{
    public MainWindow()
    {
        InitializeComponent();
    }
    private void Button_Click(object sender, RoutedEventArgs e)
    {
        MessageBox.Show("Some Magic");
    }
}

Image 7

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 our 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.

This article was originally posted at https://www.source-weave.com/blog/custom-wpf-window

License

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


Written By
Founder Source Weave
Bulgaria Bulgaria
I started my career in the WPF division of one of the world’s leading 3rd party component providers for the .NET ecosystem (Telerik, now Progress). I spent nearly 5 years there during which I worked on numerous UI components and UX frameworks for WPF and Silverlight. At some point, I started wanting to see the other side of the business, and see how a whole application is created, not just the building blocks I was used to, so I joined a product company which offered the most downloaded office app on Google Play (MobiSystems Office Suite). From the get-go, I started working on a desktop version of the suite and building a team that can extend and support the application suite in the future.

I trained several developers and lead the team for a year and a half. During that time I learned a lot about the business and development processes for a big software project and began to develop a vision for а company of my own.

I started SourceWeave at the beginning of 2016, together with a very good friend and colleague of mine, with the vision of providing the best possible services to help companies with their software development efforts. We believe every single customer should feel they have a dedicated team that can support them on the way to achieving their vision.

Comments and Discussions

 
QuestionCustom Window Properties Pin
Member 1619030226-Jan-24 5:27
Member 1619030226-Jan-24 5:27 
PraiseGreat Pin
hitchao14-Jan-23 19:52
hitchao14-Jan-23 19:52 
GeneralMy vote of 5 Pin
Carsten V2.01-Dec-18 1:11
Carsten V2.01-Dec-18 1:11 
PraiseDelightful! Pin
asiwel21-Nov-18 17:06
professionalasiwel21-Nov-18 17:06 
GeneralRe: Delightful! Pin
nvasilev121-Nov-18 21:35
nvasilev121-Nov-18 21:35 
PraiseMy vote of 5 Pin
Stylianos Polychroniadis16-Nov-18 7:24
Stylianos Polychroniadis16-Nov-18 7:24 
Nice hack, well done!

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.