Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Creating an application like Google Desktop in WPF and C#

0.00/5 (No votes)
28 Jul 2011 1  
This article explains how to create an application like Google Desktop in WPF.

Introduction

Google Desktop docks to the left or right edge of the screen and displays some gadgets. It is always visible. It does not cover other windows, and other windows do not hide it. To do that, we have to use the AppBar - the taskbar is an AppBar too. This article explains how to create an application like Google Desktop in WPF.

Background

I implemented AppBar functionalities based on this article. Yet another article helped me to create a window with extended glass.

WndProc hook

For some functions in this project, we must receive window messages. In Windows Forms, it is sufficient to override the WndProc function, but in WPF, we must add a hook. First, we create a callback for the hook:

IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, 
               IntPtr lParam, ref bool handled)
{
    return IntPtr.Zero;
}

hwnd is the handle to the window, msg is the ID of the window message, wParam and lParam are message parameters. If we process a message, we have to set handled to true. The WndProc does not receive messages, because we haven't added a hook. So add a hook using the following code:

protected override void OnSourceInitialized(EventArgs e)
{
    base.OnSourceInitialized(e); // Raising base method.

    IntPtr hwnd = new WindowInteropHelper(this).Handle; // Getting our window's handle.
    HwndSource source = HwndSource.FromHwnd(hwnd);
    source.AddHook(new HwndSourceHook(WndProc)); // Adding hook.
}

The OnSourceInitialized method is raised when the window's handle is created. The Loaded event is called after this. Now we can receive the window's messages.

Docking window

In order to get a window docked, we have to register the AppBar. To do this, we must use P/Invoke. Declare the following WinAPI functions:

// Sends messages for setup AppBar.
[DllImport("SHELL32", CallingConvention = CallingConvention.StdCall)]
static extern uint SHAppBarMessage(int dwMessage, ref APPBARDATA pData);

// Moves, resizes and optionally repaints window.
[DllImport("User32.dll", ExactSpelling = true, 
   CharSet = System.Runtime.InteropServices.CharSet.Auto)]
private static extern bool MoveWindow(IntPtr hWnd, int x, 
        int y, int cx, int cy, bool repaint);

// Registers Window Message - we will use it for register AppBar message
[DllImport("User32.dll", CharSet = CharSet.Auto)]
private static extern int RegisterWindowMessage(string msg);

Now add the following constants:

const int ABM_NEW = 0;
const int ABM_REMOVE = 1;
const int ABM_QUERYPOS = 2;
const int ABM_SETPOS = 3;
const int ABM_GETSTATE = 4;
const int ABM_GETTASKBARPOS = 5;
const int ABM_ACTIVATE = 6;
const int ABM_GETAUTOHIDEBAR = 7;
const int ABM_SETAUTOHIDEBAR = 8;
const int ABM_WINDOWPOSCHANGED = 9;
const int ABM_SETSTATE = 10;
const int ABN_STATECHANGE = 0;
const int ABN_POSCHANGED = 1;
const int ABN_FULLSCREENAPP = 2;
const int ABN_WINDOWARRANGE = 3;
const int ABE_LEFT = 0;
const int ABE_TOP = 1;
const int ABE_RIGHT = 2;
const int ABE_BOTTOM = 3;

They are for the SHAppBarMessage function. Now the structures:

// Native rectangle.
[StructLayout(LayoutKind.Sequential)]
struct RECT
{
    public int left;
    public int top;
    public int right;
    public int bottom;
}

// AppBar data.
[StructLayout(LayoutKind.Sequential)]
struct APPBARDATA
{
    public int cbSize;
    public IntPtr hWnd;
    public int uCallbackMessage;
    public int uEdge;
    public RECT rc;
    public IntPtr lParam;
}

Now go to Project Properties and to the Settings tab. Add the uEdge property. It must be of type int. uEdge is the screen's edge to which the AppBar will be docked. Set it to 2 (right edge) or 0 (left edge).

Let's now write the functions for registering and unregistering the AppBar.

// Is AppBar registered?
bool fBarRegistered = false;

// Number of AppBar's message for WndProc.
int uCallBack;

