We are developing a WPF application which uses Telerik’s suite of controls and everything works and looks fine. Unfortunately, we recently needed to replace the base class of all our dialogs, changing RadWindow by the standard WPF window (reason is irrelevant to this discussion). In doing so, we ended up having an application which still looked pretty on all developer’s computers (Windows 7 with Aero enabled) but was ugly when used in our client’s environment (Terminal Services under Windows Server 2008 R2).
Telerik’s RadWindow is a standard user control that mimicks a dialog’s behaviour so styling it was not an issue. With WPF’s Window though, I have a hard time changing its «border». What I mean by «border» here is both the title bar with the icon and the 3 standard buttons (Minimize, Maximize/Restore, Close) and the resize grip around the window.
How can I change the looks of these items:
- Title bar color
- 3 standard buttons
- Window’s real border color
With round corners if possible.
asked Apr 2, 2012 at 14:41
Marcel GosselinMarcel Gosselin
4,6202 gold badges29 silver badges52 bronze badges
0
You need to set
WindowStyle="None", AllowsTransparency="True" and optionally ResizeMode="NoResize"
and then set the Style property of the window to your custom window style, where you design the appearance of the window (title bar, buttons, border) to anything you want and display the window contents in a ContentPresenter.
This seems to be a good article on how you can achieve this, but there are many other articles on the internet.
Igor Popov
9,6657 gold badges54 silver badges68 bronze badges
answered Apr 2, 2012 at 14:57
Lescai IonelLescai Ionel
4,1973 gold badges30 silver badges48 bronze badges
3
I found a more straight forward solution from @DK comment in this question, the solution is written by Alex and described here with source,
To make customized window:
- download the sample project here
- edit the generic.xaml file to customize the layout.
- enjoy :).
answered Apr 10, 2014 at 15:00
AnasAnas
7117 silver badges24 bronze badges
Such statements as “you can’t because only Windows can control the non-client area” are not quite true — Windows lets you specify the dimensions of the non–client area.
The downside is this is only possible by calling Windows’ kernel methods, and since you’re in .NET, which is not native code, you’ll need P/Invoke. (Remember, the whole of the Windows Form UI and console application I/O methods are offered as wrappers that make system calls under the hood.) Hence, as documented in MSDN, it is completely possible to use P/Invoke to access those methods that are needed to set up the non–client area.
Update: Simpler than ever!
As of .NET 4.5, you can just use the WindowChrome class to adjust the non-client area. Get started here and here, a guide to changing the window border dimensions. By setting it to 0, you’ll be able to implement your custom window border in place of the system’s one.
answered Sep 26, 2018 at 13:19
Davide CannizzoDavide Cannizzo
2,7061 gold badge29 silver badges31 bronze badges
I suggest you to start from an existing solution and customize it to fit your needs, that’s better than starting from scratch!
I was looking for the same thing and I fall on this open source solution, I hope it will help.
answered Jun 30, 2015 at 13:32
Benzara TaharBenzara Tahar
2,0081 gold badge17 silver badges19 bronze badges
0
We are developing a WPF application which uses Telerik’s suite of controls and everything works and looks fine. Unfortunately, we recently needed to replace the base class of all our dialogs, changing RadWindow by the standard WPF window (reason is irrelevant to this discussion). In doing so, we ended up having an application which still looked pretty on all developer’s computers (Windows 7 with Aero enabled) but was ugly when used in our client’s environment (Terminal Services under Windows Server 2008 R2).
Telerik’s RadWindow is a standard user control that mimicks a dialog’s behaviour so styling it was not an issue. With WPF’s Window though, I have a hard time changing its «border». What I mean by «border» here is both the title bar with the icon and the 3 standard buttons (Minimize, Maximize/Restore, Close) and the resize grip around the window.
How can I change the looks of these items:
- Title bar color
- 3 standard buttons
- Window’s real border color
With round corners if possible.
asked Apr 2, 2012 at 14:41
Marcel GosselinMarcel Gosselin
4,6202 gold badges29 silver badges52 bronze badges
0
You need to set
WindowStyle="None", AllowsTransparency="True" and optionally ResizeMode="NoResize"
and then set the Style property of the window to your custom window style, where you design the appearance of the window (title bar, buttons, border) to anything you want and display the window contents in a ContentPresenter.
This seems to be a good article on how you can achieve this, but there are many other articles on the internet.
Igor Popov
9,6657 gold badges54 silver badges68 bronze badges
answered Apr 2, 2012 at 14:57
Lescai IonelLescai Ionel
4,1973 gold badges30 silver badges48 bronze badges
3
I found a more straight forward solution from @DK comment in this question, the solution is written by Alex and described here with source,
To make customized window:
- download the sample project here
- edit the generic.xaml file to customize the layout.
- enjoy :).
answered Apr 10, 2014 at 15:00
AnasAnas
7117 silver badges24 bronze badges
Such statements as “you can’t because only Windows can control the non-client area” are not quite true — Windows lets you specify the dimensions of the non–client area.
The downside is this is only possible by calling Windows’ kernel methods, and since you’re in .NET, which is not native code, you’ll need P/Invoke. (Remember, the whole of the Windows Form UI and console application I/O methods are offered as wrappers that make system calls under the hood.) Hence, as documented in MSDN, it is completely possible to use P/Invoke to access those methods that are needed to set up the non–client area.
Update: Simpler than ever!
As of .NET 4.5, you can just use the WindowChrome class to adjust the non-client area. Get started here and here, a guide to changing the window border dimensions. By setting it to 0, you’ll be able to implement your custom window border in place of the system’s one.
answered Sep 26, 2018 at 13:19
Davide CannizzoDavide Cannizzo
2,7061 gold badge29 silver badges31 bronze badges
I suggest you to start from an existing solution and customize it to fit your needs, that’s better than starting from scratch!
I was looking for the same thing and I fall on this open source solution, I hope it will help.
answered Jun 30, 2015 at 13:32
Benzara TaharBenzara Tahar
2,0081 gold badge17 silver badges19 bronze badges
0
There are several good reasons for wanting custom window chrome in WPF, such as fitting in additional UI or implementing a Dark theme. However the actual implementation is kind of tricky, since it is now your job to provide a bunch of features that you used to get for free. I’ll walk you through my implementation.
Appearance
I chose to emulate the Windows 10 style.
This will keep my UI consistent with the rest of Windows. I am choosing not to attempt to match the style for old versions of Windows, as I don’t think that would be a great time investment. It’s one little thing you lose when going with a full custom title bar, but it should be worth it by allowing full cohesive theming. Buttons are 46 wide, 32 tall.
Building the UI
First, set WindowStyle=”None” on your Window. That will remove the built-in title bar and allow you to do everything on your own.
Next, allocate space for your title bar in your UI. I like to allocate a Grid row for it.
Add this under the <Window> node in your XAML:
<WindowChrome.WindowChrome>
<WindowChrome CaptionHeight="32" ResizeBorderThickness="{x:Static SystemParameters.WindowResizeBorderThickness}" />
</WindowChrome.WindowChrome>
The CaptionHeight tells the OS to treat the top 32px of your window as if it was a title bar. This means that click to drag works, along with double clicking to maximize/restore, shaking to minimize other windows, etc. The ResizeBorderThickness allows the standard window resize logic to work, so we don’t need to reimplement that either.
Now we need to make the actual UI. This is mine:
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Image
Grid.Column="0"
Width="22"
Height="22"
Margin="4"
Source="/Icons/VidCoder32.png" />
<TextBlock
Grid.Column="1"
Margin="4 0 0 0"
VerticalAlignment="Center"
FontSize="14"
Text="{Binding WindowTitle}">
<TextBlock.Style>
<Style TargetType="TextBlock">
<Style.Triggers>
<DataTrigger Binding="{Binding IsActive, RelativeSource={RelativeSource AncestorType=Window}}" Value="False">
<Setter Property="Foreground" Value="{DynamicResource WindowTitleBarInactiveText}" />
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
<Button
Grid.Column="2"
Click="OnMinimizeButtonClick"
RenderOptions.EdgeMode="Aliased"
Style="{StaticResource TitleBarButtonStyle}">
<Path
Width="46"
Height="32"
Data="M 18,15 H 28"
Stroke="{Binding Path=Foreground,
RelativeSource={RelativeSource AncestorType={x:Type Button}}}"
StrokeThickness="1" />
</Button>
<Button
Name="maximizeButton"
Grid.Column="3"
Click="OnMaximizeRestoreButtonClick"
Style="{StaticResource TitleBarButtonStyle}">
<Path
Width="46"
Height="32"
Data="M 18.5,10.5 H 27.5 V 19.5 H 18.5 Z"
Stroke="{Binding Path=Foreground,
RelativeSource={RelativeSource AncestorType={x:Type Button}}}"
StrokeThickness="1" />
</Button>
<Button
Name="restoreButton"
Grid.Column="3"
Click="OnMaximizeRestoreButtonClick"
Style="{StaticResource TitleBarButtonStyle}">
<Path
Width="46"
Height="32"
Data="M 18.5,12.5 H 25.5 V 19.5 H 18.5 Z M 20.5,12.5 V 10.5 H 27.5 V 17.5 H 25.5"
Stroke="{Binding Path=Foreground,
RelativeSource={RelativeSource AncestorType={x:Type Button}}}"
StrokeThickness="1" />
</Button>
<Button
Grid.Column="4"
Click="OnCloseButtonClick"
Style="{StaticResource TitleBarCloseButtonStyle}">
<Path
Width="46"
Height="32"
Data="M 18,11 27,20 M 18,20 27,11"
Stroke="{Binding Path=Foreground,
RelativeSource={RelativeSource AncestorType={x:Type Button}}}"
StrokeThickness="1" />
</Button>
</Grid>
I’ve got the app icon. I chose not to implement the special drop-down menu that comes with the standard title bar since it’s not often used and other major apps like Visual Studio Code don’t bother with it. But it’s certainly something you could add.
The title text has a trigger to change its color based on the “Active” state of the window. That allows the user to better tell if the window has focus or not.
The actual buttons use TitleBarButtonStyle and TitleBarCloseButtonStyle:
<Style x:Key="TitleBarButtonStyle" TargetType="Button">
<Setter Property="Foreground" Value="{DynamicResource WindowTextBrush}" />
<Setter Property="Padding" Value="0" />
<Setter Property="WindowChrome.IsHitTestVisibleInChrome" Value="True" />
<Setter Property="IsTabStop" Value="False" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Button}">
<Border
x:Name="border"
Background="Transparent"
BorderThickness="0"
SnapsToDevicePixels="true">
<ContentPresenter
x:Name="contentPresenter"
Margin="0"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Focusable="False"
RecognizesAccessKey="True" />
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="true">
<Setter TargetName="border" Property="Background" Value="{DynamicResource MouseOverOverlayBackgroundBrush}" />
</Trigger>
<Trigger Property="IsPressed" Value="true">
<Setter TargetName="border" Property="Background" Value="{DynamicResource PressedOverlayBackgroundBrush}" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="TitleBarCloseButtonStyle" TargetType="Button">
<Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}" />
<Setter Property="Padding" Value="0" />
<Setter Property="WindowChrome.IsHitTestVisibleInChrome" Value="True" />
<Setter Property="IsTabStop" Value="False" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Button}">
<Border
x:Name="border"
Background="Transparent"
BorderThickness="0"
SnapsToDevicePixels="true">
<ContentPresenter
x:Name="contentPresenter"
Margin="0"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Focusable="False"
RecognizesAccessKey="True" />
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="true">
<Setter TargetName="border" Property="Background" Value="{DynamicResource MouseOverWindowCloseButtonBackgroundBrush}" />
<Setter Property="Foreground" Value="{DynamicResource MouseOverWindowCloseButtonForegroundBrush}" />
</Trigger>
<Trigger Property="IsPressed" Value="true">
<Setter TargetName="border" Property="Background" Value="{DynamicResource PressedWindowCloseButtonBackgroundBrush}" />
<Setter Property="Foreground" Value="{DynamicResource MouseOverWindowCloseButtonForegroundBrush}" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
These are buttons with stripped-down control templates to remove a lot of the extra gunk. They have triggers to change the background color on mouse over (and the foreground color in the case of the Close button). Also they set WindowChrome.IsHitTestVisibleInChrome to True, which allows them to pick up clicks even though they are in the 32px caption area we set up earlier.
The button content itself uses <Path> to draw the icons. The minimize button uses RenderOptions.EdgeMode=”Aliased” to disable anti-aliasing and make sure it renders crisply without blurring over into other pixels. I set the Stroke to pick up the Foreground color from the parent button. The Path data for the maximize/restore buttons are all set on .5 to make sure it renders cleanly at the standard 96 DPI. With whole numbers it ends up drawing on the edge of the pixel and blurring the lines. We can’t use the same “Aliased” trick here as that might cause the pixel count for different lines to change and look off at different zoom levels like 125%/150%.
Responding to button clicks
Now that we have the UI in place, we need to respond to those button clicks. I normally use databinding/MVVM, but in this case I decided to bypass the viewmodel since these are really talking directly to the window.
Event handler methods:
private void OnMinimizeButtonClick(object sender, RoutedEventArgs e)
{
this.WindowState = WindowState.Minimized;
}
private void OnMaximizeRestoreButtonClick(object sender, RoutedEventArgs e)
{
if (this.WindowState == WindowState.Maximized)
{
this.WindowState = WindowState.Normal;
}
else
{
this.WindowState = WindowState.Maximized;
}
}
private void OnCloseButtonClick(object sender, RoutedEventArgs e)
{
this.Close();
}
Helper method to refresh the maximize/restore button:
private void RefreshMaximizeRestoreButton()
{
if (this.WindowState == WindowState.Maximized)
{
this.maximizeButton.Visibility = Visibility.Collapsed;
this.restoreButton.Visibility = Visibility.Visible;
}
else
{
this.maximizeButton.Visibility = Visibility.Visible;
this.restoreButton.Visibility = Visibility.Collapsed;
}
}
This, we call in the constructor and in an event handler for Window.StateChanged:
private void Window_StateChanged(object sender, EventArgs e)
{
this.RefreshMaximizeRestoreButton();
}
This will make sure the button displays correctly no matter how the window state change is invoked.
Maximized placement
You thought we were done? Hah. Windows has other plans. You might notice that when you maximize your window, some content is getting cut off and it’s hiding your task bar. The default values it picks for maximized window placement are really weird, where it cuts off 7px of your window content and doesn’t account for task bar placement.
To fix this, we need to listen for the WM_GETMINMAXINFO WndProc message to tell our window it needs to go somewhere different when maximize. Put this in your window codebehind:
protected override void OnSourceInitialized(EventArgs e)
{
base.OnSourceInitialized(e);
((HwndSource)PresentationSource.FromVisual(this)).AddHook(HookProc);
}
public static IntPtr HookProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
{
if (msg == WM_GETMINMAXINFO)
{
// We need to tell the system what our size should be when maximized. Otherwise it will cover the whole screen,
// including the task bar.
MINMAXINFO mmi = (MINMAXINFO)Marshal.PtrToStructure(lParam, typeof(MINMAXINFO));
// Adjust the maximized size and position to fit the work area of the correct monitor
IntPtr monitor = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST);
if (monitor != IntPtr.Zero)
{
MONITORINFO monitorInfo = new MONITORINFO();
monitorInfo.cbSize = Marshal.SizeOf(typeof(MONITORINFO));
GetMonitorInfo(monitor, ref monitorInfo);
RECT rcWorkArea = monitorInfo.rcWork;
RECT rcMonitorArea = monitorInfo.rcMonitor;
mmi.ptMaxPosition.X = Math.Abs(rcWorkArea.Left - rcMonitorArea.Left);
mmi.ptMaxPosition.Y = Math.Abs(rcWorkArea.Top - rcMonitorArea.Top);
mmi.ptMaxSize.X = Math.Abs(rcWorkArea.Right - rcWorkArea.Left);
mmi.ptMaxSize.Y = Math.Abs(rcWorkArea.Bottom - rcWorkArea.Top);
}
Marshal.StructureToPtr(mmi, lParam, true);
}
return IntPtr.Zero;
}
private const int WM_GETMINMAXINFO = 0x0024;
private const uint MONITOR_DEFAULTTONEAREST = 0x00000002;
[DllImport("user32.dll")]
private static extern IntPtr MonitorFromWindow(IntPtr handle, uint flags);
[DllImport("user32.dll")]
private static extern bool GetMonitorInfo(IntPtr hMonitor, ref MONITORINFO lpmi);
[Serializable]
[StructLayout(LayoutKind.Sequential)]
public struct RECT
{
public int Left;
public int Top;
public int Right;
public int Bottom;
public RECT(int left, int top, int right, int bottom)
{
this.Left = left;
this.Top = top;
this.Right = right;
this.Bottom = bottom;
}
}
[StructLayout(LayoutKind.Sequential)]
public struct MONITORINFO
{
public int cbSize;
public RECT rcMonitor;
public RECT rcWork;
public uint dwFlags;
}
[Serializable]
[StructLayout(LayoutKind.Sequential)]
public struct POINT
{
public int X;
public int Y;
public POINT(int x, int y)
{
this.X = x;
this.Y = y;
}
}
[StructLayout(LayoutKind.Sequential)]
public struct MINMAXINFO
{
public POINT ptReserved;
public POINT ptMaxSize;
public POINT ptMaxPosition;
public POINT ptMinTrackSize;
public POINT ptMaxTrackSize;
}
When the system asks the window where it should be when it maximizes, this code will ask what monitor it’s on, then place itself in the work area of the monitor (not overlapping the task bar).
Declare ourselves DPI-aware
In order to get the WM_GETMINMAXINFO handler to behave correctly, we need to declare our app as per-monitor DPI aware to the unmanaged system.
Right click on your project and select Add -> New Item -> Application Manifest File (Windows Only) .
Add these settings to declare us as properly DPI-aware:
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitor</dpiAwareness>
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true</dpiAware>
</windowsSettings>
</application>
Otherwise with multi-monitor setups you can get maximized windows that stretch off the screen. This setting is a little confusing because WPF is by default DPI aware and normally works just fine out of the box on multiple monitors. But this is required to let the unmanaged APIs that we are hooking into know how WPF is handling things.
Window border
Finally, the window can be kind of hard to pick out when it doesn’t have a border, if it’s put against the wrong background. Let’s fix that now. Wrap your window UI in this:
<Border Style="{StaticResource WindowMainPanelStyle}">
...your UI
</Border>
This is the style:
<Style x:Key="WindowMainPanelStyle" TargetType="{x:Type Border}">
<Setter Property="BorderBrush" Value="{DynamicResource WindowBorderBrush}" />
<Setter Property="BorderThickness" Value="1" />
<Style.Triggers>
<DataTrigger Binding="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}, Path=WindowState}" Value="Maximized">
<Setter Property="BorderThickness" Value="0" />
</DataTrigger>
</Style.Triggers>
</Style>
This will remove the 1px border when the window is maximized.
Okay, now we’re actually done
At least until Windows decides they want to shake up the title bar style again.
Окна
Последнее обновление: 21.03.2016
Ключевым элементом в системе графического интерфейса в WPF является окно, которое содержит все необходимые элементы управления.
Окно в WPF представлено классом Window, который является производным от класса ContentControl. Поэтому окно является элементом управления содержимым, и как, к примеру,
кнопка, может содержать в себе один дочерний элемент. Как правило, в его качестве выступает один из элементов компоновки, например, Grid.
Класс Window привносит ряд свойств, которые позволяют настроить окно приложения:
-
AllowsTransparency: при значении
trueпозволяет установить прозрачный фон окна -
Icon: представляет иконку, которая отображается в левом верхнем углу окна и в панели задач. Если иконка не установлена,
то система будет использовать стандартную иконку по умолчанию. -
Top: устанавливает отступ окна приложения от верхней границы экрана
-
Left: устанавливает отступ окна приложения от левой границы экрана
-
ResizeMode: задает режим изменения размеров окна. Может принимать следующие значения:
-
CanMinimize: окно можно только свернуть -
NoResize: у окна нельзя изменить начальные размеры -
CanResize: у окна можно изменять размеры -
CanResizeWithGrip: в правом нижнем углу окна появляется визуализация того, что у окна можно изменять размеры
-
-
RestoreBounds: возвращает границы окна
-
ShowInTaskbar: при значении
trueиконка окна отображается на панели задач -
SizeToContent: позволяет автоматически масштабировать размеры окна в зависимости от содержимого. Может принимать
следующие значения:-
Width: автоматически масштабируется только ширина -
Height: автоматически масштабируется только высота -
WidthAndHeight: автоматически масштабируются высота и ширина -
Manual: автоматическое масштабирование отсутствует
-
-
Title: заголовок окна
-
Topmost: при значении
trueокно устанавливается поверх других окон приложения -
WindowStartupLocation: устанавливает стартовую позицию окна. Может принимать следующие значения:
-
CenterOwner: если данное окно было запущено другим окном, то данное окно позиционируется относительно центра запустившего его окна -
CenterScreen: окно помещается в центре экрана -
Manual: позиция устанавливается вручную с помощью свойств Top и Left
-
-
WindowState: состояние окна. Возможные значения:
-
Maximized: раскрыто на весь экран -
Minimized: свернуто -
Normal: стандартное состояние
-
Жизненный цикл
В процессе работы окно в WPF проходит ряд этапов жизненного цикла, которые доступны нам через обработку событий класса Window:
-
Initialized: это событие возникает при инициализации окна, когда у него устанавливаются
все свойства, но до применения к нему стилей и привязки данных. Это общее событие для всех элементов управления в WPF, поэтому
следует учитывать, что сначала возникают события вложенных элементов, а затем их контейнеров.
То есть событие Initialized окна приложения генерируется только после того, как отработает событие Initialized для всех вложенных элементов. -
Loaded: возникает после полной инициализации окна и применения к нему стилей и привязки данных.
После генерации этого события происходит визуализация элемента, и окно отображается на экране и становится видимым для пользователя -
Closing: возникает при закрытии окна
-
Closed: возникает, когда окно становится закрытым
-
Unloaded: возникает после закрытия окна при выгрузке всех связанных ресурсов из памяти
Соответственно, если нам надо выполнить некоторые действия при загрузке или при закрытии окна, мы можем обработать события Loaded и Closing/Closed. Например,
запишем в текстовый лог события жизненного цикла:
using System;
using System.Windows;
using System.Windows.Media;
using System.IO;
namespace WindowApp
{
public partial class MainWindow : Window
{
string path = "log.txt";
public MainWindow()
{
InitializeComponent();
this.Loaded += MainWindow_Loaded;
this.Closing += MainWindow_Closing;
this.Closed += MainWindow_Closed;
}
private void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
Log("Loaded");
}
private void MainWindow_Closing(object sender, System.ComponentModel.CancelEventArgs e)
{
Log("Closing");
}
private void MainWindow_Closed(object sender, EventArgs e)
{
Log("Closed");
}
private void Log(string eventName)
{
using (StreamWriter logger = new StreamWriter(path, true))
{
logger.WriteLine(DateTime.Now.ToLongTimeString() + " - " + eventName);
}
}
}
}
… 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.
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.
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:
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.
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.
… 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.
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"); } }
Wrap up
Ok so… We learned How to:
-
Inherit from the System Window
-
Customize our Window’s content template
-
Remove the Window Chrome
-
Make the Chromeless Window, actually behave as we would expect it to
-
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.








