65.9K
CodeProject 正在变化。 阅读更多。
Home

创建类似 Google Desktop 的 WPF 和 C# 应用程序

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.83/5 (25投票s)

2011年7月28日

CPOL

4分钟阅读

viewsIcon

70850

downloadIcon

5245

本文介绍如何使用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,wParamlParam是消息参数。如果我们处理了消息,我们必须将handled设置为trueWndProc不会接收消息,因为我们还没有添加钩子。所以使用以下代码添加一个钩子

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属性。它的类型必须是intuEdge是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.xamlWindow标签中添加这些参数

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没有提供托管库来实现这些功能。

© . All rights reserved.