Click here to Skip to main content
15,881,687 members
Articles / Desktop Programming / WPF

Building a Metro UI with WPF

Rate me:
Please Sign up or sign in to vote.
4.00/5 (5 votes)
5 Jun 2012CPOL4 min read 33.8K   22   4
How to build a Metro UI with WPF?

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 (e.g., a Rectangle) and registering on it's PreviewMouseDown event:

C#
// Is this a double-click?
if (DateTime.Now.Subtract(m_headerLastClicked) <= s_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);
}

m_headerLastClicked = DateTime.Now;

if (Mouse.LeftButton == MouseButtonState.Pressed)
{
  DragMove();
}

How can we resize the window?

By adding Shapes (e.g., Rectangles) one on each side of the window (left, top, right, bottom) and registering on its PreviewMouseDown event:

C#
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.

C#
/// <summary>
/// Resizes the window.
/// </summary>
/// <param name="direction">The direction.</param>
private void ResizeWindow(ResizeDirection direction)
{
  NativeMethods.SendMessage(m_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.

C#
/// <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)
{
  m_hwndSource = (HwndSource)PresentationSource.FromVisual(this);

  // Returns the HwndSource object for the window
  // which presents WPF content in a Win32 window.
  HwndSource.FromHwnd(m_hwndSource.Handle).AddHook(
      new HwndSourceHook(NativeMethods.WindowProc));

  // http://msdn.microsoft.com/en-us/library/aa969524(VS.85).aspx
  Int32 DWMWA_NCRENDERING_POLICY = 2;
  NativeMethods.DwmSetWindowAttribute(
      m_hwndSource.Handle,
      DWMWA_NCRENDERING_POLICY,
      ref DWMWA_NCRENDERING_POLICY,
      4);

  // http://msdn.microsoft.com/en-us/library/aa969512(VS.85).aspx
  NativeMethods.ShowShadowUnderWindow(m_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:

  1. Create a transparent window in code (and also set it's background).
  2. Find the Main Window position on screen. Fortunately I could access the Left and Top properties and by it's width and height I could calculate the window boundary.
  3. Calculate position for each external window.
  4. When moving the Main Window the external windows had to "glue" or better "dock" with the Main Window.
  5. When resizing the Main Window the external windows had to resize as well, according to the Main Window size.

..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:

C#
/// <summary>
/// Initializes the surrounding windows.
/// </summary>
private void InitializeSurrounds()
{
  // Top.
  m_wndT = CreateTransparentWindow();

  // Left.
  m_wndL = CreateTransparentWindow();

  // Bottom.
  m_wndB = CreateTransparentWindow();

  // Right.
  m_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(Boolean active = true)
{
  if (active)
  {
      Double cornerRadius = 1.75;

      m_wndT.Content = GetDecorator(
          "Images/ACTIVESHADOWTOP.PNG");
      m_wndL.Content = GetDecorator(
          "Images/ACTIVESHADOWLEFT.PNG", cornerRadius);
      m_wndB.Content = GetDecorator(
          "Images/ACTIVESHADOWBOTTOM.PNG");
      m_wndR.Content = GetDecorator(
          "Images/ACTIVESHADOWRIGHT.PNG", cornerRadius);
  }
  else
  {
      m_wndT.Content = GetDecorator(
          "Images/INACTIVESHADOWTOP.PNG");
      m_wndL.Content = GetDecorator(
          "Images/INACTIVESHADOWLEFT.PNG");
      m_wndB.Content = GetDecorator(
          "Images/INACTIVESHADOWBOTTOM.PNG");
      m_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:

C#
/// <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)
{
  m_wndT.Left   = Left  - c_edgeWndSize;
  m_wndT.Top    = Top   - m_wndT.Height;
  m_wndT.Width  = Width + c_edgeWndSize * 2;
  m_wndT.Height = c_edgeWndSize;

  m_wndL.Left   = Left - m_wndL.Width;
  m_wndL.Top    = Top;
  m_wndL.Width  = c_edgeWndSize;
  m_wndL.Height = Height;

  m_wndB.Left   = Left  - c_edgeWndSize;
  m_wndB.Top    = Top   + Height;
  m_wndB.Width  = Width + c_edgeWndSize * 2;
  m_wndB.Height = c_edgeWndSize;

  m_wndR.Left   = Left + Width;
  m_wndR.Top    = Top;
  m_wndR.Width  = c_edgeWndSize;
  m_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();
  }
}

License

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



Comments and Discussions

 
GeneralMy vote of 1 Pin
Member 46851846-Feb-13 21:47
Member 46851846-Feb-13 21:47 
QuestionSource Code Pin
knoami5-Jun-12 22:19
knoami5-Jun-12 22:19 
GeneralRe: Source Code Pin
Khaari8-Aug-12 4:14
professionalKhaari8-Aug-12 4:14 
GeneralMy vote of 4 Pin
ZeroDotNet5-Jun-12 11:36
ZeroDotNet5-Jun-12 11:36 

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.