// Register AppBar.
void RegisterBar()
{
    WindowInteropHelper helper = new WindowInteropHelper(this);
    HwndSource mainWindowSrc = (HwndSource)HwndSource.FromHwnd(helper.Handle);

    APPBARDATA abd = new APPBARDATA();
    abd.cbSize = Marshal.SizeOf(abd);
    abd.hWnd = mainWindowSrc.Handle;

    if (!fBarRegistered)
    {
        uCallBack = RegisterWindowMessage("AppBarMessage");
        abd.uCallbackMessage = uCallBack;

        uint ret = SHAppBarMessage(ABM_NEW, ref abd);
        fBarRegistered = true;

        ABSetPos();
    }
}
// Unregister AppBar.
void UnregisterBar()
{
    WindowInteropHelper helper = new WindowInteropHelper(this);
    HwndSource mainWindowSrc = (HwndSource)HwndSource.FromHwnd(helper.Handle);

    APPBARDATA abd = new APPBARDATA();
    abd.cbSize = Marshal.SizeOf(abd);
    abd.hWnd = mainWindowSrc.Handle;

    if (fBarRegistered)
    {
        SHAppBarMessage(ABM_REMOVE, ref abd);
        fBarRegistered = false;
    }
}
// Set position of AppBar.
void ABSetPos()
{
    if (fBarRegistered)
    {
        WindowInteropHelper helper = new WindowInteropHelper(this);
        HwndSource mainWindowSrc = (HwndSource)HwndSource.FromHwnd(helper.Handle);

        APPBARDATA abd = new APPBARDATA();
        abd.cbSize = Marshal.SizeOf(abd);
        abd.hWnd = mainWindowSrc.Handle;
        abd.uEdge = Properties.Settings.Default.uEdge;

        if (abd.uEdge == ABE_LEFT || abd.uEdge == ABE_RIGHT)
        {
            abd.rc.top = 0;
            abd.rc.bottom = (int)SystemParameters.PrimaryScreenHeight;
            if (abd.uEdge == ABE_LEFT)
            {
                abd.rc.left = 0;
                abd.rc.right = (int)this.ActualWidth;
            }
            else
            {
                abd.rc.right = (int)SystemParameters.PrimaryScreenWidth;
                abd.rc.left = abd.rc.right - (int)this.ActualWidth;
            }
        }
        else
        {
            abd.rc.left = 0;
            abd.rc.right = (int)SystemParameters.PrimaryScreenWidth;
            if (abd.uEdge == ABE_TOP)
            {
                abd.rc.top = 0;
                abd.rc.bottom = (int)this.ActualHeight;
            }
            else
            {
                abd.rc.bottom = (int)SystemParameters.PrimaryScreenHeight;
                abd.rc.top = abd.rc.bottom - (int)this.ActualHeight;
            }
        }

        SHAppBarMessage(ABM_QUERYPOS, ref abd);

        SHAppBarMessage(ABM_SETPOS, ref abd);
        MoveWindow(abd.hWnd, abd.rc.left, abd.rc.top, 
          abd.rc.right - abd.rc.left, abd.rc.bottom - abd.rc.top, true);
    }
}

Now add this line into OnSourceInitialized:

RegisterBar();

This will register the AppBar. When the screen resolution is changed, we have to resize the AppBar. So add these lines to WndProc:

if (msg == uCallBack && wParam.ToInt32() == ABN_POSCHANGED)
{
    ABSetPos();
    handled = true;
}

When the window is closing, call UnregisterBar.

private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e)
{
    UnregisterBar();
}

Now set the following properties of the window (add these lines into the Window tag in XAML):

WindowStyle="ToolWindow" ResizeMode="NoResize" 
  Closing="Window_Closing" ShowInTaskbar="False" 
  Title="My own Google Desktop!"
  Width="200" Height="500"

Extending glass

This feature requires Windows Vista or higher. When Aero (DWM) composition is enabled, the caption and borders of the window are translucent, like glass. We can extend the glass effect into the client area. To do that, we need P/Invoke.

// Native thickness struct.
[StructLayout(LayoutKind.Sequential)]
struct MARGINS
{
    public int cxLeftWidth;
    public int cxRightWidth;
    public int cyTopHeight;
    public int cyBottomHeight;
}

// Extending glass into client area.
[DllImport("dwmapi.dll")]
static extern int DwmExtendFrameIntoClientArea(IntPtr hWnd, ref MARGINS pMarInset);

// Is Aero composition enabled?
[DllImport("dwmapi.dll")]
extern static int DwmIsCompositionEnabled(ref int en);
const int WM_DWMCOMPOSITIONCHANGED = 0x031E;

This function will create a fully glass window:

