Have you ever used the Zune software? I guess so, but I haven't until version 4.7.1404.0 came out. This version comes with significant changes: Windows Phone 7 support and integration with Windows Live Essentials 2011 among other.
When I first run the software I got amazed by the user interface (UI). I told to myself, “this must not be WPF, no way!”. The text was so clear and the UI was so responsive. I also looked in Wikipedia and read that the first versions of Zune software were released back in 2006. At this time WPF was about to be released with .NET 3.0 (release date was Nov 2006).
Since the UI is not built with WPF then what kind of technology did the Zune team used? Could it be MFC or some other unmanaged UI? To find out, I launched the Process Explorer utility and looked for the Zune executable. By default, .NET Processes are highlighted with yellow as shown in the image below.
Great, so Zune software is a managed application, or better, if it's an unmanaged application at least it hosts the CLR in it's process. (Any Windows application can host the CLR). A quick look in the installation directory yield the following output:
Followed by a quick view with Reflector:
As you can see, the root namespace is Microsoft.Iris. A quick search returned this blog post and this one. It looks like some kind of WPF ancestor combined with MCML.
Is it possible to build a similar UI with WPF?
The first difficulties came when setting the WindowStyle enumeration to None. We need that because with this style only the client area is visible - the title bar and border are not shown.
The image above is not what we want. We need to hide the window boundaries. This can be done by setting the ResizeMode enumeration to NoResize. But now, we can't move the window, we can't resize it and the mouse events are not raised! Here is a very nice blog post discussing in very detail (among other) the reason for that.
How can we move the window?
By adding a Shape (ex. a Rectangle) and registering on it's PreviewMouseDown event:
// Is this a double-click? if (DateTime.Now.Subtract(this.headerLastClicked) <= doubleClick) { // Execute the code inside the event handler for the // restore button click passing null for the sender // and null for the event args. HandleRestoreClick(null, null); } this.headerLastClicked = DateTime.Now; if (Mouse.LeftButton == MouseButtonState.Pressed) { DragMove(); }
How can we resize the window?
By adding Shapes (ex. Rectangles) one on each side of the window (left, top, right, bottom) and registering on it's PreviewMouseDown event:
Rectangle clickedRectangle = (Rectangle)sender; switch (clickedRectangle.Name) { case "top": Cursor = Cursors.SizeNS; ResizeWindow(ResizeDirection.Top); break; case "bottom": Cursor = Cursors.SizeNS; ResizeWindow(ResizeDirection.Bottom); break; // ... }
Here is the code for resizing the window. It uses the underlying Windows USER component.
/// <summary> /// Resizes the window. /// </summary> /// <param name="direction">The direction.</param> private void ResizeWindow(ResizeDirection direction) { NativeMethods.SendMessage(this.hwndSource.Handle, WM_SYSCOMMAND, (IntPtr)(61440 + direction), IntPtr.Zero); } [DllImport("user32.dll", CharSet = CharSet.Auto)] internal static extern IntPtr SendMessage( IntPtr hWnd, UInt32 msg, IntPtr wParam, IntPtr lParam);
How can we add a drop shadow to the window?
At the time of this writing, I know two ways of doing this:
The first one (which is described here) uses the Desktop Window Manager (DWM) API. Specifically it uses the DwmSetWindowAttribute Function combined with the DwmExtendFrameIntoClientArea Function to place a drop shadow around the window area. This method works by registering at the SourceInitialized event. When this event is raised, it is a good place to call any code that can interoperate with the underlying Win32 window.
/// <summary> /// Raises the <see cref="FrameworkElement.Initialized"/> event. /// This method is invoked whenever /// <see cref="P:FrameworkElement.IsInitialized"/> /// is set to true internally. /// </summary> /// <param name="e">The <see cref="T:RoutedEventArgs"/> /// that contains the event data.</param> protected override void OnInitialized(EventArgs e) { AllowsTransparency = false; ResizeMode = ResizeMode.NoResize; Height = 480; Width = 852; WindowStartupLocation = WindowStartupLocation.CenterScreen; WindowStyle = WindowStyle.None; SourceInitialized += HandleSourceInitialized; base.OnInitialized(e); } /// <summary> /// Handles the source initialized. /// </summary> /// <param name="sender">The sender.</param> /// <param name="e">The <see cref="System.EventArgs"/> /// instance containing the event data.</param> private void HandleSourceInitialized(object sender, EventArgs e) { this.hwndSource = (HwndSource)PresentationSource.FromVisual(this); // Returns the HwndSource object for the window // which presents WPF content in a Win32 window. HwndSource.FromHwnd(this.hwndSource.Handle).AddHook( new HwndSourceHook(NativeMethods.WindowProc)); // http://msdn.microsoft.com/en-us/library/aa969524(VS.85).aspx int DWMWA_NCRENDERING_POLICY = 2; NativeMethods.DwmSetWindowAttribute( this.hwndSource.Handle, DWMWA_NCRENDERING_POLICY, ref DWMWA_NCRENDERING_POLICY, 4); // http://msdn.microsoft.com/en-us/library/aa969512(VS.85).aspx NativeMethods.ShowShadowUnderWindow(this.hwndSource.Handle); }
Without the drop shadow
With the drop shadow
There is a problem here though. If the user goes to System Properties, Performance Options and uncheck the “Show shadows under windows” checkbox, the shadow will not be visible.
The Zune software still keeps it's drop shadow visible even if the “Show shadows under windows” checkbox is unchecked.
How can this possibly be?
Well, the Zune software does not use the DWM API to place drop shadows. Instead, it uses four external, transparent, windows on each size to create an illusion of a drop shadow. The drop shadow is actually “composed” by four transparent windows on each side.
The second way, of placing the drop shadows, via external windows is the main reason for this post.
Here is what I had to do:
..Sounds like a lot of work to do for displaying a drop shadow that remains visible even if the user unchecks the “Show shadows under windows” checkbox!
Creating the transparent window in code was easy:
/// <summary> /// Initializes the surrounding windows. /// </summary> private void InitializeSurrounds() { // Top. this.wndT = CreateTransparentWindow(); // Left. this.wndL = CreateTransparentWindow(); // Bottom. this.wndB = CreateTransparentWindow(); // Right. this.wndR = CreateTransparentWindow(); SetSurroundShadows(); } /// <summary> /// Creates an empty window. /// </summary> /// <returns></returns> private static Window CreateTransparentWindow() { Window wnd = new Window(); wnd.AllowsTransparency = true; wnd.ShowInTaskbar = false; wnd.WindowStyle = WindowStyle.None; wnd.Background = null; return wnd; } /// <summary> /// Sets the artificial drop shadow. /// </summary> /// <param name="active">if set to <c>true</c> [active].</param> private void SetSurroundShadows(bool active = true) { if (active) { double cornerRadius = 1.75; this.wndT.Content = GetDecorator( "Images/ACTIVESHADOWTOP.PNG"); this.wndL.Content = GetDecorator( "Images/ACTIVESHADOWLEFT.PNG", cornerRadius); this.wndB.Content = GetDecorator( "Images/ACTIVESHADOWBOTTOM.PNG"); this.wndR.Content = GetDecorator( "Images/ACTIVESHADOWRIGHT.PNG", cornerRadius); } else { this.wndT.Content = GetDecorator( "Images/INACTIVESHADOWTOP.PNG"); this.wndL.Content = GetDecorator( "Images/INACTIVESHADOWLEFT.PNG"); this.wndB.Content = GetDecorator( "Images/INACTIVESHADOWBOTTOM.PNG"); this.wndR.Content = GetDecorator( "Images/INACTIVESHADOWRIGHT.PNG"); } } [DebuggerStepThrough] private Decorator GetDecorator(string imageUri, double radius = 0) { Border border = new Border(); border.CornerRadius = new CornerRadius(radius); border.Background = new ImageBrush( new BitmapImage( new Uri(BaseUriHelper.GetBaseUri(this), imageUri))); return border; }
Calculating the position, width and height for each external window was also not difficult:
/// <summary> /// Raises the <see cref="FrameworkElement.Initialized"/> event. /// This method is invoked whenever /// <see cref="FrameworkElement.IsInitialized"/> /// is set to true internally. /// </summary> /// <param name="e">The <see cref="T:RoutedEventArgs"/> /// that contains the event data.</param> protected override void OnInitialized(EventArgs e) { // ... LocationChanged += HandleLocationChanged; SizeChanged += HandleLocationChanged; StateChanged += HandleWndStateChanged; InitializeSurrounds(); ShowSurrounds(); base.OnInitialized(e); } /// <summary> /// Handles the location changed. /// </summary> /// <param name="sender">The sender.</param> /// <param name="e">The <see cref="System.EventArgs"/> /// instance containing the event data.</param> private void HandleLocationChanged(object sender, EventArgs e) { this.wndT.Left = Left - edgeWndSize; this.wndT.Top = Top - this.wndT.Height; this.wndT.Width = Width + edgeWndSize * 2; this.wndT.Height = edgeWndSize; this.wndL.Left = Left - this.wndL.Width; this.wndL.Top = Top; this.wndL.Width = edgeWndSize; this.wndL.Height = Height; this.wndB.Left = Left - edgeWndSize; this.wndB.Top = Top + Height; this.wndB.Width = Width + edgeWndSize * 2; this.wndB.Height = edgeWndSize; this.wndR.Left = Left + Width; this.wndR.Top = Top; this.wndR.Width = edgeWndSize; this.wndR.Height = Height; } /// <summary> /// Handles the windows state changed. /// </summary> /// <param name="sender">The sender.</param> /// <param name="e">The <see cref="System.EventArgs"/> /// instance containing the event data.</param> private void HandleWndStateChanged(object sender, EventArgs e) { if (WindowState == WindowState.Normal) { ShowSurrounds(); } else { HideSurrounds(); } }
I hope you find this post useful, there is a lot of information around but I think this post connects the pieces.
The solution contains two projects. The first one uses the 1st method for displaying the drop shadow. The second one uses the method described above.
Due to popular demand, you can download the sample projects here and here.