Custom Window Chrome in WPF

Guest post by Joe Castro, WPF product team developer

This document covers the design and some implementation details of getting WPF windows wrapped in custom chrome. Currently WPF supports standard windows, with an icon, title-text, and caption buttons, as well as borderless windows, which when combined with transparency allow arbitrary top-level shapes but the application loses system support for behaviors associated with having a standard caption area, such as maximize. There is a desire to create applications that fill a middle-ground here: the appearance and behavior of a standard window with a more complete branding experience.

Scenarios:

There are many applications (WPF and non-) that customize their chrome to varying degrees. This document discusses some of the types of changes and how to emulate these behaviors. Some of these scenarios have also been discussed in other blogs with code samples by better writers than me, so in those cases I’ll just point to URL rather than duplicating their code. There is also a project that accompanies this document that has code for everything that’s described.

These are some existing apps that modify their chrome in different ways:

Office 2007 – In a lot of ways this is the holy grail of custom chrome, leveraging Aero when it’s available and gracefully degrading the experience when it’s not. When glass is on programs like Word appear to have a standard caption area, although the title text is centered and there’s some custom drawing in the top left. It includes the pearl and “Quick Access Toolbar” in the apparent non-client region. It also keeps the standard caption buttons. When composition is disabled (i.e. glass is off) it keeps the non-client layout, but draws its own caption and buttons in its own style regardless of the user’s Windows theme.

joe1

Office chrome

Safari, iTunes, and Quicktime on Windows – Apple retains a lot of the OS X look and feel with their applications on Windows There’s a seamless visual transition between the client and non-client areas. In Safari the top corners are slightly rounded and the bottom corners are right-angles, it has a one-pixel border surrounding it, and it has its own style of caption buttons. The current version of iTunes has a similar border, but the round corners on all sides. It also has caption buttons that are styled more like Vista, but is not using the DWM for drawing them. Since the DWM isn’t involved there is no hover glow outside the window frame.

joe2

iTunes chrome

Vista Explorer and Aero Wizards – In Vista, Explorer chooses to not display the icon or title text on the windows because the same information is displayed right below in the breadcrumb bar. It does not adjust the size of the caption area, instead just leaving it blank when glass is enabled. This area is especially noticeable in wizards written for Vista–like the DVD maker–which behave the same as Explorer. The effect here is different from what happens when not setting a system icon and clearing the title text: both elements still appear in other contexts, such as the taskbar and the alt-tab window.

joe3

Vista DVD maker wizard with Aero

Internet Explorer 7 and Windows Media Player on Vista – These programs extend the glass frame to focus the user’s attention on the content rather than the controls. IE doesn’t actually modify the caption area but by extending glass into the address bar they have effectively made it part of the caption. WMP has a similar effect with their controls at the bottom of the window, effectively creating a second caption area where the controls live.

joe4 joe5

IE7 and WMP on Vista

Baby Smash! – And now for something completely different, www.babysmash.com. This application takes over the users screen, intentionally obscuring the taskbar. When this program is running it’s the only application the user is supposed to be able to interact with.

 

joe6

How do they do that?

Every version of Windows has had support for writing programs that mess with the non-client area, but WPF doesn’t expose this through the Window class. WPF is designed in a way that when functionality is missing you can dig underneath the public APIs and directly manipulate the Win32 handles that WPF resides on top of.

This document has accompanying code to help clarify the descriptions, but there’s still an assumption about basic Win32 knowledge. Really, it should be enough if you know that a WndProc is basically a giant function for handling system and user events that the Window needs to respond to, and recognize that words in all capitals (like HWND and NCCALCSIZE_PARAMS) probably refer to Win32 structures. I also use the convention dllName!FunctionName to refer to native functions that are exported from a DLL. In C# these are accessed through P/Invoking. MSDN is usually a good reference for supplementary information.

In C# you can get an HWND for a WPF Window after it’s been shown,

IntPtr hwnd = new WindowInteropHelper(_window).Handle;

and then subclass the WndProc with your own,

HwndSource.FromHwnd(hwnd).AddHook(_WndProc);

Then most of the interesting stuff happens inside your implementation of _WndProc.

IE7 – Extending the glass frame

The DWM allows applications to extend the frame into their window via a simple P/Invoke call, dwmapi!DwmExtendFrameIntoClientArea. This function was introduced in Vista, so its usage must be checked to ensure that it exists. This doesn’t actually impact the non-client area, but for supported themes it can diminish the distinction between client and non-client areas. This function will also only succeed if composition is enabled, and if composition gets toggled off and then on, it needs to be called again to re-enable it. Composition can be queried using dwmapi!DwmIsCompositionEnabled.

