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);
IntPtr hwnd = new WindowInteropHelper(this).Handle; HwndSource source = HwndSource.FromHwnd(hwnd);
source.AddHook(new HwndSourceHook(WndProc)); }
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:
[DllImport("SHELL32", CallingConvention = CallingConvention.StdCall)]
static extern uint SHAppBarMessage(int dwMessage, ref APPBARDATA pData);
[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);
[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:
[StructLayout(LayoutKind.Sequential)]
struct RECT
{
public int left;
public int top;
public int right;
public int bottom;
}
[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.
bool fBarRegistered = false;
int uCallBack;
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();
}
}
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;
}
}
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.
[StructLayout(LayoutKind.Sequential)]
struct MARGINS
{
public int cxLeftWidth;
public int cxRightWidth;
public int cyTopHeight;
public int cyBottomHeight;
}
[DllImport("dwmapi.dll")]
static extern int DwmExtendFrameIntoClientArea(IntPtr hWnd, ref MARGINS pMarInset);
[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:
const int ABE_LEFT = 0;
const int ABE_TOP = 1;
const int ABE_RIGHT = 2;
const int ABE_BOTTOM = 3;
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:
const int WM_NCLBUTTONDOWN = 0x00A1;
const int WM_EXITSIZEMOVE = 0x0232;
...and these variables:
TransPrev tp = new TransPrev();
bool nclbd = false;
These methods are for calculating the edge and refresh arrow in TransPrev
:
void RefreshTransPrev()
{
CalculateHorizontalEdge();
tp.SetArrow(Properties.Settings.Default.uEdge);
}
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
.
else if (msg == WM_NCLBUTTONDOWN)
{
nclbd = true;
}
else if (msg == WM_EXITSIZEMOVE)
{
nclbd = false;
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
:
-->
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
-->
<Ellipse Grid.RowSpan="8" Fill="LightGray" Stroke="Gray" />
-->
<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 Grid.RowSpan="2" Fill="LightGray" Margin="10" />
-->
<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>
-->
<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:
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;
void tim_Tick(object sender, EventArgs e)
{
RefreshClock();
}
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.