Exploring borderless windows

Using WPF, it is possible to create windows without borders, a title bar, and the standard minimize, restore and close buttons. It is also possible to create irregular shaped windows and windows with transparent areas that display whatever lies beneath. Although it would be somewhat unconventional to make our main application window borderless, we can still take advantage of this ability.

For example, we could create a borderless window for custom message boxes, or perhaps for extended tooltips, or any other popup control that provides information to the end user. Creating borderless windows can be achieved in a few simple steps. Let's start with the basics and assume that we're adding this to our existing application framework.

In this case, we've already got our MainWindow class and need to add an additional window. As we saw in Chapter 6, Adapting the Built-In Controls, we can do this by adding a new UserControl to our project and replacing the word UserControl with the word Window, in both the XAML file and its associated code behind file. Failure to change both will result in a design time error that complains about mismatched classes.

Alternatively, we can right click on the start up project and select Add and then Window…, and then cut and paste it wherever you want it to reside. Unfortunately, Visual Studio provides no other way to add a Window control into our other projects.

Once we have our Window object, all we need to do is to set its WindowStyle property to None and its AllowsTransparency property to true. This will result in the white background of our window appearing:

<Window  
  x:Class="CompanyName.ApplicationName.Views.Controls.BorderlessWindow"    
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
  Height="100" Width="200" WindowStyle="None" AllowsTransparency="True"> 
</Window>

...

using System.Windows; 
 
namespace CompanyName.ApplicationName.Views.Controls 
{ 
  public partial class BorderlessWindow : Window 
  { 
    public BorderlessWindow() 
    { 
      InitializeComponent(); 
    } 
  } 
} 

However, while this removes the default window chrome that we are all used to and provides us with a borderless window, it also removes the standard buttons, so we are unable to close, resize, or even move the window directly. Luckily, making our window moveable is a very simple matter. We just need to add the following line of code into our window's constructor after the InitializeComponent method is called:

MouseLeftButtonDown += (o, e) => DragMove(); 

This DragMove method is declared within the Window class and enables us to click and drag the window from anywhere within its bounds. We could easily recreate the normal window functionality of only being able to move the window from the title bar by adding our own title bar and attaching this anonymous event handler to that object's MouseLeftButtonDown event instead.

If we want our borderless window to be resizable, there is a ResizeMode property in the Window class that provides us with a few options. One value that we can use with our borderless window is the CanResizeWithGrip value. This option adds a so-called resize grip, specified by a triangular pattern of dots in the bottom right corner of the window, that users can resize the window with.

If we set the ResizeMode property to this value and set the background to a color that will contrast with this resize grip, we will end with this visual output:

However, we still have no way to close the window. For this, we could add our own button, or perhaps enable the window to be closed by pressing the escape Esc key or some other key on the keyboard. Either way, whatever the trigger, closing the window is a simple matter of calling the window's Close method.

Rather than implementing a replacement window chrome, which could be easily achieved with a few borders, let's focus on developing a borderless window with an irregular shape, that we could use to popup helpful information for the users. Ordinarily, we would need to set the window's background to transparent to hide it, but we will be replacing its control template, so we don't need to do this.

For this example, we don't need a resize grip either, so let's set the ResizeMode property to NoResize. We also have no need to move this callout window by mouse, so we don't need to add the anonymous event handler that calls the DragMove method.

As this window will only offer information to the user, we should also set a few other window properties. One important property to set is the ShowInTaskbar property, which specifies whether the application icon should appear in the Windows Taskbar or not. As this window will be an integral part of our main application, we set this property to false, so that its icon will be hidden.

Another useful property for this situation is the WindowStartupLocation property, which enables the window to be positioned using the Window.Top and Window.Left properties. In this way, the callout window can be programmatically positioned on screen anywhere that it is needed. Before continuing any further, let's see the code for this window:

