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:
if (DateTime.Now.Subtract(m_headerLastClicked) <= s_doubleClick)
{
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:
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.
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.
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);
}
private void HandleSourceInitialized(Object sender, EventArgs e)
{
m_hwndSource = (HwndSource)PresentationSource.FromVisual(this);
HwndSource.FromHwnd(m_hwndSource.Handle).AddHook(
new HwndSourceHook(NativeMethods.WindowProc));
Int32 DWMWA_NCRENDERING_POLICY = 2;
NativeMethods.DwmSetWindowAttribute(
m_hwndSource.Handle,
DWMWA_NCRENDERING_POLICY,
ref DWMWA_NCRENDERING_POLICY,
4);
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:
- Create a transparent window in code (and also set it's background).
- 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.
- Calculate position for each external window.
- When moving the Main Window the external windows had to "glue" or better "dock" with the Main Window.
- 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:
private void InitializeSurrounds()
{
m_wndT = CreateTransparentWindow();
m_wndL = CreateTransparentWindow();
m_wndB = CreateTransparentWindow();
m_wndR = CreateTransparentWindow();
SetSurroundShadows();
}
private static Window CreateTransparentWindow()
{
Window wnd = new Window();
wnd.AllowsTransparency = true;
wnd.ShowInTaskbar = false;
wnd.WindowStyle = WindowStyle.None;
wnd.Background = null;
return wnd;
}
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:
protected override void OnInitialized(EventArgs e)
{
LocationChanged += HandleLocationChanged;
SizeChanged += HandleLocationChanged;
StateChanged += HandleWndStateChanged;
InitializeSurrounds();
ShowSurrounds();
base.OnInitialized(e);
}
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;
}
private void HandleWndStateChanged(Object sender, EventArgs e)
{
if (WindowState == WindowState.Normal)
{
ShowSurrounds();
}
else
{
HideSurrounds();
}
}