void ExtendGlass()
{
    try
    {
        int isGlassEnabled = 0;
        DwmIsCompositionEnabled(ref isGlassEnabled);
        if (Environment.OSVersion.Version.Major > 5 && isGlassEnabled > 0)
        {
            WindowInteropHelper helper = new WindowInteropHelper(this);
            HwndSource mainWindowSrc = (HwndSource)HwndSource.FromHwnd(helper.Handle);

            mainWindowSrc.CompositionTarget.BackgroundColor = Colors.Transparent;
            this.Background = Brushes.Transparent;

            MARGINS margins = new MARGINS();
            margins.cxLeftWidth = -1;
            margins.cxRightWidth = -1;
            margins.cyBottomHeight = -1;
            margins.cyTopHeight = -1;

            DwmExtendFrameIntoClientArea(mainWindowSrc.Handle, ref margins);
        }
    }
    catch (DllNotFoundException) { }
}

If Aero composition is changed, we will have to re-extend the glass. So add this into WndProc:

else if (msg == WM_DWMCOMPOSITIONCHANGED)
{
    ExtendGlass();
    handled = true;
}

Now extend the glass, when the source is initialized:

ExtendGlass();

Changing edge

Now we can move the window wherever we want. It should go to the left or right edge depending on where you release the window. So let's do that.

Add a new window to the project and name it TransPrev. This window will show arrows when the window is dragging. Add these parameters into the Window tag in TransPrev.xaml:

AllowsTransparency="True" Background="Transparent"
   WindowState="Maximized" WindowStyle="None"
   Height="400" Width="900"
<Path x:Name="left" Data="M0,150 L200,0 L200,100 L400,100 L400,200 L200,200 L200,300 Z" 
      Stroke="Black" Fill="LightGray" VerticalAlignment="Center" 
      HorizontalAlignment="Left" Visibility="Collapsed" Margin="10" />
<Path x:Name="right" Data="M400,150 L200,0 L200,100 L0,100 L0,200 L200,200 L200,300 Z" 
      Stroke="Black" Fill="LightGray" VerticalAlignment="Center" 
      HorizontalAlignment="Right" Visibility="Collapsed" Margin="10" />
<Path x:Name="left" Data="M0,150 L200,0 L200,100 L400,100 L400,200 L200,200 L200,300 Z" 
      Stroke="Black" Fill="LightGray" VerticalAlignment="Center" HorizontalAlignment="Left" 
      Visibility="Collapsed" Margin="10" />
<Path x:Name="right" Data="M400,150 L200,0 L200,100 L0,100 L0,200 L200,200 L200,300 Z" 
      Stroke="Black" Fill="LightGray" VerticalAlignment="Center" 
      HorizontalAlignment="Right" Visibility="Collapsed" Margin="10" />

Now add the arrows' paths:

<Path x:Name="left" Data="M0,150 L200,0 L200,100 L400,100 L400,200 L200,200 L200,300 Z" 
  Stroke="Black" Fill="LightGray" VerticalAlignment="Center" 
  HorizontalAlignment="Left" Visibility="Collapsed" Margin="10" />
<Path x:Name="right" Data="M400,150 L200,0 L200,100 L0,100 L0,200 L200,200 L200,300 Z" 
  Stroke="Black" Fill="LightGray" VerticalAlignment="Center" 
  HorizontalAlignment="Right" Visibility="Collapsed" Margin="10" />

Go to TransPrev.xaml.cs and add these lines:

// Edges
const int ABE_LEFT = 0;
const int ABE_TOP = 1;
const int ABE_RIGHT = 2;
const int ABE_BOTTOM = 3;

// Sets arrows' visibility
public void SetArrow(int uEdge)
{
    if (uEdge == ABE_LEFT)
    {
        right.Visibility = System.Windows.Visibility.Collapsed;
        left.Visibility = System.Windows.Visibility.Visible;
    }
    else if (uEdge == ABE_RIGHT)
    {
        left.Visibility = System.Windows.Visibility.Collapsed;
        right.Visibility = System.Windows.Visibility.Visible;
    }
}

This code is for setting the arrows' visibility. Now go to MainWindow.xaml.cs and add these constants:

// Non-client area left button down
const int WM_NCLBUTTONDOWN = 0x00A1;

// Window moving or sizing is exited.
const int WM_EXITSIZEMOVE = 0x0232;

...and these variables:

// Translate preview window
TransPrev tp = new TransPrev();

