创建类似 Google Desktop 的 WPF 和 C# 应用程序
本文介绍如何使用WPF创建一个类似Google Desktop的应用程序。
引言
Google Desktop停靠在屏幕的左侧或右侧边缘,并显示一些小工具。它始终可见。它不会覆盖其他窗口,其他窗口也不会隐藏它。要实现这一点,我们必须使用AppBar - 任务栏也是一个AppBar。本文介绍如何使用WPF创建一个类似Google Desktop的应用程序。
背景
我基于这篇文章实现了AppBar功能。另一篇文章帮助我创建了一个具有扩展玻璃效果的窗口。
WndProc钩子
对于本项目中的某些功能,我们必须接收窗口消息。在Windows Forms中,重写WndProc
函数就足够了,但在WPF中,我们必须添加一个钩子。首先,我们为钩子创建一个回调
IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam,
IntPtr lParam, ref bool handled)
{
return IntPtr.Zero;
}
hwnd
是窗口的句柄,msg
是窗口消息的ID,wParam
和lParam
是消息参数。如果我们处理了消息,我们必须将handled
设置为true
。WndProc
不会接收消息,因为我们还没有添加钩子。所以使用以下代码添加一个钩子
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.
}
当窗口的句柄创建时,会引发OnSourceInitialized
方法。之后会调用Loaded
事件。现在我们可以接收窗口的消息了。
停靠窗口
为了使窗口停靠,我们必须注册AppBar。要做到这一点,我们必须使用P/Invoke。声明以下WinAPI函数
// 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);
现在添加以下常量
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;
它们是为SHAppBarMessage
函数准备的。现在是结构体
// 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;
}
现在转到项目属性,然后到“设置”选项卡。添加uEdge
属性。它的类型必须是int
。uEdge
是AppBar将停靠的屏幕边缘。将其设置为2(右边缘)或0(左边缘)。
现在让我们编写注册和注销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);
}
}
现在将这一行添加到OnSourceInitialized
中
RegisterBar();
这将注册AppBar。当屏幕分辨率改变时,我们必须重新调整AppBar的大小。所以将这些行添加到WndProc
中
if (msg == uCallBack && wParam.ToInt32() == ABN_POSCHANGED)
{
ABSetPos();
handled = true;
}
当窗口关闭时,调用UnregisterBar
。
private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e)
{
UnregisterBar();
}
现在设置窗口的以下属性(在XAML的Window
标签中添加这些行)
WindowStyle="ToolWindow" ResizeMode="NoResize"
Closing="Window_Closing" ShowInTaskbar="False"
Title="My own Google Desktop!"
Width="200" Height="500"
扩展玻璃效果
此功能需要Windows Vista或更高版本。当Aero(DWM)合成启用时,窗口的标题栏和边框会半透明,就像玻璃一样。我们可以将玻璃效果扩展到客户区域。为此,我们需要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;
此函数将创建一个完全玻璃效果的窗口
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) { }
}
如果Aero合成被更改,我们将不得不重新扩展玻璃效果。所以将这个添加到WndProc
中
else if (msg == WM_DWMCOMPOSITIONCHANGED)
{
ExtendGlass();
handled = true;
}
现在在源初始化时扩展玻璃效果
ExtendGlass();
改变边缘
现在我们可以将窗口移动到任何我们想要的位置。它应该停靠在左边缘或右边缘,取决于你释放窗口的位置。所以,我们来实现它。
向项目中添加一个新窗口,并将其命名为TransPrev
。这个窗口将在窗口拖动时显示箭头。在TransPrev.xaml的Window
标签中添加这些参数
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" />
现在添加箭头的路径
<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" />
转到TransPrev.xaml.cs并添加这些行
// 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;
}
}
这段代码用于设置箭头的可见性。现在转到MainWindow.xaml.cs并添加这些常量
// Non-client area left button down
const int WM_NCLBUTTONDOWN = 0x00A1;
// Window moving or sizing is exited.
const int WM_EXITSIZEMOVE = 0x0232;
……以及这些变量
// Translate preview window
TransPrev tp = new TransPrev();
// Is left button down on non-client area?
bool nclbd = false;
这些方法用于计算边缘并在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();
}
处理LocationChanged
事件
private void Window_LocationChanged(object sender, EventArgs e)
{
}
LocationChanged="Window_LocationChanged"
将这些行添加到Window_LocationChanged
中
if (nclbd)
{
if (fBarRegistered)
{
UnregisterBar();
tp.Show();
}
RefreshTransPrev();
}
现在是时候处理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();
}
因为我们只隐藏TransPrev
而不关闭它,所以在Window_Closing
中(在UnregisterBar
之后)必须添加这一行
Application.Current.Shutdown();
如果我们不添加这一行,应用程序将不会关闭。
模拟时钟小工具
主小工具通常是一个模拟时钟。让我们来创建它。添加一个UserControl
并命名为Clock
。将以下XAML添加到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" />
它们是时钟的元素。现在添加一个Load
事件的处理器,并将这些行放在生成的函数中
// Timer setup.
tim = new DispatcherTimer();
tim.Interval = new TimeSpan(0, 0, 0, 0, 100);
tim.Tick += new EventHandler(tim_Tick);
tim.Start();
RefreshClock();
添加一个System.Threading
引用
using System.Windows.Threading;
将这些行添加到类中
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;
}
时钟应该能工作了。
便笺小工具
让我们来创建便笺小工具。添加一个新的设置notes
,类型为string
。在这个设置中,我们将保存便笺。现在添加一个新类并命名为Notes
。添加这些引用
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;
该类将重写TextBox
。这是代码
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();
}
}
将所有小工具放入MainWindow
现在我们可以将所有小工具放入MainWindow
中了。这里有一个时钟、一个日历和一个便笺。Calendar
是一个标准的WPF元素。首先,添加Grid
的行定义
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
现在添加小工具
<my:Clock Grid.Row="0" Margin="4" Width="180" Height="180" />
<Calendar Grid.Row="1" Margin="4" />
<my:Notes Grid.Row="2" Margin="4" />
我们简单的Google Desktop就完成了。
结论
使用AppBar API,我们可以创建类似Google Desktop的应用程序。DWM函数允许使用Aero效果,但它们在Windows Vista之前的系统上不起作用。为了在WPF中使用WndProc
,我们必须添加一个钩子。为了使用AppBar API、DWM API和其他很酷的东西,我们必须使用P/Invoke,因为.NET没有提供托管库来实现这些功能。