Very readable code for this has been written by Adam Nathan and is posted on his blog, http://blogs.msdn.com/adam_nathan/archive/2006/05/04/589686.aspx. Rather than duplicating what he’s done, I’m just going to point there.

An addition to what Adam shows is that to properly handle extending the glass frame you also need to detect when the theme changes to or from one that doesn’t support glass. The WndProc will receive a DWM_COMPOSITIONCHANGED message when this happens, so your app can respond accordingly.

Baby Smash! – Maximized, and respectful of the taskbar

This is WindowStyle.None working like it’s supposed to. Maximizing a window with a style of None will obscure the taskbar. Oftentimes when dealing with custom chrome this isn’t really what was intended, but in this scenario it is.

For apps using WindowStyle.None that don’t want that default behavior an app can handle WM_GETMINMAXINFO. Again, I’m going to defer to someone else’s code to demonstrate this.

Lester Lobo has an excellent write up of how to do this here, http://blogs.msdn.com/llobo/archive/2006/08/01/Maximizing-window-_2800_with-WindowStyle_3D00_None_2900_-considering-Taskbar.aspx.

Vista Explorer – Removing redundant information from the title bar

It’s reasonable to not want to use the title bar if the same information is going to be displayed in a more natural way elsewhere in the window. To do this on Vista there is uxtheme!SetWindowThemeAttribute. This function enables the removal of the system icon and title text. This is different from simply clearing those fields as they’ll still appear in the taskbar and on the alt-tab window. This can also be used to turn off only one or the other in the window frame.

This effect only works on Vista and only with the Aero theme, though unlike the glass extensions it does work in Aero Basic. The code for this is actually really similar to extending the glass frame. A streamlined version of how to do it (assuming the necessary native structs and functions have been declared) is:

void SetWindowThemeAttribute(Window window, bool showCaption, bool showIcon)
{
    bool isGlassEnabled = NativeMethods.DwmIsCompositionEnabled();

    IntPtr hwnd = new WindowInteropHelper(window).Handle;

    var options = new WTA_OPTIONS
    {
        dwMask = (WTNCA.NODRAWCAPTION | WTNCA.NODRAWICON)
    };
    if (isGlassEnabled)
    {
        if (!showCaption)
        {
            options.dwFlags |= WTNCA.NODRAWCAPTION;
        }
        if (!showIcon)
        {
            options.dwFlags |= WTNCA.NODRAWICON;
        }
    }

    NativeMethods.SetWindowThemeAttribute(hwnd, WINDOWTHEMEATTRIBUTETYPE.WTA_NONCLIENT, ref options, WTA_OPTIONS.Size);
}

Just like with glass, to properly handle theme changes the app should listen for WM_DWMCOMPOSITIONCHANGED and reapply the theme attributes.

Office 2007 with Aero – Drawing in the NC area with glass

The DWM will turn off glass effects if it detects that the program is attempting to draw into the non-client area. Office handles this by removing the non-client area.

Handling the WM_NCCALCSIZE message allows an app to adjust the relative size of the client area. The correct handling of this function is pretty complicated. For this purpose the program can actually claim to handle it and not modify the lParam NCCALCSIZE_PARAMS struct (or the RECT, depending on the value of the wParam). When the message is received, the lParam contains the new window rectangle, but it expects the field to be overwritten with the new client rectangle. By not modifying the lParam value the non-client region has been effectively eliminated!

This doesn’t impact the effects of extending the glass frame, as described earlier, so by extending the glass frame at the top by the desired caption height, and the other borders by a reasonable width the app still gets the appearance of a standard window but is now free to draw anywhere on it. The frame can be extended an arbitrary amount, but to get the Office behavior that respect the user’s settings for caption sizes, you can use the SystemParameters class to get the information. Extending glass based on ResizeFrameHorizontalBorderHeight, ResizeFrameVerticalBorderWidth, and CaptionHeight, do a reasonable job of emulating those settings.

When the window is initially displayed, you’ll need to get the system to send a WM_NCCALCSIZE. Calling user32!SetWindowPos with SWP_FRAMECHANGED and SWP_NOSIZE after hooking the WndProc does this without any other adverse effects.

The behavior of the DWM is a bit strange at this point. If glass is extended from the top, it will still draw the Aero caption buttons. The height of the buttons is capped, but if there’s not enough vertical space they’ll squish and still appear. They aren’t responsive to user interaction without a little prodding, but this part isn’t difficult. To make them respond to mouse clicks and hover (including the glow outside the window bounds) call dwmapi!DwmDefWindowProc as part of the handling for WM_NCHITTEST, respecting whether it wants to have handled the message. However, the DWM will not draw the icon or the caption text so you’re on your own there. Neither of those is hard to draw with WPF visuals. To go the native route to get the window text to display with the glow effect and appropriately change color when the window is maximized, you can invoke uxtheme!DrawThemeTextEx. The GDI interop to obtain the parameters that the call requires probably isn’t worth it since you can fake it easily with WPF. Be aware that the style of the caption changes when the window is maximized: in the normal state it’s usually black text with a white glow. When maximized the glow goes away and the text turns white.

