In Chromium, we use custom frame windows on Windows XP, and on Windows Vista without DWM compositing enabled. We do something slightly different when DWM compositing is turned on, which I’ll cover in a separate post. Right now, I’m going to focus on how we built and are building the custom window frame for Windows XP. There are two components to this work, rendering the contents of the window and making Windows happy.
To render the UI, including the browser window chrome, we use a lightweight UI framework that we call ChromeViews. ChromeViews is a set of C++ classes that provides a layout, rendering and event processing system similar to NSView, and the rendering classes used by popular layout engines like WebKit and Gecko. At the core is the concept of a View, which simply represents a rectangle. This rectangle can paint itself, lay out child views, and receive various kinds of events based on user input. By subclassing this View object we are able to develop the various bits of UI that Chrome uses. Each window is composed of a hierarchy of these View objects. The bridge between the native OS window and the View hierarchy is an object called aViewContainer, which hosts the RootView that owns the hierarchy. The ViewContainer is an abstract interface that is typically implemented byHWNDViewContainer, which wraps a windows HWND. The window procedure for this HWND receives various messages, translates some into ChromeViews events and fires them into the hierarchy, paints the Views into a bitmap device context using a graphics API called Skia, and does various other things to make Windows happy.
Let’s take a look at the browser window, since it’s an interesting and complex example of a custom frame window. At the top level there’s a View called the RootView that contains the rest of the browser UI. Nested in layers beneath this are other Views like the TabStrip, the ToolbarView, Buttons, and so on and so forth.
But rendering the contents of the window is only part of the story. Since we wanted a custom look for our UI, we wanted to draw our own title bars and sizing borders too. In fact, for our browser window, we wanted to do something even stranger - we wanted to have the tabs living up in the title bar of the window - where they’d form the distinctive “skyline” of the browser. On Windows, the window title bar (called the “caption”), the sizing borders and other window controls are referred to as the “non-client area”. The stuff inside them is called the “client area”. We were throwing a spanner into works by having a non-rectangular client area that intrudes into the non-client area.
The first step was to keep things simple. We don’t treat the non-client area as a separate set of stuff to paint. Rather, the non-client area is just another View in the same hierarchy that hosts the contents of the window. This had some implications though on how we implemented a ViewContainer that could host this view hierarchy to match the look we wanted.
Keeping Windows Happy
In the first implementation, which is what was shipped with the Google Chrome beta by the way, we create a Windows HWND with very basic window styles - because the ChromeViews hierarchy contained within is providing all the non-client area, we wanted to suppress things like WS_CAPTION etc. To make the portions of the window title bar area not occupied by tabs act like a title bar though, messages like WM_NCHITTEST were handled to determine if the mouse was in fact in a blank area, and if so HTCAPTION was returned. On the trunk now we are now somewhat more sophisticated about this - we establish hit-test masks for the tabs that closely follow their shape such that clicking in the blue area where two tabs meet is treated like a click to the title bar!
Because we weren’t a frame window, some things like window resizing had to be implemented by hand. This had some undesirable side-effects: when Windows resizes a window it runs a modal loop that synchronously paints the window(s) being revealed… but with our system based entirely on the user dragging the mouse this didn’t happen, and so we left artifacts behind as Windows caught up later and repainted the revealed window. We also found we didn’t work well with Windows’ built in window management features (e.g. window tiling) and various window manager extensions like SplitView didn’t work because we either didn’t have the appropriate window styles or just did things a little differently. The ViewContainer implementation for the browser window in the Chrome beta is called XPFrame.
This is where a new set of ChromeViews ViewContainers stepped in. Dialog boxes in Chrome on Windows XP are actually hosted in a different kind of window called a CustomFrameWindow. This derives (via a class called Window) from HWNDViewContainer, a general purpose ViewContainer that wraps a HWND. Here’s what CustomFrameWindow does differently:
- Bears the WS_CAPTION, WS_THICKFRAME, and WS_OVERLAPPED window styles, as well as the WS_EX_WINDOWEDGE and WS_EX_APPWINDOW extended styles. By having these styles, CustomFrameWindow gets a lot of the functionality that XPFrame had to do by hand for free (such as specifying the contents and enabled-state of system menu items). This did mean however some gymnastics when painting.
- Handles WM_NCCALCSIZE, passing the bounds of a “ClientView” as the bounds of the client area. This rectangle is smaller than the window rectangle, just like for a standard top level window. In XPFrame, the client area bounds were equal to the window bounds.
- Handles WM_NCHITTEST, doing more comprehensive checks to determine if the mouse is over the sizing borders etc. Doing this allows the CustomFrameWindow to be moved and sized by Windows, with all the painting benefits that come with this.
- Handles WM_NCPAINT, to make sure the client area is painted synchronously with the non-client area, to prevent effects where the window frame appears to “snap in” around its contents.
- Handles WM_NCLBUTTONDOWN, to make sure clicks to the non-client areas specifying the window controls are translated into events that are sent to the relevant ChromeView.
- Handles WM_SETCURSOR, to make sure the sizing cursors are set for various hit-test codes.
- Handles the undocumented WM_NCUAHDRAWCAPTION and WM_NCUAHDRAWFRAME messages to suppress excessive flickering in the title bar area.
- Update:Handle a growing list of messages, starting with WM_SETTEXT and WM_ENTERMENULOOP, removing the window’s WS_VISIBLE style, calling the default window procedure and then adding the WS_VISIBLE style back. Why? Well, sometimes the window gets into a state (not reproducible via a repeatable set of steps, but always eventually) where it begins to paint the standard non-client title bar and controls over the one you rendered. Windows doesn’t do this in its default WM_NCPAINT handler, rather it does it directly in its default handlers for these messages. You need to handle these messages and fake invisibility to convince Windows not to do it. If anyone knows other messages that this rule applies to please let me know! Update 2 this is not quite right. I’m going to summarize in a separate document.
CustomFrameWindow also taps in to a lot of the benefits offered by theWindow/WindowDelegate/NonClientView/ClientView framework we use for our dialog boxes. As an example, the shape of our window is actually slightly different from the standard Windows shape. Windows supports rounded corners on its windows with 1-bit transparency by using window regions. This is used on Windows XP Luna to provide rounded corners at the top left and right of each window. We use a custom radius, and the ChromeViews framework allows us to easily specify a custom window shape by creating a Skia path.
In developing frameworks for building UI in Chromium, we’ve found that trying wherever possible to do what Windows expects means less code, better display integrity and performance, and that the more subtle features some users expect “just work”.
A lot of what I’ve described are things that relate closely to Chromium source code, and design decisions that we’ve made to meet the objectives we’ve set for ourselves, and as such they may not be directly applicable to other work. But I do think many of the issues addressed in the implementation of the CustomFrameWindow ViewContainer are potentially useful to others developing custom user interfaces on Windows, especially considering the documentation for some of the things we had to do here is non-existent aside from postings similar to this!
If you’ve done work like this before, I’d be interested in hearing your thoughts on our approach. Maybe you have some good stories to share!