// Is left button down on non-client area?
bool nclbd = false;

These methods are for calculating the edge and refresh arrow in TransPrev:

// Refresh arrows in TransPrev
void RefreshTransPrev()
{
    CalculateHorizontalEdge();
    tp.SetArrow(Properties.Settings.Default.uEdge);
}

// Calculate new edge
void CalculateHorizontalEdge()
{
    if (SystemParameters.PrimaryScreenWidth / 2 > this.Left)
        Properties.Settings.Default.uEdge = ABE_LEFT;
    else
        Properties.Settings.Default.uEdge = ABE_RIGHT;
    Properties.Settings.Default.Save();
}

Handle the LocationChanged event:

private void Window_LocationChanged(object sender, EventArgs e)
{
}
LocationChanged="Window_LocationChanged"

Add these lines to Window_LocationChanged:

if (nclbd)
{
    if (fBarRegistered)
    {
        UnregisterBar();
        tp.Show();
    }
    RefreshTransPrev();
}

Now it's time for WndProc.

// Non-client area left button down.
else if (msg == WM_NCLBUTTONDOWN)
{
    nclbd = true;
}
// Moving or sizing has ended.
else if (msg == WM_EXITSIZEMOVE)
{
    nclbd = false;
    // Hide TransPrev, no Close.
    tp.Hide();
    CalculateHorizontalEdge();
    RegisterBar();
}

Because we only hide TransPrev and don't close it, we must add this line into Window_Closing (after UnregisterBar):

Application.Current.Shutdown();

If we don't add this line, then the application will not shutdown.

Analogue clock gadget

The main gadget is often an analog clock. Let's create it. Add a UserControl and name it Clock. Add the following XAML into Grid:

<!-- Rows -->
<Grid.RowDefinitions>
    <RowDefinition Height="*" />
    <RowDefinition Height="*" />
</Grid.RowDefinitions>
        
<!-- Main circle -->
<Ellipse Grid.RowSpan="8" Fill="LightGray" Stroke="Gray" />

<!-- Markers -->
<Rectangle Grid.Row="1" Width="1" Fill="Black" Margin="0,0,0,2">
    <Rectangle.RenderTransform>
        <RotateTransform Angle="0" />
    </Rectangle.RenderTransform>
</Rectangle>
<Rectangle Grid.Row="1" Width="1" Fill="Black" Margin="0,0,0,2">
    <Rectangle.RenderTransform>
        <RotateTransform Angle="30" />
    </Rectangle.RenderTransform>
</Rectangle>
<Rectangle Grid.Row="1" Width="1" Fill="Black" Margin="0,0,0,2">
    <Rectangle.RenderTransform>
        <RotateTransform Angle="60" />
    </Rectangle.RenderTransform>
</Rectangle>
<Rectangle Grid.Row="1" Width="1" Fill="Black" Margin="0,0,0,2">
    <Rectangle.RenderTransform>
        <RotateTransform Angle="90" />
    </Rectangle.RenderTransform>
</Rectangle>
<Rectangle Grid.Row="1" Width="1" Fill="Black" Margin="0,0,0,2">
    <Rectangle.RenderTransform>
        <RotateTransform Angle="120" />
    </Rectangle.RenderTransform>
</Rectangle>
<Rectangle Grid.Row="1" Width="1" Fill="Black" Margin="0,0,0,2">
    <Rectangle.RenderTransform>
        <RotateTransform Angle="150" />
    </Rectangle.RenderTransform>
</Rectangle>
<Rectangle Grid.Row="1" Width="1" Fill="Black" Margin="0,0,0,2">
    <Rectangle.RenderTransform>
        <RotateTransform Angle="180" />
    </Rectangle.RenderTransform>
</Rectangle>
<Rectangle Grid.Row="1" Width="1" Fill="Black" Margin="0,0,0,2">
    <Rectangle.RenderTransform>
        <RotateTransform Angle="210" />
    </Rectangle.RenderTransform>
</Rectangle>
<Rectangle Grid.Row="1" Width="1" Fill="Black" Margin="0,0,0,2">
    <Rectangle.RenderTransform>
        <RotateTransform Angle="240" />
    </Rectangle.RenderTransform>
</Rectangle>
<Rectangle Grid.Row="1" Width="1" Fill="Black" Margin="0,0,0,2">
    <Rectangle.RenderTransform>
        <RotateTransform Angle="270" />
    </Rectangle.RenderTransform>
