全屏 WPF 应用程序和低级别键盘挂钩






4.75/5 (27投票s)
此应用程序显示随机大小和颜色的形状。非常适合喜欢坐着并随意敲击键盘的婴儿和幼儿。
引言
成为父亲后,我发现女儿很快就想做我所做的一切。而且,由于我花大量时间在键盘上打字,她在不到一岁的时候就想使用电脑了。所以,我有一个绝妙的主意,我将打开记事本让她随意打字。这奏效了,大约 10 秒钟后,她不知怎么就进入了控制面板并打开了添加硬件向导。于是,我在网上搜索了一个防婴儿应用程序,她会喜欢。我在 Code Project 上找到了一个(底部链接)。我的女儿已经使用并喜爱了这个程序很长一段时间了。当我决定开始学习 WPF 时,我需要一个我可以处理的小项目来入门。而且,重写我女儿如此喜欢的形状显示应用程序似乎是一个很好的选择。于是我做了,并将其命名为 ShapeShow。我希望您能发现本文对开发目的以及为您的孩子提供一些乐趣有所帮助。
必备组件
运行此应用程序需要
- Windows XP 或 Vista
- .NET Framework 3.5
- Visual Studio 2008(打开项目)
形状库
我想做的第一件事是定义屏幕上可能显示的形状。所以,我创建了一个包含要显示的每种形状(圆形、三角形和正方形)的类的形状库项目。尽管如此,圆形和正方形类最终也代表了椭圆和矩形。
形状库的目标是提供随机颜色和大小的随机形状。为了做到这一点,我实际上不需要了解实际形状的任何信息。我只需要一个可以创建这些形状的类。这听起来是一个使用工厂模式的好机会。因此,我决定为形状创建一个接口,并为每种形状类型实现该接口。尽管如此,在这种情况下,基类可能效果更好,因为每种形状的实现方式几乎相同。下面是从 WPF 应用程序调用以获取新随机形状的方法。
public static IShape CreateShape()
{
return CreateShape((ShapeType)RandomNumber.Next(0, ShapeCount));
}
它调用一个接受形状类型的重载方法。形状类型是一个枚举,通过将随机整数转换为形状类型来随机选择。这反过来又调用另一个重载的 CreateShape
方法,该方法为形状选择随机颜色。最后,形状在下面显示的方法中创建
public static IShape CreateShape(ShapeType shapeType, ShapeColor shapeColor)
{
IShape shape = CreateShapeFromType(shapeType);
shape.Height = RandomNumber.NextDouble() *
(SystemParameters.VirtualScreenHeight / 2) + MinHeight;
shape.Width = RandomNumber.NextDouble() *
(SystemParameters.VirtualScreenWidth / 2) + MinWidth;
shape.Top = RandomNumber.NextDouble() *
(SystemParameters.VirtualScreenHeight - shape.Height);
shape.Left = RandomNumber.NextDouble() *
(SystemParameters.VirtualScreenWidth - shape.Width);
shape.FillBrush = CreateBrushFromColor(shapeColor);
return shape;
}
形状类型和颜色通过一组简单的 Case
语句选择,这些语句选择要创建的正确形状和要填充的正确笔刷。形状大小受屏幕大小限制,并且有最小高度和宽度。
WPF 应用程序
WPF 应用程序的工作量不大。它只需要在每次按下按键时请求一个新形状并在屏幕上显示该形状。为了做到这一点,我只需重写 OnKeyDown
方法。但是,我也想在那里做其他事情,主要是给用户一种通过特殊按键组合关闭应用程序的方法。而这正是我通过“选项”菜单所做的。下面是重写 OnKeyDown
方法的代码
protected override void OnKeyDown(KeyEventArgs e)
{
if (IsRequestingOptions)
{
ShowOptions();
}
else if (CanDraw(e))
{
DrawShape(ShapeFactory.CreateShape());
}
base.OnKeyDown(e);
}
绘制形状就像将形状添加到名为 DrawingSurface
的画布元素一样简单。我还限制了屏幕上同时显示的形状数量。这有助于防止屏幕变得过于混乱,并降低内存使用量。
private void DrawShape(IShape shape)
{
if (DrawingSurface.Children.Count == MaxShapeCount)
{
DrawingSurface.Children.RemoveAt(0);
}
DrawingSurface.Children.Add(shape.UIElement);
}
正如我之前提到的,由于应用程序是全屏的,因此没有关闭或最小化按钮。因此,我提供了一个特殊的按键组合来打开“选项”屏幕。“选项”屏幕显示三个选项:清除屏幕、返回 ShapeShow 和退出 ShapeShow。用户可以通过按 Ctrl+Alt+O 来调出选项屏幕。为了显示选项,我们只需将选项用户控件的 Visibility
属性设置为 Visible
。同时,我还降低了绘图表面的不透明度,以视觉上提示绘图表面已停用。用户控件是一个简单的 StackPanel
,带有三个垂直排列的按钮。下面是代码和选项控件的图片
<StackPanel x:Name="ButtonPanel">
<Button Name="ClearScreen" Content="Clear Screen" Height="60" Width="Auto"
Style="{DynamicResource OptionsButton}"></Button>
<Button Name="ReturnButton" Content="Return To ShapeShow" Height="60" Width="Auto"
Style="{DynamicResource OptionsButton}"></Button>
<Button Name="CloseButton" Content="Exit ShapeShow" Height="60" Width="Auto"
Style="{DynamicResource OptionsButton}"></Button>
</StackPanel>
正如您所见,我还创建了自己的按钮。这在 WPF 中非常简单。我只需像往常一样声明一个 Button
并应用一个 Style
。Style
非常灵活且易于使用。我将我的按钮样式放在一个名为 *Resources.xaml* 的单独文件中。下面是这些按钮的样式,称为 OptionsButton
<Style x:Key="OptionsButton" TargetType="Button">
<Setter Property="Margin" Value="10" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Grid Height="{TemplateBinding Height}" Width="{TemplateBinding Width}">
<Rectangle x:Name="ButtonRect" RadiusX="10" RadiusY="10"
StrokeThickness="2" Stroke="#555555"
Style="{StaticResource OptionsButtonUp}" />
<ContentPresenter x:Name="ButtonContent"
Content="{TemplateBinding Content}"
HorizontalAlignment="Center"
VerticalAlignment="Center"
TextElement.FontSize="20"
TextElement.Foreground="#C8C8C8"/>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="ButtonRect" Property="Style"
Value="{StaticResource OptionsButtonOver}" />
<Setter TargetName="ButtonContent" Property="TextElement.Foreground"
Value="#FFFFFF" />
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter TargetName="ButtonRect" Property="Style"
Value="{StaticResource OptionsButtonDown}" />
<Setter TargetName="ButtonContent" Property="TextElement.Foreground"
Value="#AAAAAA" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
虽然这乍一看可能有点复杂,但实际上并非如此。首先,必须声明一个 Style
元素,并带有 Key
和 TargetType
。在这种情况下,样式应用于选项菜单中的按钮,因此 Key
为 OptionsButton
,TargetType
为 Button
。然后通过声明一个 Setter
元素来设置属性,该元素定义要设置的属性和要使用的值。设置边距属性是一个简单的示例。但是,请注意设置按钮模板的惊人灵活性。每个按钮都可以由您想要的任何 UI 元素组成。在这种情况下,我使用一个 Rectangle
(包含在 Grid
中)作为我的按钮内容。我还将 Rectangle
的 Fill
属性设置为同一文件中定义的静态资源。这使我可以轻松地在鼠标悬停和鼠标按下事件中更改按钮的外观。这些由上面的 Trigger
元素控制。另请注意上面的 TemplateBindings。这些允许根据实际按钮属性设置属性。例如,在上面的示例中,Grid
采用了我们在 ButtonControl
类中按钮元素指定的。的高度。
最后一步是将用户控件添加到主窗口并在适当的时候隐藏或显示它。主窗口的 XAML 如下所示
<Window x:Class="ShapeShow.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:uc="clr-namespace:ShapeShow"
Title="ShapeShow" ResizeMode="NoResize"
WindowStyle="None" WindowState="Maximized" Topmost="True">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<TextBlock Foreground="Gray" Grid.Row="0"
Padding="10,0">Press Ctrl+Alt+O to show options.</TextBlock>
<Canvas x:Name="DrawingSurface" Grid.Row="1" />
<uc:OptionsControl x:Name="Options"
Grid.RowSpan="2" Visibility="Collapsed" />
</Grid>
</Window>
添加用户控件时,即使控件与本控件位于同一程序集中,也必须确保为其添加 XML 命名空间。请注意 xmlns:uc="clr-namespace:ShapeShow"
行。并且,确保窗口以全屏显示并置顶非常简单,只需将 WindowStyle
设置为 None
,将 WindowState
设置为 Maximized
,并将 TopMost
设置为 true
。另请注意第二个行定义的 Height="*"。这告诉第二行占据所有剩余空间。而这正是绘图表面的位置。最后,我们通过简单地为选项菜单指定 RowSpan 为 2,就可以使其显示在两行之上。
键盘挂钩
低级键盘挂钩不是本文的重点。在许多其他地方都有更好的解释。事实上,Emma 的文章(底部链接)对此进行了很好的介绍。不过,我将继续提供一些关于其工作原理的常规信息。
尽管我已经重写了 OnKeyDown
方法,但仍然存在一些我无法控制的按键组合。这些是系统键,例如 Windows 键或 Alt+Tab。为了阻止这些键被处理,我必须使用系统范围的低级键盘挂钩来捕获它们。这实际上很容易做到。所有需要的是一个处理按键事件的函数,以及对 user32.dll 函数的一些调用来设置挂钩。下面显示了这些函数
[DllImport("user32.dll", SetLastError = true)]
private static extern IntPtr SetWindowsHookEx(int idHook,
HookHandlerDelegate callbackPtr,
IntPtr hInstance, uint dwThreadId);
[DllImport("user32.dll", SetLastError = true)]
private static extern bool UnhookWindowsHookEx(IntPtr hhk);
[DllImport("user32.dll", SetLastError = true)]
private static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode,
IntPtr wParam, ref KBHookStruct lParam);
现在,我们只需要为挂钩定义一个回调函数,并将我们想要允许的按键传递过去。我们通过使用 CallNextHookEx
函数传递按键。我们可以通过简单地从函数返回而不调用 CallNextHookEx
来丢弃按键。回调函数必须具有如图所示的特定签名
private static IntPtr KeyboardHookHandler(int nCode, IntPtr wParam,
ref KBHookStruct lParam)
{
if (nCode == 0)
{
if (((lParam.vkCode == 0x09) && (lParam.flags == 0x20)) || // Alt+Tab
((lParam.vkCode == 0x1B) && (lParam.flags == 0x20)) || // Alt+Esc
((lParam.vkCode == 0x1B) && (lParam.flags == 0x00)) || // Ctrl+Esc
((lParam.vkCode == 0x5B) && (lParam.flags == 0x01)) || // Left Windows Key
((lParam.vkCode == 0x5C) && (lParam.flags == 0x01)) || // Right Windows Key
((lParam.vkCode == 0x73) && (lParam.flags == 0x20)) || // Alt+F4
((lParam.vkCode == 0x20) && (lParam.flags == 0x20))) // Alt+Space
{
return new IntPtr(1);
}
}
return CallNextHookEx(hookPtr, nCode, wParam, ref lParam);
}
就是这样
我们有了一个完整的全屏 WPF 应用程序。欢迎随时提问或提出建议。我希望在将来扩展此功能以显示其他形状,例如数字或字母。所以,如果您有任何其他有趣的想法,请告诉我。感谢您阅读我的第一篇 CodeProject 文章,希望您从中获得了一些有用的东西。
这是启发本文的 Emma Burrows 的文章链接 - 使用 C# 的低级 Windows API 挂钩来阻止不必要的按键。