After all this manipulation the window doesn’t have an internal understanding of what you’ve done with the client area, so the content presenter is the same size as it would have otherwise been, but it’s shifted up and to the left. To correct this, use a Canvas as the window’s content and bind its Width and Height properties to the ActualWidth and ActualHeight of the window. This could also be done as part of a replacement for the Window’s ControlTemplate.

When the window is maximized, Windows will clip out the border on the app’s behalf if a clipping region hasn’t been applied. In this scenario there’s no reason to have done that, but the thing to watch out for is if glass isn’t extended as deeply into the borders as Windows would have (SM_CXFRAME (+ SM_CXPADDEDBORDER on Vista)) then a maximized window will clip the edges of your client region. The simplest way to work around this is to just place a Border around the window’s content that has a thickness of clipping region, and make its Visibility collapsed when not maximized.

This issue is more prevalent when glass is off, but it’s a reason to not mess with the glass padding when using the DWM. If the depth of the Glass is different it will need to be accounted for during the processing of WM_WINDOWPOSCHANGED when the window is maximized. The general technique for this is covered in the next section.

The only detail left with this scenario is handling the WM_NCHITTEST message. This lets the system do the grunt work of handling resize and caption dragging. To get the intuitive behavior, return the appropriate HT value based on whether the mouse position (as determined by the lParam) is anywhere over the glass frame. If interactive controls are being drawn over the caption area, then there’s a need to perform a second WPF hit test before blindly handing back HTCAPTION. If VisualTreeHelper.HitTest doesn’t return null for the mouse position relative to the window, returning HTNOWHERE will ensure that the visual behaves like a control instead of the caption area. This gets potentially expensive, so applications that can specialize their behavior here probably should. By handling WM_NCHITTEST rather than using WPF for dragging the window and Thumbs for resizing, the app gets to retain more of the standard system behaviors, e.g., the system menu for Size will work properly, and right-clicking in the caption area will pop up the system menu. Because of the granular control handling this message gives, the caption area can effectively be created anywhere in the window. But having it anywhere other than docked at the top and extending the width of the window is likely to confuse users.

It’s worth noting that Office turns off its Aero chrome when the window resizes to small enough dimensions. This may be appropriate for some other applications if elements are going to flow over the DWM caption buttons. WPF elements will draw on top of them, but by calling DwmDefWindowProc they’ll still glow when hovered over. The illusion of pseudo-standard chrome doesn’t hold up under all circumstances, or at least not for Office. By drawing the caption themselves they occasionally draw it over the caption buttons. There are a lot of system metrics that interact with the caption area that impact the size and font and style of the caption text. It’s very difficult to accurately simulate the system behavior in all cases.

Office 2007 without Aero – Or, you are responsible for everything

This is a more complicated variation of the previous Office scenario, in that you don’t get DWM to handle any aspect of the frame for you. This covers getting behaviors of Office without Aero, WMP 11 on XP, and Apple Safari and iTunes. This isn’t necessarily how these applications do what they do, but it’s a way of going about it in WPF.

Applications that support custom chrome with Aero Glass should gracefully degrade into this scenario. Even if the program is running on Vista, the user’s theme doesn’t necessarily have composition enabled. Some applications, like iTunes and Safari, simply don’t integrate with the higher tier features of Vista and give a consistent experience across all themes.

First of all, everything mentioned in the last section regarding WM_NCCALCSIZE still applies. It can be used to remove the non-client area so it’s easy to place elements in arbitrary locations. Without the DWM, though, the window border appears blank and rectangular. Since the canvas is covering the entire window, you can use it to draw the borders in any fashion.

If hard corners aren’t your thing, you can round them by applying an HRGN to the window. This needs to be done whenever there’s a state change to the window, e.g., when it’s shown, hidden, or resized. The safest place to do this is when handling WM_WINDOWPOSCHANGED. Calling gdi32!CreateRoundRectRgn with the window’s width and height, and the desired roundness, followed by gdi32!SetWindowRgn with the created HRGN will round the corners of the windows. If you only want certain corners rounded, e.g., only the top, like Safari, then you can gdi32!CreateRectRgn for any corner or edge and gdi32!CombineRgn to combine them.