<Window x:Class="CompanyName.ApplicationName.Views.Controls.CalloutWindow" 
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
  xmlns:Controls=
    "clr-namespace:CompanyName.ApplicationName.Views.Controls" 
  WindowStartupLocation="Manual"> 
  <Window.Resources> 
    <Style TargetType="{x:Type Controls:CalloutWindow}"> 
      <Setter Property="ShowInTaskbar" Value="False" /> 
      <Setter Property="WindowStyle" Value="None" /> 
      <Setter Property="AllowsTransparency" Value="True" /> 
      <Setter Property="ResizeMode" Value="NoResize" /> 
      <Setter Property="Template"> 
        <Setter.Value> 
          <ControlTemplate TargetType="{x:Type Controls:CalloutWindow}"> 
            <Grid Margin="0,0,0,12"> 
              <Grid.ColumnDefinitions> 
                <ColumnDefinition Width="*" /> 
                <ColumnDefinition Width="5*" /> 
              </Grid.ColumnDefinitions> 
              <Path Grid.ColumnSpan="2"  
                Fill="{TemplateBinding Background}"  
                Stroke="{TemplateBinding BorderBrush}"  
                StrokeThickness="2" Stretch="Fill"> 
                <Path.Data> 
                  <CombinedGeometry GeometryCombineMode="Union"> 
                    <CombinedGeometry.Geometry1> 
                      <PathGeometry> 
                        <PathFigure StartPoint="0,60"> 
                          <LineSegment Point="50,45" /> 
                          <LineSegment Point="50,75" /> 
                        </PathFigure> 
                      </PathGeometry> 
                    </CombinedGeometry.Geometry1> 
                    <CombinedGeometry.Geometry2> 
                      <RectangleGeometry RadiusX="20" RadiusY="20"  
                        Rect="50,0,250,150" /> 
                    </CombinedGeometry.Geometry2> 
                  </CombinedGeometry> 
                </Path.Data> 
              </Path> 
              <ContentPresenter Grid.Column="1"  
                Content="{TemplateBinding Content}"  
                HorizontalAlignment="{TemplateBinding  
                HorizontalContentAlignment}"  
                VerticalAlignment="{TemplateBinding  
                VerticalContentAlignment}"  
                Margin="{TemplateBinding Padding}"> 
                <ContentPresenter.Resources> 
                  <Style TargetType="{x:Type TextBlock}"> 
                    <Setter Property="TextWrapping" Value="Wrap" /> 
                  </Style> 
                </ContentPresenter.Resources> 
              </ContentPresenter> 
              <Grid.Effect> 
                <DropShadowEffect Color="Black"  
                  Direction="270" ShadowDepth="7" Opacity="0.3" /> 
              </Grid.Effect> 
            </Grid> 
          </ControlTemplate> 
        </Setter.Value> 
      </Setter> 
    </Style> 
  </Window.Resources> 
</Window> 

While this example is not overly long, there is a lot to discuss here. In order to clarify the situation somewhat, let's also see the code behind before we examine this code:

using System.Windows; 
using System.Windows.Media; 
 
namespace CompanyName.ApplicationName.Views.Controls 
{ 
  public partial class CalloutWindow : Window 
  { 
    static CalloutWindow() 
    { 
      BorderBrushProperty.OverrideMetadata(typeof(CalloutWindow),  
        new FrameworkPropertyMetadata( 
        new SolidColorBrush(Color.FromArgb(255, 238, 156, 88))));       
      HorizontalContentAlignmentProperty.OverrideMetadata(  
        typeof(CalloutWindow),  
        new FrameworkPropertyMetadata(HorizontalAlignment.Center)); 
      VerticalContentAlignmentProperty.OverrideMetadata( 
        typeof(CalloutWindow),  
        new FrameworkPropertyMetadata(VerticalAlignment.Center)); 
    } 
 
    public CalloutWindow() 
    { 
      InitializeComponent(); 
    } 
 
    public new static readonly DependencyProperty BackgroundProperty =       
      DependencyProperty.Register(nameof(Background), typeof(Brush),  
      typeof(CalloutWindow), 
      new PropertyMetadata(new LinearGradientBrush(Colors.White, 
      Color.FromArgb(255, 250, 191, 143), 90))); 
 
    public new Brush Background 
    { 
      get { return (Brush)GetValue(BackgroundProperty); } 
      set { SetValue(BackgroundProperty, value); } 
    } 
  } 
} 

This code-behind file is simpler than the XAML file, so let's quickly walk through it first. We added a static constructor in order to call the OverrideMetadata method on a few pre-existing Dependency Properties. This enables us to override the default settings of these properties, and we do this in a static constructor because we want to run this code just once per class and because it is called before any other constructor or method in the class.

In this constructor, we override the metadata for the BorderBrush property, in order to set a default border color for our callout window. We do the same for both the HorizontalContentAlignment and VerticalContentAlignment properties to ensure that the window content will be centered by default. By doing this, we are re-using these existing properties.

However, we can also totally replace the pre-existing properties. As an example, we've replaced the Background property to paint our callout background. In this case, we declare our own Background property, specified by the new keyword, and set its own default brush color. We then use that to paint the background of our callout shape, although we could just as easily add another setter into our style to reuse the original Background property.

Looking at the XAML code now, we can see the WindowStartupLocation property set in the Window declaration, followed by a style in the window's Resources section. In this style, we set the aforementioned properties and define the window's control template. Inside the ControlTemplate object, we define a Grid panel. We'll return to this later, but for now, note that there is a nine pixel margin set on the bottom of the panel.

Next, note that the panel has two star-sized ColumnDefinition elements declared, one with a width of * and another with a width of 5*. If we add these together, we end with a total width of six equal divisions. This means that the first column will be one sixth of the total width of the window and the second column will take up the remaining five sixths. We will soon see why this is set as it is.

