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

WPF Non-Client Area Design Techniques For Custom Window Frames

Rate me:
Please Sign up or sign in to vote.
4.81/5 (39 votes)
10 Oct 2008CPOL8 min read 226.2K   17.8K   167   44
A 100% WPF solution to creating custom Window frames of any shape, supporting all standard frame behaviors

Introduction

Microsoft has reported that a frequent request from WPF developers is for more information on how to customize the NON-CLIENT area of a Window, to create custom Window frames. Published demos are often limited to supporting only rectangular windows, require Win32 Interop, or pollute client-side markup with frame-related details...

This article demonstrates a 100% WPF solution to support frames of any shape, with expected min/max/close button behaviors, mouse detection of irregular sizing borders with standard six-directional sizing, and information on how to maintain compatibility with Expression Blend (which is essential for designing complex "wrapping" and other non-trivial visual effects).

XFrameStyle_RoundMonitor.png

XFrameStyle_OfficeButton.png

Customization is supported through a "CustomWindow" base class, fully encapsulated in a WPF Custom Control Library, insulating the client (below) from all frame-related details.

XClientAreaDefault2.png

The attached "CustomFrameStyle" project features a minimal set of code, which might serve as an appropriate base for real world projects that require customization of the non-client area. The more complex "DynamicFrameSkinning" project is primarily a platform to demonstrate some useful related techniques that we leverage to dynamically switch "skins" based on selections made in the client.

The Basic Architecture

To apply a custom frame, simply derive your standard WPF client Window from the CustomWindow base class using the syntax below. Everything else happens inside the custom control library.

XML
<custom:CustomWindow x:Class="ClientUI.MainWindow"
   xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
   xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
   xmlns:custom="clr-namespace:CustomControls;assembly=CustomControls"

   etc...

<custom:CustomWindow />

The main template for our custom control (in Themes\resCustomWindow.xaml) simply sets properties to remove the default frame appearance, and defines an embedded ContentControl which is styled to provide the desired custom look.

XML
<Style TargetType="{x:Type local:CustomWindow}">
   
    <!-- Remove default frame appearance -->
    <Setter Property="WindowStyle" Value="None" />
    <Setter Property="AllowsTransparency" Value="True" />

    <Setter Property="Template" />
        <Setter.Value />
            <ControlTemplate TargetType="{x:Type local:CustomWindow" />

                <ContentControl x:Name="PART_CustomFrame" />

                    <ContentPresenter x:name="ClientArea" />

                </ContentControl />

            </ControlTemplate />
        </Setter.Value />
    </Setter />
</Style>

If your frame is nearly rectangular, you may want to define it entirely inside this template, but custom frames are often specifically intended NOT to be rectangular, and our demo switches between two styles, identified by ComponentResourceKeys "Norm" and "Max", to support normal and maximized Window states. Here is a view of the "Max" style (resCustomWindowMaximized.xaml), opened for editing in Expression Blend.

XBlend_CustomWndMax.png

Switching Styles

Performing a style switch involves three steps, carried out in CustomWindow.cs.

First, we store a reference to the element we intend to style.

C#
_customFrame = (Control)Template.FindName( "PART_CustomFrame", this );

Next, we apply the appropriate "Norm" or "Max" style...

C#
Style style = (Style)TryFindResource
	( new ComponentResourceKey( typeof(CustomWindow), strStyleId ) );

if( style != null )
    _customFrame.Style = style;

Finally, if the Window state is NOT maximized, we must re-attach event handlers in the new template, to restore support for standard behaviors, such as dragging the TitleBar, mouse interaction with sizing borders, etc. Note that attempts to detect template "PART"s immediately after setting the style will fail, and we must hook up our handlers in a separate method that we call asynchronously.

C#
if( WindowState == WindowState.Normal )
    new Thread( () => { UpdateFrameBehavior(); } ).Start();

Inside UpdateFrameBehavior, we marshall processing back to the UI thread, specifying DispatcherPriority.ContextIdle to ensure all background processing has completed.