When the window is maximized, Windows will usually do HRGN clipping on your behalf to remove the padding around your content. If you’ve applied your own HRGN then it respects your settings, as such you’ll need to detect when the window is maximized and apply an appropriate rectangular region based on the size of the monitor to be maximized to. This is really similar in implementation to maximizing the borderless window while respecting the taskbar.

Overall, the trickiest part of this scenario is that Windows will often try to draw the default title bar, even though there’s no non-client area. It does this at generally predictable times, when Windows would generally have had to perform a repaint to update the title bar. This includes maximizing, restoring, minimizing, setting the icon or text, and activating/deactivating the window, or whenever the enabled state of any of the caption buttons gets toggled. To avoid this, these types of changes can be detected and intercepted.

For messages like WM_SETTEXT and WM_SETICON, you can avoid the redraw by temporarily removing the WS_VISIBLE style from the Window and let the change get processed, and then restore the style afterwards.

For WM_NCACTIVATE, the MSDN documentation is incomplete. Intercepting the call and passing it to user32!DefWindowProc with an lParam of -1 will cause the system to not redraw the caption area. Handling WM_NCACTIVATE in this way allows you to not have to handle WM_ACTIVATE.

If a goal is also to respect the location and size of the caption buttons, potentially even the style, then the NONCLIENTMETRICS should be used to determine their location and size. Generally WPF’s theme support should be enough to detect how to style the buttons based on the user’s theme.

Code!

The last two scenarios have been heavy on Win32 jargon without actual code snippets. The implementation of what was described gets complicated enough that it becomes distracting when inlined. So instead this document comes with a DLL + source that can be dropped into an existing project to enable WPF windows to get the Office behavior, and a simple app that demonstrates the API.

http://code.msdn.microsoft.com/chrome

Predicting the future is hard – or “How to make some people unhappy, all of the time”

Ultimately replacing the window chrome is doing the job of the window manager. Emulating Windows like this has potential to miss behaviors that your users expect, or not work correctly under some circumstances (like high-DPI, or under a screen reader). Care should be taken to ensure that the behaviors your users care about will work correctly with your replacement.

For example, some of the standard window caption features today are:

  • Left-click on the icon to get the system menu.
  • Double click on the system icon to close the app (Office’s pearl does this as well).
  • Right click in the caption to get the system menu.
  • Double-click the caption to maximize the app.
  • With DWM, change the caption text style when maximized.
  • Change colors based on Active/Inactive states (The colors it uses respect DWM colorization when Aero glass is on, the Theme colors when not in Windows Classic, and System colors when in Windows Classic.)
  • It respects system metrics for sizes, and the metrics available for measurement have changed in different versions of Windows (e.g. iPaddedBorderWidth was added for Vista).

These are all things that the system no longer does automatically for you when you use your own chrome. And some of this behavior has changed in different versions of Windows.

Replacing the Window chrome is a very visible way to differentiate your app and increase branding impact, but it’s likely that an implementation will miss some things, or that future versions of Windows may change some behaviors making your app look out of place.

This doesn’t mean “don’t do this.” Just that replacing the chrome should be an informed decision.

One more caveat

What’s described here is also what would be required for WPF to implement this kind of functionality into the framework. For the foreseeable future everyone has to work on top of Win32. That means that applications that hook into the WndProc under WPF may potentially modify the window’s state in incompatible ways. Applications that use techniques described in this document may not be able to take advantage of future versions of the framework without modification. This isn’t a definite happening, just a warning about a potential issue.

Sources:

There’s a fair amount of documentation available online that was used to compile the information for this document. These are some pages that might be useful additional reading for implementing some of these features:

Custom Window Frame Using DWM: http://msdn.microsoft.com/en-us/library/bb688195(VS.85).aspx

Frequently asked questions about the Aero Basic window frame: http://shellrevealed.com/blogs/shellblog/archive/2006/10/12/Frequently-asked-questions-about-the-Aero-Basic-window-frame.aspx

MSDN documentation about the NONCLIENTMETRICS structure: http://msdn.microsoft.com/en-us/library/ms724506.aspx

Maximizing window (with WindowStyle=None) considering Taskbar: http://blogs.msdn.com/llobo/archive/2006/08/01/Maximizing-window-_2800_with-WindowStyle_3D00_None_2900_-considering-Taskbar.aspx

Towards an even deeper understanding of the WM_NCCALCSIZE message: http://blogs.msdn.com/oldnewthing/archive/2003/09/15/54925.aspx

Aero Glass inside a WPF Window: http://blogs.msdn.com/adam_nathan/archive/2006/05/04/589686.aspx

Using WM_WINDOWPOSCHANGED to react to window state changes: http://blogs.msdn.com/oldnewthing/archive/2008/01/15/7113860.aspx

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值