How to create a custom window in WPF
Posted: March 16, 2013 Filed under: Custom Controls, WPF | Tags: .NET, C#, Custom Controls, Visual Studio, WPF 90 CommentsThis introductory post will provide a walkthrough on how to create your own custom looking window control with resize, drag, minimize, restore and close functionality and how to use it in your WPF applications.
The solution includes two Visual Studio projects, the first one a class library where the control itself is implemented and the second one a sample WPF application which will have a reference to the class library and use the custom window control as its main window.
Get started
Start by creating a new C# class library in Visual Studio (File->New Project) and give it a suitable name like Mm.Wpf.Controls.
Add references
After removing the automatically generated Class1.cs from the project the first thing to do is to add some references. We will need a reference to the PresentationFramework.dll where the out-of-the-box System.Windows.Window class, which our custom window class will inherit from, is implemented. We also need to add a reference to PresentationCore.dll where the RoutedEventArgs class lives, System.Xaml.dll and WindowsBase.dll. You add a reference in Visual Studio by choosing “Add reference” from the Project menu or by right-clicking the project in the solution explorer and the required DLLs should be found under the .NET tab.
Implement the control
The next step is to add a new class called CustomWindow.cs to the project by right-clicking the project name in the solution explorer and choose Add->Class. The class will be public and inherit from the default WPF window class (Sytem.Windows.Window). By extending the default Window class we can use its built-in functionality for minimizing, restoring and closing the window. We add the three click event handlers shown below to take care of this.
using System.Windows; namespace Mm.Wpf.Controls { public class CustomWindow : Window { #region Click events protected void MinimizeClick(object sender, RoutedEventArgs e) { WindowState = WindowState.Minimized; } protected void RestoreClick(object sender, RoutedEventArgs e) { WindowState = (WindowState == WindowState.Normal) ? WindowState.Maximized : WindowState.Normal; } protected void CloseClick(object sender, RoutedEventArgs e) { Close(); } #endregion } }
When you create a custom control in WPF you define a template for it in XAML. The default style must be located in a folder called “Themes” at the root of the class library project and for our custom window control to be able to find its default style we add a static constructor where the default value of the DefaultStyleKey property is set to the type object of our class.
static CustomWindow() { DefaultStyleKeyProperty.OverrideMetadata(typeof(CustomWindow), new FrameworkPropertyMetadata(typeof(CustomWindow))); }
To define the style for the window we then add a “Themes” folder to the project. Inside this folder you can add different templates for each Windows theme but since theming is out of the scope of this article we will only add a so called fallback style. This style must be located in (or defined in a file referenced from) a file called “Generic.xaml” in the “Themes” folder.
Since the project template is a generic class library you probably won’t find the option to add a WPF Resource dictionary when right-clicking the newly created folder and choosing Add. Instead, choose to add a simple text file, name it “Generic.xaml” and give it the below content.
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:Mm.Wpf.Controls"> <!-- Button style --> <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> <!-- Window style --> <Style TargetType="{x:Type local:CustomWindow}"> <Setter Property="WindowStyle" Value="None"/> <Setter Property="ResizeMode" Value="NoResize"/> <Setter Property="Background" Value="White"/> <Setter Property="BorderThickness" Value="1"/> <Setter Property="BorderBrush" Value="Silver"/> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="{x:Type local:CustomWindow}"> <Border BorderThickness="{TemplateBinding BorderThickness}" BorderBrush="{TemplateBinding BorderBrush}"> <Grid> <Grid> <Grid.RowDefinitions> <RowDefinition Height="Auto"/> <RowDefinition /> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition /> <ColumnDefinition Width="Auto"/> </Grid.ColumnDefinitions> <Rectangle x:Name="moveRectangle" Fill="Transparent" Grid.Row="0" Grid.Column="0"/> <StackPanel Grid.Row="0" Grid.Column="1" Orientation="Horizontal"> <Button x:Name="minimizeButton" Style="{StaticResource WindowButtonStyle}" Content="0" /> <Button x:Name="restoreButton" Style="{StaticResource WindowButtonStyle}" Content="1" /> <Button x:Name="closeButton" Style="{StaticResource WindowButtonStyle}" Content="r" /> </StackPanel> <Grid Background="{TemplateBinding Background}" Grid.Row="1" Grid.ColumnSpan="2" Margin="5,5,5,5"> <AdornerDecorator> <ContentPresenter/> </AdornerDecorator> </Grid> </Grid> <! -- Resize grid to be inserted here --> </Grid> </Border> </ControlTemplate> </Setter.Value> </Setter> </Style> </ResourceDictionary>
Note that the buttons for minimizing, restoring and closing the window have their own template defined above the actual custom window template. Also these buttons have been assigned a name by the x:Name attribute for us to be able to find them in code and hook up their click event handlers. This is done by overriding the OnApplyTemplate in our CustomWindow class.
public override void OnApplyTemplate() { Button minimizeButton = GetTemplateChild("minimizeButton") as Button; if (minimizeButton != null) minimizeButton.Click += MinimizeClick; Button restoreButton = GetTemplateChild("restoreButton") as Button; if (restoreButton != null) restoreButton.Click += RestoreClick; Button closeButton = GetTemplateChild("closeButton") as Button; if (closeButton != null) closeButton.Click += CloseClick; base.OnApplyTemplate(); }
Make it draggable
At this stage our custom window is almost (the ThemeAttribute is missing) ready to be used in a WPF application but it cannot yet be dragged around on the screen nor resized. You may have noticed that the template above contains a rectangle named “moveRectangle” and this one will serve as a ‘handle’ to drag our window. We add a PreviewMouseDown event handler for the Rectangle in the OnApplyTemplate method and implement it as below.
private void moveRectangle_PreviewMouseDown(object sender, MouseButtonEventArgs e) { if (Mouse.LeftButton == MouseButtonState.Pressed) DragMove(); }
Make it resizable
To make the window resizable requires a bit more effort. We start by adding a bunch of invisible (Fill=”Transparent”) rectangles, one for each side of the window and another one for each corner of the window, to our XAML defined template. These rectangles will serve as the window’s edges and we will show the resizing cursors when the mouse moves over these. It is accomplished by implementing the MouseMove event for each of the rectangles. Note that the rectangle’s name decides which type of cursor to display.
<Grid x:Name="resizeGrid"> <Rectangle Stroke="{x:Null}" Fill="Transparent" VerticalAlignment="Top" Height="5" x:Name="top" Margin="5,0,5,0" /> <Rectangle Stroke="{x:Null}" Fill="Transparent" x:Name="bottom" Height="5" VerticalAlignment="Bottom" Margin="5,0,5,0" /> <Rectangle Stroke="{x:Null}" Fill="Transparent" HorizontalAlignment="Left" Margin="0,5,0,5" Width="5" x:Name="left"/> <Rectangle Stroke="{x:Null}" Fill="Transparent" Margin="0,5,0,5" Width="5" HorizontalAlignment="Right" x:Name="right" /> <Rectangle Stroke="{x:Null}" Fill="Transparent" HorizontalAlignment="Left" VerticalAlignment="Bottom" Width="5" Height="5" x:Name="bottomLeft" /> <Rectangle Stroke="{x:Null}" Fill="Transparent" VerticalAlignment="Bottom" Height="5" Width="5" HorizontalAlignment="Right" x:Name="bottomRight" /> <Rectangle Stroke="{x:Null}" Fill="Transparent" HorizontalAlignment="Right" Width="5" Height="5" VerticalAlignment="Top" x:Name="topRight" /> <Rectangle Stroke="{x:Null}" Fill="Transparent" HorizontalAlignment="Left" Width="6" VerticalAlignment="Top" Height="5" x:Name="topLeft" /> </Grid>
public override void OnApplyTemplate() { /* omitted */ Grid resizeGrid = GetTemplateChild("resizeGrid") as Grid; if (resizeGrid != null) { foreach (UIElement element in resizeGrid.Children) { Rectangle resizeRectangle = element as Rectangle; if (resizeRectangle != null) { resizeRectangle.PreviewMouseDown += ResizeRectangle_PreviewMouseDown; resizeRectangle.MouseMove += ResizeRectangle_MouseMove; } } } base.OnApplyTemplate(); } protected void ResizeRectangle_MouseMove(Object sender, MouseEventArgs e) { Rectangle rectangle = sender as Rectangle; switch (rectangle.Name) { case "top": Cursor = Cursors.SizeNS; break; case "bottom": Cursor = Cursors.SizeNS; break; case "left": Cursor = Cursors.SizeWE; break; case "right": Cursor = Cursors.SizeWE; break; case "topLeft": Cursor = Cursors.SizeNWSE; break; case "topRight": Cursor = Cursors.SizeNESW; break; case "bottomLeft": Cursor = Cursors.SizeNESW; break; case "bottomRight": Cursor = Cursors.SizeNWSE; break; default: break; } }
We also need to remember to reset the cursor to its default state when the mouse moves from one of the edges into the content area and for this we add a constructor to our custom window class and an event handler to the Window’s PreviewMouseMove event.
public CustomWindow() : base() { PreviewMouseMove += OnPreviewMouseMove; } protected void OnPreviewMouseMove(object sender, MouseEventArgs e) { if (Mouse.LeftButton != MouseButtonState.Pressed) Cursor = Cursors.Arrow; }
To implement the actual resizing behavior in the PreviewMouseDown event handler for each of the edge rectangles we then make use of the platform invocation services (PInvoke) to call the unmanaged Windows API SendMessage method.
[DllImport("user32.dll", CharSet = CharSet.Auto)] private static extern IntPtr SendMessage(IntPtr hWnd, UInt32 msg, IntPtr wParam, IntPtr lParam); protected void ResizeRectangle_PreviewMouseDown(object sender, MouseButtonEventArgs e) { Rectangle rectangle = sender as Rectangle; switch (rectangle.Name) { case "top": Cursor = Cursors.SizeNS; ResizeWindow(ResizeDirection.Top); break; case "bottom": Cursor = Cursors.SizeNS; ResizeWindow(ResizeDirection.Bottom); break; case "left": Cursor = Cursors.SizeWE; ResizeWindow(ResizeDirection.Left); break; case "right": Cursor = Cursors.SizeWE; ResizeWindow(ResizeDirection.Right); break; case "topLeft": Cursor = Cursors.SizeNWSE; ResizeWindow(ResizeDirection.TopLeft); break; case "topRight": Cursor = Cursors.SizeNESW; ResizeWindow(ResizeDirection.TopRight); break; case "bottomLeft": Cursor = Cursors.SizeNESW; ResizeWindow(ResizeDirection.BottomLeft); break; case "bottomRight": Cursor = Cursors.SizeNWSE; ResizeWindow(ResizeDirection.BottomRight); break; default: break; } } private void ResizeWindow(ResizeDirection direction) { SendMessage(_hwndSource.Handle, 0x112, (IntPtr)(61440 + direction), IntPtr.Zero); } private enum ResizeDirection { Left = 1, Right = 2, Top = 3, TopLeft = 4, TopRight = 5, Bottom = 6, BottomLeft = 7, BottomRight = 8, }
For the above ResizeWindow wrapper method to compile we need to add a member variable of type HwndSource to represent our window and provide access to its window handle. The variable is initialized in the Window.SourceInitialized event.
private HwndSource _hwndSource; protected override void OnInitialized(EventArgs e) { SourceInitialized += OnSourceInitialized; base.OnInitialized(e); } private void OnSourceInitialized(object sender, EventArgs e) { _hwndSource = (HwndSource)PresentationSource.FromVisual(this); }
At this point the class library should compile just fine if you have included the following required using statements at the top of the CustomWindow.cs file.
using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Shapes; using System; using System.Windows.Interop; using System.Runtime.InteropServices;
The last thing to do is to add a ThemeInfo attribute to our assembly to specify where WPF should look for the possible theme dictionaries and the generic dictionary. As we don’t have any specific theme styles and our Generic.xaml is located in the current assembly we add the following to the automatically generated AssemblyInfo.cs located under the Properties folder at the root of the project.
[assembly: ThemeInfo(ResourceDictionaryLocation.None, ResourceDictionaryLocation.SourceAssembly)]
Use the control in a WPF application
The CustomWindow control is now ready be used. We add a new WPF application to our solution by choosing File->Add->New project on the menu bar.
We then set the newly added project to the startup project by right-clicking on it in the solution explorer and choosing “Set as StartUp project”. If you run the solution at this point you should see an ordinary empty window popup on your screen.
To be able to replace this window with our custom style window, we first add a reference to the Mm.Wpf.Controls class library by right-clicking the WPF application project and choosing “Add reference” and the “Projects” tab.
Then we simply edit the MainWindow class, both the code-behind C# and the XAML, to inherit from our custom CustomWindow class instead of the default Window class as shown below.
namespace WpfApplication { /// <summary> /// Interaction logic for MainWindow.xaml /// </summary> public partial class MainWindow : CustomWindow { public MainWindow() { InitializeComponent(); } } }
<control:CustomWindow x:Class="WpfApplication.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:control="clr-namespace:Mm.Wpf.Controls;assembly=Mm.Wpf.Controls" Title="MainWindow" Height="350" Width="525"> <Grid> <TextBlock Text="Here goes the content!"/> </Grid> </control:CustomWindow>
The application’s main window should now show up with our custom style and you should be able to drag it around on the screen, resize it, minimize it, restore it and close it just like you would with the default window.
Hi Swarnava,
Are you using VB6 or VB.net?
There is a bug:
The window doesn’t move. It resizes, but does not move.
example move:
window_1.DragMove();
Hey, I’m in search for a solution that when going maximized not covering taskbar.. tried your example but still no luck :/ found some solutions but placing taskbar in top of screen always gets covered.
Hi Joel,
Please refer to my reply in the following thread for an example of how you could handle the SourceInitialized event of the window and call some interop code to hopefully make it work as expected: https://social.msdn.microsoft.com/Forums/sqlserver/en-US/3a5266e0-c555-425e-a979-0d74061540e8/my-window-overlaps-taskbar-when-windowsstyle-none-and-transparency-true-wpf?forum=wpf
Works like a dream, many thanks!!
Got a hint on Windows resize feature when mouse touches screen edges too? Some way to tap in to it or is it manual work all the way?
Thanks in advance!
That is a great solution. one thing missing is the toggle between restore and minimize when clicking on task bar. I am still unable to find any good solution to that. any idea?
Hi, it works also for me.
But how can I add the Window Title to the xaml File?
Thanks Christoph
Ok, in OnApplyTemplate you can add the folowing:
Label WindowTitle = GetTemplateChild(“WindowTitle”) as Label;
WindowTitle.Content = Title;
And you have to add a label names WindowTitle to your xaml
When I tried to create this project using .NET 4.5, I was getting a type “x:Type” not found. Are you missing a reference.
Adding System.Xaml fixed this issue.
Very nice. There is another toolkit, LinsUIWPF, which is very useful for developers who will like their application is customizable. If you are interested in it, the link is here, http://goldcing.blogspot.com/2012/07/introduction-to-linsuiwpf-suite.html.
“CustomWindow” does not exist in the namespace “clr-namespace:Mm.Wpf.Controls”
i m getting this error Pls Help me
Hi Ashwanikumar singh,
Please take a look at the sample markup again. You need to specify the name of the assembly where the CustomWindow class is defined:
xmlns:control=”clr-namespace:Mm.Wpf.Controls;assembly=Mm.Wpf.Controls”
Thanks for the reply Sir ….
Dear sir i am using .net 4.5 I tried all the way but i am still getting 5 errors.. Sir, I am beginner in WPF please help me I am sending my code in notepad I have followed all the steps you mentioned on the site Of “How to create Custom window ” I am not able to solve my errors please Help me out
Thanks for the tutorial Magnus.
I’m getting the same error as Ashwanikumar singh posted.
The Wpf.Controls assembly is showing in the references.
Still I get “CustomWindow” does not exist in the namespace “clr-namespace:Wpf.Controls;assembly=Wpf.Controls”
(I chose to name mine differently)
Thanks for this tutorial!
@CJohnson (dragging issue):
If moving the window doesn’t work, add the following code to the OnApplyTemplate method of the CustomWindow class:
Rectangle moveRect = GetTemplateChild(“moveRectangle”) as Rectangle;
if(moveRect != null)
moveRect.PreviewMouseDown += moveRectangle_PreviewMouseDown;
where is the source code?
Thought I’d just update on my own problem. My issue was related to changing the name of the assembly and I missed a name change.
Hi, I tried the same thing, i could successfully build and run the application. But I’ve got some problem with the visual. The visual is proper in visualstudio designer, but when i run it in my system, it is totally different. Please suggest what I should do
Hi, Everything is working fine, except for black borders. Does anyone have a solution as to how to get rid of them?
Hey I face the same problem just like Ashwani and Chris. Any luck on that?
Thank you so much for this tutorial. I am facing a problem with this, please help me.
This tutorial work fine bot the Form (main window) come with Black form border. For that when i move my cursor at the cross button, then only button show otherwise main form border style property is totally black.
Please help me to resolve.
Thanks in advance.
Nice tutorial :)
If you have black rectangle problems as I do, you should change the following in the “Window Style”:
I think that as everything is transparent, Windows puts black as default background.
Hi i am getting the Black screen, Help me
I was able to get this to work in VS2013, Net 4.5. I did however, as some suggested, create the project as a WPF Custom Control Library.
The “black title bar” issue I fixed by adding Background=”White” to the Border style of the window template:
… Border BorderThickness=”{TemplateBinding BorderThickness}” BorderBrush=”{TemplateBinding BorderBrush}” Background=”White”
To prevent the window from being resized below a given value, just set the MinWidth and MinHeight properties in the constructor.
Hi Claire, Are u able to see ur screen the way it is described in this tutorial?
Could u please share ur code by email me at sanyamra5@gmail.com
@sanyam:
Done.
Claire, sanyam. Could you please send source code to chertykto@gmail.com
Thanks
@alex:
I put a copy of the solution on my OneDrive account:
https://onedrive.live.com/?id=B7DC65465093599B!105&cid=B7DC65465093599B&group=0
Anybody should be able to download it from there.
This being said, people can also take advantage of the WindowChrome class which allows you to customize a window without having to re-implement the standard functionalities:
https://msdn.microsoft.com/en-us/library/system.windows.shell.windowchrome(v=vs.110).aspx
This class is available only in .NET 4.5+ though (for prior versions you may still be able to download it as a separate DLL).
Thank you very much
Thanks a lot, works like a charm!!
On .NET 4.5 there’s no need to write code to move, neither resize. There’s a WindowChrome extension that’s do all de job. Use properties “CaptionHeight” (Height from top that’s accepts moving) and “BorderThickness” to resize operations.
For everyone who was having the issue with the black border, Claire’s solution works fine, however if you want the border to change along with the background color of the window, the Background property of the Border control should be set to
Background=”{TemplateBinding Background}”
instead of
Background=”White”
this way when you change the Background of your window the border will change along with it.
can we set button background as icon or image instead of color?
Followed to the T. Still shows a normal window when run
Only issue I have is after adding the Title it works fine After it applies the template but I want to be able to change the title during run time or set it from a bind but setting it in the OnApplyTemplate routine won’t allow for this.
@Claire’s comment saved my day.
In .NET 4.5 we get WindowChrome class which easily allows window customization.
https://msdn.microsoft.com/en-us/library/system.windows.shell.windowchrome(v=vs.110)
Hi Magnus,
Hi Michael,
Thanks for the post. I followed your post but I am having some troubles with modifying the control style with implicit styles.
I have two custom controls that represent a custom base window (like above) and a base page. I have added default styles with default templates to generic.xaml and the default styles get applied correctly. In order to make the controls customizeable, I used template bindings to existing properties like Background, and to a few custom dependency properties. To make sure the template bindings work, I made implicit styles and added them to the merged dictionaries in my app.xaml file. However, the values in the implicit styles are never applied to the controls defined by the default templates. The confusing thing is if I look at the live visual tree, I can see that the implicit style is being applied to the page or the window, however, the children controls defined by the default template are not updating their template-bound values based on these implicit styles. The only way I have been able to get the implicit styles to work is by doing the following in the constructors:
var style = this.TryFindResource(typeof(BasePageView)) as Style;
if (style != null)
this.Style = style;
However this seems very hacky and completely unnecessary based on my understanding of implicit styling and template bindings. Do you have any idea what I might be missing?
I first got the idea for the above from https://social.msdn.microsoft.com/Forums/vstudio/en-US/7c119e29-e7e3-4e9a-b732-7ee0e050c0dd/why-a-window-style-couldnt-apply-to-wpf-windows-automatically-without-xkey-property?forum=wpf. But the behavior doesn’t seem limited to windows.
One comment: if your custom control expects a control on its template you should use the name “PART_ResizeGrid” and annotate that in your custom control class like so
[TemplatePart(Name = “PART_ResizeGrid”, Type = typeof(Grid))]
Hey there,
Thanks for the info.
Just fyi, don’t know if this was true when you made this blog, but you needn’t go through so much trouble to change the cursors. WPF has a cursor property on most (if not all) elements in Xaml.
For example, on the border rectangles, you can just set the cursor to “SizeNS”, etc in xaml and it will switch back and fourth automatically.
Thanks again!