</Rectangle>
<Rectangle Grid.Row="1" Width="1" Fill="Black" Margin="0,0,0,2">
    <Rectangle.RenderTransform>
        <RotateTransform Angle="300" />
    </Rectangle.RenderTransform>
</Rectangle>
<Rectangle Grid.Row="1" Width="1" Fill="Black" Margin="0,0,0,2">
    <Rectangle.RenderTransform>
        <RotateTransform Angle="330" />
    </Rectangle.RenderTransform>
</Rectangle>
        
<!-- Ellipse for hiding part of markers -->
<Ellipse Grid.RowSpan="2" Fill="LightGray" Margin="10" />

<!-- Clockwises -->
<Rectangle Grid.Row="1" Width="1" Fill="Black" Margin="0,0,0,50">
    <Rectangle.RenderTransform>
        <RotateTransform x:Name="hour" Angle="-180" />
    </Rectangle.RenderTransform>
</Rectangle>
<Rectangle Grid.Row="1" Width="1" Fill="Black" Margin="0,0,0,30">
    <Rectangle.RenderTransform>
        <RotateTransform x:Name="minute" Angle="-180" />
    </Rectangle.RenderTransform>
</Rectangle>
<Rectangle Grid.Row="1" Width="1" Fill="Gray" Margin="0,0,0,4">
    <Rectangle.RenderTransform>
        <RotateTransform x:Name="second" Angle="-180" />
    </Rectangle.RenderTransform>
</Rectangle>

<!-- Fixing of clockwises -->
<Ellipse Grid.RowSpan="2" Fill="Gray" Width="5" Height="5" />

They are elements of the clock. Now add a handler for the Load event and put these lines in the generated method:

// Timer setup.
tim = new DispatcherTimer();
tim.Interval = new TimeSpan(0, 0, 0, 0, 100);
tim.Tick += new EventHandler(tim_Tick);
tim.Start();
RefreshClock();

Add a System.Threading reference:

using System.Windows.Threading;

Add these lines into the class:

DispatcherTimer tim;

// Timer tick.
void tim_Tick(object sender, EventArgs e)
{
    RefreshClock();
}

// Refreshes clockwises.
void RefreshClock()
{
    DateTime dt = DateTime.Now;
    second.Angle = (double)dt.Second / 60 * 360 - 180;
    minute.Angle = (double)dt.Minute / 60 * 360 - 180 + (second.Angle + 180) / 60;
    hour.Angle = (double)dt.Hour / 12 * 360 - 180 + (minute.Angle + 180) / 12;
}

The clock should work.

Notes gadget

Let's create the notes gadget. Add a new setting notes of type string. In this setting, we will save notes. Now add a new class and name it Notes. Add these references:

using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.Runtime.InteropServices;
using System.Windows.Interop;

The class will override TextBox. This is the code:

public class Notes : TextBox
{
    public Notes()
    {
        base.AcceptsReturn = true;
        base.Background = Brushes.LightYellow;
        base.FontFamily = new FontFamily("Comic Sans MS");
        base.Text = Properties.Settings.Default.notes;
        base.TextWrapping = System.Windows.TextWrapping.Wrap;
        base.FontSize = 15;
    }

    protected override void OnTextChanged(TextChangedEventArgs e)
    {
        base.OnTextChanged(e);
        Properties.Settings.Default.notes = base.Text;
        Properties.Settings.Default.Save();
    }
}

Putting all the gadgets into MainWindow

We can now put all the gadgets into MainWindow. There is a clock, a calendar, and a notes. Calendar is a standard WPF element. First, add the row definitions of Grid:

<Grid.RowDefinitions>
    <RowDefinition Height="Auto" />
    <RowDefinition Height="Auto" />
    <RowDefinition Height="*" />
</Grid.RowDefinitions>

Now add the gadgets:

<my:Clock Grid.Row="0" Margin="4" Width="180" Height="180" />
<Calendar Grid.Row="1" Margin="4" />
<my:Notes Grid.Row="2" Margin="4" />

Our simple Google Desktop is complete.

Conclusion

With the AppBar API, we can create applications like Google Desktop. DWM functions allow to use Aero effects, but they don't work in systems older than Windows Vista. In order to use WndProc in WPF, we must add a hook. In order to get the AppBar API, DWM API, and other cool stuff, we must use P/Invoke, because .NET doesn't have managed libraries to do that.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here