C#
Dispatcher.BeginInvoke( DispatcherPriority.ContextIdle, (ThreadStart)delegate
{
    // (We're back on the UI thread and can hook up our handlers...)
});

To reference the active TitleBar and sizing borders, we must call FindName on the current frame's template (not on CustomWindow's own template). 

C#
FrameworkElement titleBar = 
    (FrameworkElement)_customFrame.Template.FindName
	( "PART_TitleBar", _customFrame );

The Role of Expression Blend

It's unlikely you'd resort to creating custom Window frames unless you intended to make significant changes, possibly involving Paths and non-rectangular regions, that are almost impossible to design without a tool like Expression Blend.

To edit an existing frame style in our "CustomFrameStyle" project, open the top level *.sln file, or CustomControls.csproj in Blend (I'm currently using the June 2.5 preview version). Under the Project tab, double-click on any XAML file in the Themes folder, click on the Resources tab, expand the section for the style you want to edit (for example, resCustomWindowNormal.xaml), and double-click on the ComponentResourceKey reference for the associated style. Because the client area is empty at design time, you will see something like this:

XBlend_ZeroClientAreaSize.png

Click on the XAML tab and edit the top-level grid to provide some temporary dimensions (don't forget to delete these before running, or the frame will lose its ability to auto-size).

XML
<Grid Margin="8" Width="500" Height="400" >

Switching back to the Design tab and double-clicking on the ComponentResourceKey style reference once again, will generate a more useful design surface.

XBlend_ClientAreaTempSize.png

Finally, under the "Objects and Timelines" section, right-click on "Style", and select "Edit Template".

XBlend_EditTemplate.png

Some published non-client area customization demos place invisible rectangles over the left, top, right, and bottom sides of a Window to create sizing borders, but this won't work for Frames with rounded or irregularly-shaped borders.  Ideally, we'd like to be able to trace a Path around the entire frame, but such unbroken outline paths become distorted as the frame is resized (You can verify this by creating two identical overlapping rectangles with rounded corners in an empty project in Blend, converting one of the rectangles to a path, and watching the edges grow out-of-synch as the Window is resized).

XPathBorderMisMatch.png

To address this, we trace six SEPARATE overlapping paths in Blend, providing each Path segment with the appropriate sizing cursor, with no fill, and a transparent stroke 10 pixels wide. Mouse over detection will not be pixel perfect, but some experimentation with Blend's pen tool can get us very close. Note that we define sizing border segments in a separate grid, to avoid accidentally corrupting row or column dimensions auto-sized on contained design elements.

To edit existing border segments, expand the resource resColorsCustomWindow.xaml, and temporarily set brushSizingRectangleOutline to any bright color (remembering to set the value back to Transparent when editing is complete).

XBlendResColors.png

XBlend_SizingBorders.png

If you draw a NEW path segment, select the segment, go to the Properties tab, expand the Miscellaneous section, and set the path Style to ResizePathSegmentStyle:

XBlend_SetPathStyle.png

You'll need to click on the XAML tab and manually edit the generated XAML to eliminate conflicts with default Path properties, using existing border path definitions as a guide. Be sure to change the XAML style reference on your new path from DynamicResource to StaticResource, or it will remain invisible. It's usually much easier just to copy an existing path segment and edit its values with Blend tools.

Managing Source Volume and Complexity

In addition to our custom frame control, our custom control library also defines a custom button class, "XSButton", featuring glass and diffuse highlights, inner glow, and other effects (detailed in my other project, "WPF Custom Controls - Without the Pain" ). Because most custom control libraries can eventually be expected to contain MANY different controls, we've taken explicit steps to manage growing code complexity by encapsulating control-specific markup in separate ResourceDictionaries rather than dumping everything in Generic.xaml, and even grouping all button-related value converters in a single file.

Similarly, we've tried to keep markup defining actual layout separate from styles APPLIED to that layout -- although its not always clear where to draw that line. It is especially important to define colors separately from layouts and styles, since colors tend to change frequently over the course of a project, must often be changed together as a group (as designers adjust the "palette"), and individual color values are very difficult to pick out of large volumes of XAML.

Finally, some people have been mislead to believe that designers can make indiscriminate changes in Blend, while developers make indiscriminate changes to the shared project source in Visual Studio, and that as long as everything works in one environment, it will work in the other... and that designers and developers can work in harmony while remaining largely ignorant of each other's area of concern. Many teams, including some inside Microsoft, are finding that this is simply not true.

MCS User Experience Team UK - Lessons learned in teaming devs and designers on actual WPF projects

At the very least, developers need to be aware that Blend's simple point-and-click interface encourages generation of massive amounts of convoluted XAML, which must be refactored early and often if you expect to have any hope of maintaining the source.

Dynamic Skinning Project

There is another common technique for "skinning" a WPF UI, which we use in our second demo, consisting of switching out the currently-loaded ResourceDictionary.

The client UI in the "DynamicFrameSkinning" project contains a ComboBox which is data bound to "Layout" enum values defined in our custom control library, retrieved through an ObjectDataProvider.

XML
<ObjectDataProvider MethodName="GetValues" 
	ObjectType="{x:Type sys:Enum}" x:Key="LayoutOptions" >
    <ObjectDataProvider.MethodParameters>
        <x:Type TypeName="custom:Layout" />
    </ObjectDataProvider.MethodParameters>
</ObjectDataProvider>

When our CustomWindow-derived client sets the current "Layout" property, instead of setting a style, we load the appropriate ResourceDictionary before carrying out the same steps as in our original demo.

C#
private void UpdateFrameAppearance( string strResourceFile )
{
    ResourceDictionary currentResources = new ResourceDictionary();

    foreach( DictionaryEntry entry in Resources )
    {
        currentResources[entry.Key] = entry.Value;
    }

    Uri uri = new Uri( strResourceFile, UriKind.Relative );
    ResourceDictionary fromFile = 
	Application.LoadComponent( uri ) as ResourceDictionary;
    currentResources.MergedDictionaries.Add( fromFile );

    Resources = currentResources;
}

Notice that the main CustomWindow template now assigns a single specific style as a DynamicResource, and we no longer need to identify our styles using ComponentResourceKeys.

XML
<ContentControl x:Name="PART_CustomFrame" Style="{DynamicResource FrameLayout}" >

Notice also that the "OfficeButton" frame style is nearly rectangular, so there is no need to define a separate style to support the maximized Window state.

Limitations and Enhancements

Any design constructed from irregular Paths will suffer some amount of distortion as the Window is resized, and the caption and status areas of our RoundMonitor frame style were purposely intended to represent extreme examples of this issue.  A production-quality implementation would want to add some additional refinement to offset this, and possible strategies to mitigate the effects include maintaining the design-time aspect ratio during sizing, placing the entire view in a ViewBox (which causes contained controls and their displayed text to grow and shrink), and relying on more regular shapes such as ellipses and rounded rectangles.  Another approach involves converting the entire assembled set of paths and shapes into a single background image, but this makes it more difficult to position the client area, and makes accurate detection of TitleBar boundaries impossible.

The code we use to handle sizing border mouse events in our demos is also a little crude, and lacks error-checking for situations like dragging a frame past the point of zero size.

Even with these limitations, the examples and techniques shown here should enable any developer who really needs to deliver a highly-polished custom Window frame solution, using a minimum of clean, maintainable, extensible WPF code and mark up.

Other Projects by Andy L.

History

  • 10th October, 2008: Initial post

License

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


Written By
United States United States
I started out writing applications in C for a software development shop in Japan, did alot of work in C++/MFC, and some DirectX, at two Silicon Valley startups, and have been working with C# and Windows Forms ever since the release of .Net 1.0. Although I took a couple intro. to CS courses at CAL (Berkeley), my degree was actually in Asian Studies, and I learned to program "in the trenches". I was also the .Net evangelist at my most recent company, authoring internal white papers on .Net, sending out a weekly ".Net FYI" e-mail, conducting .Net training, and sweating the truth out of job candidates who claimed to have .Net experience (You'd be amazed at the number of Silicon Valley engineers who list "three years of C#" on their resumes, who are unable to explain how to hook up a simple event handler, or identify basic terms like "reflection", "attributes" -- or "Richter" and "Löwy").

Comments and Discussions

 
SuggestionValidations don't work Pin
Member 1039679421-May-17 23:04
professionalMember 1039679421-May-17 23:04 
QuestionChanging it back to the regular window during runtime Pin
Member 107610455-Apr-15 18:56
Member 107610455-Apr-15 18:56 
Questionhelp translating a nice project to wpf Pin
lagartus9-Oct-12 11:10
lagartus9-Oct-12 11:10 
Generalvery nice article Pin
Aarti Meswania26-Sep-12 20:13
Aarti Meswania26-Sep-12 20:13 
QuestionStatusBar text Pin
Anarchi9-May-12 3:26
Anarchi9-May-12 3:26 
Questionhow to set ResizeMode to noResize Pin
denu102413-Dec-11 16:24
denu102413-Dec-11 16:24 
BugMaximize by dragging to the top of the screen Pin
bkovacic11-Oct-11 4:44
bkovacic11-Oct-11 4:44 
GeneralRe: Maximize by dragging to the top of the screen Pin
bkovacic11-Oct-11 5:06
bkovacic11-Oct-11 5:06 
BugAdding GridSplitter - Drag=CRASH! :( Pin
waltlounsbury4-Sep-11 8:39
waltlounsbury4-Sep-11 8:39 
GeneralFIXED! - MS Bug! Pin
waltlounsbury4-Sep-11 9:09
waltlounsbury4-Sep-11 9:09 
QuestionCombobox issue Pin
pchak23-Jun-11 7:11
pchak23-Jun-11 7:11 
GeneralBug, I cannot use any ActiveX object in this custom window. Pin
Jirapat3-Oct-10 17:29
Jirapat3-Oct-10 17:29 
GeneralRe: Bug, I cannot use any ActiveX object in this custom window. Pin
savarex24-Nov-11 8:20
savarex24-Nov-11 8:20 
I fixed it setting AllowTransparency = false near

in resCustomWindow.xaml

This means that you can use path in normal mode window.

Bye,
Ema
AnswerBUG (and fix): When Resizing windows the app might crash Pin
mrfrostfire11-Aug-10 4:37
mrfrostfire11-Aug-10 4:37 
QuestionHow do you turn it off? Pin
SpaceCowboy85016-Jul-10 10:38
SpaceCowboy85016-Jul-10 10:38 
GeneralSizeToContent Pin
Kfirp24-Jun-10 5:31
Kfirp24-Jun-10 5:31 
QuestionI Can't Display webbrowser on CustomPage Pin
windies2115-Jun-10 0:15
windies2115-Jun-10 0:15 
Generalproblem with menuitems and combobox in winXP Pin
cha_cher23-May-10 5:59
cha_cher23-May-10 5:59 
GeneralMaximise takes up the whole display Pin
Alxandr6-Apr-10 8:09
Alxandr6-Apr-10 8:09 
GeneralRe: Maximise takes up the whole display Pin
AndyL26-Apr-10 9:52
AndyL26-Apr-10 9:52 
GeneralRe: Maximise takes up the whole display Pin
Alxandr6-Apr-10 13:49
Alxandr6-Apr-10 13:49 
GeneralRe: Maximise takes up the whole display Pin
AndyL26-Apr-10 21:12
AndyL26-Apr-10 21:12 
GeneralCombo box on the Title of the window Pin
Vijay Karajgikar23-Feb-10 8:08
Vijay Karajgikar23-Feb-10 8:08 
QuestionWPF and flashing taskbar button [modified] Pin
Chris1012-Jan-10 10:52
Chris1012-Jan-10 10:52 
GeneralCustom Control vs ControlTemplate for Window Pin
tinatran307-Jan-10 13:01
tinatran307-Jan-10 13:01 

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.