Inside the Grid panel, we first declare the Path element that is used to define the shape of our callout. We set the Grid.ColumnSpan property on it to 2, to ensure that it takes all of the space of the parent window. Next, we set our new Background property to the Fill property, so that users of our window can set Background property and have that brush paint just the background of our path.

We also set the Stroke property of the Path element to the overridden BorderBrush property and although we didn't, we could have exposed the StrokeThickness property by declaring another Dependency Property. Note that we use TemplateBinding elements to access the properties of the window, as they are the most efficient in this particular case.

Take special note of the Path.Stretch property, which we have set to Fill and defines how the shape should fill the space that it is provided with. Using this Fill value specifies that the content should fill all of the available space, rather than preserve its originally defined aspect ratio. However, if we want to preserve the aspect ratio, then we can change this property to the Uniform value instead.

The most important part of the path is found in the Path.Data section. This defines the shape of the rendered path and like our layered background example, we utilize a CombinedGeometry element here to combine two separate geometries. Unlike the previous example, here we use a GeometryCombineMode value of Union, which renders the output of both geometry shapes together.

In the CombinedGeometry.Geometry1 element, we declare a PathGeometry object with a PathFigure element that has a starting point and two LineSegment elements. Together with the starting point, these two elements form the triangular section of our callout, that points to the area on the screen that our window's information relates to. Note that this triangle is fifty pixels wide in the path.

In the CombinedGeometry.Geometry2 element, we declare a RectangleGeometry object, with its size specified by the Rect property and the size of its rounded corners being specified by the RadiusX and RadiusY properties. The rectangle is positioned fifty pixels away from the left edge and its width is two hundred and fifty pixels wide.

The overall area taken up by the rectangle and the triangle is therefore three hundred pixels. One sixth of three hundred is fifty and this is how wide the triangle in our shape is. This explains why our first Grid column is set to take one sixth of the total space.

After the Path object, we declare the ContentPresenter element that is required to output the actual content of the window and set it to be in the second column of the panel. In short, this column is used to position the ContentPresenter element directly over the rectangular section of our shape, avoiding the triangular section.

In the ContentPresenter element, we data bind several positional properties to the relevant properties of the window using TemplateBinding elements. We also data bind its Content property to the Content property of the window using another TemplateBinding element.

Note that we could have declared our UI controls directly within the Window control. However, had we done that, then we would not be able to data bind to its Content property in this way, as setting it externally would replace all of our declared XAML controls, including the ContentPresenter object. By providing a new template, we are totally overriding the default behavior of the window.

Also note that we have declared a style in the Resources section of the ContentPresenter element. This style has been declared without the x:Key directive. This is so that it will be implicitly applied to all TextBlock objects within scope, specifically to affect the TextBlock objects that the ContentPresenter element will automatically generate for string values, while not affecting others.

The style sets the TextBlock.TextWrapping property to the Wrap member of the TextWrapping enumeration, which has the effect of wrapping long text lines onto the following lines. The default setting is NoWrap, which would result in long strings not being fully displayed in our window.

Finally, we come to the end of the XAML example and find a DropShadowEffect object set as the Effect property of the Grid panel. As with all shadow effects, we set the Color property to black and the Opacity property to a value less or equal to 0.5. The Direction property is set to 270, which produces a shadow that lies directly underneath our callout shape.

Note that we set the ShadowDepth property to a value of 7. Now, do you remember the bottom margin that was set on the grid? That was set to a value just above this value and was to ensure that enough space was left in the window to display our shadow underneath our callout shape. Without this, the shadow would sit outside the bounding box of the window and not be displayed.

If we had set a different value for the Direction property, then we would need to adjust the Grid panel's margin to ensure that it left enough space around the window to display the shadow in its new location. Let's now take a look at how we could use our new window:

CalloutWindow calloutWindow = new CalloutWindow(); 
calloutWindow.Width = 225; calloutWindow.Height = 120; calloutWindow.FontSize = 18; calloutWindow.Padding = new Thickness(20); calloutWindow.Content = "Please fill in the first line of your address."; calloutWindow.Show();

Running this code from a suitable location would result in the following rendered output:

In our window-showing code, we set a string to the Content property of the window. However, this property is of type object, so we can add any object as its value. In the same way that we set our View Model instances to the Content property of a ContentControl earlier in this book, we can also do that with our window.

Given a suitable DataTemplate that defines some UI for a particular custom object type, we could set an instance of that object to our window's Content property and have the controls from that template rendered within our callout window, so we are not restricted to only using type string for content here. Let's use a previous example:

calloutWindow.DataContext = new UsersViewModel(); 

With a few slight adjustments to our calloutWindow dimension properties, we would see this: