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

WPF 应用程序的软件虚拟键盘

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.96/5 (89投票s)

2011 年 1 月 12 日

Ms-PL

19分钟阅读

viewsIcon

358581

downloadIcon

23024

介绍如何为输入非 ASCII 字符创建屏幕上的虚拟键盘。

 

Virtual-WPF/KBImage.png

引言

有时,为您的桌面应用程序配备一个基于软件的“虚拟键盘”会非常有帮助。您可以为用户提供一种输入其物理键盘上没有的字符的方式。或者,提供一种比其他应用程序更用户友好的方式。您还可以通过这种方式输入密码来规避隐秘的键盘记录器。并且,在书写其他语言的信件或帖文时,它可以节省大量时间。

因此,有了 JHVirtualKeyboard 项目。您可以在 此处 下载二进制文件,并在此 获取完整的源代码。我使用 Visual Studio 2010 构建了它:如果您确实需要 VS 2008 解决方案,请给我发电子邮件。

背景

Virtual-WPF/DemoApp.png

这是一个使用 Windows Presentation Foundation (WPF) 完成的有趣项目,我认为它会为您提供一篇有用的文章,因为它使用了多种设计技术。WPF 带来了一些意想不到的好处,例如您可以调整此窗口的大小,所有按键都会相应地调整大小。这要归功于 Grid 控件。当然,我认为 WPF(和 SilverLight)通常很有趣;它是目前首屈一指的 GUI 设计平台,我对它的创建者表示高度敬意。也就是说,这个项目确实付出了大量工作。让我换个说法:付出了大量的工作,才能做到尽善尽美。然后将其提炼成您面前这个较小应用程序的精华。我与您分享此项目,是为了节省您项目上的时间并说明相关技术。请在您的反馈中表现出宽容,并贡献您自己的改进,以便我们能够通过改进来发展它。如果您将您的建议发送给我,我将在下一个版本中将其编辑到代码中,并注明您为贡献者。集思广益总是优于独断独行。

我们将一步步讲解它的设计,轻松愉快地进行,并提供一个链接,您可以下载完整的演示应用程序和源代码,以便立即投入使用。

JHVirtualKeyboard 的运行

它是一个辅助的 WPF 窗口,如果您将其集成到您的应用程序中,它将允许您的用户调用它,并单击按键按钮将字符插入到您的应用程序文本字段中。

Virtual-WPF/DemoAppWithHands_919x449.jpg

我为其提供了几种语言模式作为起点。我最初的需求是开发一个用于学习各种语言术语(如阿拉伯语或俄语)的记忆闪卡程序,所以我首先创建了它。它还支持法语、德语和西班牙语,但这些语言实际上只添加了少数特定于地区的字符。也许您可以尝试实现一个中文键盘并发送给我以供包含 – 我很想看看!哈哈

您通过右下角的 ComboBox 选择键盘布局。更改语言会导致键盘自动更新。

我在维基百科等地方找到了大部分键盘布局信息。不过我必须警告您:那很容易让您分心,最终深入研究连字和语言学的历史根源。但无论如何,我提倡通过标准化来实现简化。因此,我试图选择最适合给定语言/文化的标准布局,但不会做得太过分以至于损害标准的美国键盘本身。我可以看到,对于某些语言来说,这并不是一个容易取得的平衡。根据我的推理,应该选择最能接近母语者习惯的布局,同时又不能超出标准键盘布局的限制。毕竟,我们不是在为 GUI 选择一个文化/语言。那完全是另一个话题了。

总之……

作为进一步输入的辅助,在底部边缘有十三个额外的排版符号供您输入。我经常需要它们。

在上面图片中的演示应用程序中,您可以看到有两个文本字段。一个是 TextBox,另一个是 RichTextBox。虚拟键盘 (VK) 对两者都一样工作。无论哪个文本字段当前具有焦点,它都会接收 VK 中输入的任何内容。

键盘具有保存其状态的机制,下次调用时它将预设为上次使用的语言,并且它还会记住您上次退出应用程序时是否处于显示状态,以便下次可以自动启动。

为了进一步方便,只是为了好玩——当您在屏幕上移动应用程序时,此键盘窗口可以与之同步移动。

按键的 ToolTips 是为了方便用户识别按键。我曾想在其中插入每个字形的全部历史。但是,此应用程序/工具是为了帮助当前文化的用户输入来自外国字母(相对于该用户)的字符。因此,ToolTips(默认为英文)用于识别这些字形。对于此应用程序,这些 ToolTips 可能很重要。

当您单击 CAPS LOCK 键时,所有字母将变为大写。它会一直保持此状态,直到您再次单击 CAPS LOCK。

当您单击 SHIFT LOCK 键时,除了大写字母外,您还可以访问按键上的移位状态符号。这与物理键盘的功能相同。在这种情况下,经过十秒钟的超时后,它会回落到未移位状态。

Control 和 Alt 键不起作用。我只是保留它们,使其看起来更像标准的(美国英语)键盘。但是您可以为某些键盘使用的语言(例如死字符键)使用它们。或者将其删除。

平台

这是使用 Visual Studio 2010 在 Windows 7 x64 上创建的,目标是 .NET Framework 4。我没有在早期版本上进行测试。我坚信让供应商不断发展我们的工具并利用最新版本。如果您仍然坚持使用 1.0 版本,因为您过于保守——我只能说:走开!我有一个独立的库,此项目和其他所有项目都使用该库,但我已为此项目的下载精简到最少的使用量,并将其包含在您的源代码下载中。

我将配置设置为目标“Any CPU”。这花了一些时间进行调整。我不确定原因。它一直自己切换回 x64。当 VS 2010 在 Windows x64 上似乎无法识别您的某些项目时,请检查构建配置。如果一个项目是 x86 而另一个是 x64,它们肯定会相互冲突!可能需要多次尝试才能使设置生效。

我相信您想知道如何在自己的程序中使用它。然后我们将讨论它设计中的一些有趣方面,最后将逐步讲解代码本身。

在您的应用程序中使用它

要告诉您如何在自己的设计中使用 VK 的最简单方法是向您展示一个执行此操作的简单演示应用程序。让我们分三个步骤来了解它。

在您的应用程序中使用它很简单。随下载附带的演示应用程序是一个精简的 WPF 应用程序,其中包含一个简单的 TextBox 用于接收字符,以及一个用于启动虚拟键盘的 Button。另外,我添加了第二个文本控件,一个 RichTextBox,用于说明使用 VK 输入多个输入字段。

这是演示应用程序的 XAML

    <DockPanel LastChildFill="True">
         <StackPanel Orientation="Horizontal" HorizontalAlignment="Right" 
             DockPanel.Dock="Bottom" Margin="0,8,10,8">
             <Button Name="btnVK" Margin="5,0" Width="100" Click="btnVK_Click">
                 Virtual Keyboard</Button>
             <Button Name="btnClose" Margin="5,0" Width="100" IsCancel="True" 
                 Click="btnClose_Click">Exit</Button>
         </StackPanel>
         <Grid>
             <Grid.RowDefinitions>
                 <RowDefinition Height="Auto"/>
                 <RowDefinition Height="*"/>
                 <RowDefinition Height="Auto"/>
                 <RowDefinition Height="*"/>
                 <RowDefinition Height="Auto"/>
             </Grid.RowDefinitions>
             <Label Name="lblTextBox" Margin="25,0,0,0" Padding="5,5,5,0">TextBox:</Label>
             <TextBox Name="txtBox" Margin="20,0,20,10" Grid.Row="1" 
                 GotFocus="txtBox_GotFocus" />
             <Label Name="lblRichTextBox" Margin="25,0,0,0" Padding="5,5,5,0" 
                 Grid.Row="2">RichTextBox:</Label>
             <RichTextBox Name="txtrichBox" Margin="20,0,20,5" Grid.Row="3" 
                 GotFocus="txtrichBox_GotFocus" />
         </Grid>
     </DockPanel>
 </Window>

主要有一个 TextBox 和一个 RichTextBox。第一行的 DockPanel 仅用于方便地将按钮沿底部排列。我习惯于在创建新窗口时总是添加 DockPanel。请注意,当您将 Label 放在另一个字段上方时,如果您想让它足够靠近,您需要将它的底部内边距和底部外边距设置为零。其他侧面的内边距值保持默认的 5。在某些情况下,我不得不使用负的底部外边距才能使其尽可能靠近我想要的距离。我曾尝试用同样的办法对待剧院里的女孩,但没有取得积极的成果。

这两个按钮都有 click-handlers,用于启动虚拟键盘并关闭应用程序。

我在所有项目中都使用一种特定的编码格式标准,我在 此处 详细说明了这一点。

让我们看一下 MainWindow 的代码隐藏

   1. using System;
   2. using System.Windows;
   3. using System.Windows.Controls;
   4. using JHVirtualKeyboard;
   5.  
   6. namespace JHVirtualKeyboardDemoApp
   7. {
   8.     public partial class MainWindow : Window, IVirtualKeyboardInjectable
   9.     {
  10.         public MainWindow()
  11.         {
  12.             InitializeComponent();
  13.             // Remember which text field has the current focus.
  14.             _txtLastToHaveFocus = txtBox;
  15.             // Set up our event handlers.
  16.             this.ContentRendered += OnContentRendered;
  17.             this.LocationChanged += OnLocationChanged;
  18.             this.Closing += new System.ComponentModel.CancelEventHandler(OnClosing);
  19.         }

您需要为 JHVirtualKeyboard 命名空间添加一个 using pragma,并实现上面所示的 IVirtualKeyboardInjectable 接口。该接口是您与虚拟键盘 (VK) 的合同,以便它们可以进行互操作。

请注意第 14 行我们正在做什么:我们有一个实例变量 _txtLastToHaveFocus,它指向当前具有焦点的任何文本字段。我们使用它来告诉 VK 在哪里放置它的字符。

同时请注意,我们在这里定义了三个事件处理程序。让我们来看看它们。

ContentRendered 事件处理程序

   1.         void OnContentRendered(object sender, EventArgs e)
   2.         {
   3.             // If the Virtual Keyboard was up (being shown) when this
   4.             // application was last closed, show it now.
   5.             if (Properties.Settings.Default.IsVKUp)
   6.             {
   7.                 ShowTheVirtualKeyboard();
   8.             }
   9.             // Put the initial focus on our first text field.
  10.             txtBox.Focus();
  11.         }

这里有 3 件事要做。

  1. 如第 5 行所示,它会记住我们在上次运行此应用程序时 VK 是否处于打开状态。要实现这一点,您只需在您的设置中添加一个布尔标志;在这种情况下,我们称之为 IsVKUp
  2. 使用 ShowTheVirtualKeyboard 方法调用 VK。这被放在 ContentRendered 处理程序中,因为此时您的窗口已经渲染完毕,VK 可以知道其位置。
  3. 在第 10 行,我们将焦点设置到 TextBox。因此,您仍然会在文本字段中看到闪烁的光标,就像没有 VK 一样。

这是 ShowTheVirtualKeyboard 方法

   1. public void ShowTheVirtualKeyboard()
   2. {
   3.     // (Optional) Enable it to remember which language it was set to last time,
          // so that it can preset itself to that this time also.
   4.     VirtualKeyboard.SaveStateToMySettings(Properties.Settings.Default);
   5.     // Show the keyboard.
   6.     VirtualKeyboard.ShowOrAttachTo(this, ref _virtualKeyboard);
   7. }

这是我们启动 VK 的地方。您在这里做了两件事。

  1. 通过调用 SaveStateToMySettings,您告诉它如何将其状态保存到您的应用程序设置中。这是可选的。
  2. 您调用 ShowOrAttachTo 来启动 VK。您将 this 指针传递给它,以便它知道其所有者窗口是谁。您将它指向本地的 _virtualKeyboard 实例变量,以便您有一个对它的引用。

您需要实现 IVirtualKeyboardInjectable 接口。演示应用程序的实现方式如下

  public System.Windows.Controls.Control ControlToInjectInto
  {
    get { return _txtLastToHaveFocus; }
  }

通过实现 ControlToInjectInto,您告诉 VK 将用户输入的字符发送到哪里。

如果您只有一个文本字段,您只需返回该字段(字段的实例变量)。这里我们有两个文本字段。所以我们而是通过这个实例变量跟踪哪个字段具有焦点,并在该属性的 getter 中返回它。

让我们来看看另一个很酷的东西……

   1. void OnLocationChanged(object sender, EventArgs e)
   2. {
   3.     // Do this if, when your user moves the application window around the screen,
   4.     // you want the Virtual Keyboard to move along with it.
   5.     if (_virtualKeyboard != null)
   6.     {
   7.         _virtualKeyboard.MoveAlongWith();
   8.     }
   9. }

如果您像这样处理您窗口的 LocationChanged 事件,那么 VK 将会随着您的窗口一起移动,就像粘在它上面一样。您仍然可以将其定位在其他位置。通常,我会在我的应用程序的选项对话框中包含一个复选框,让用户能够打开或关闭此功能。MoveAlongWith 是来自 WPFExtensions.cs 文件的窗口扩展方法。

哦,对了……那个启动它的点击处理程序。没什么特别的……

   1. #region btnVK_Click
   2. /// <summary>
   3. /// Handle the event that happens when the user clicks on the
      /// Show Virtual Keyboard button.
   4. /// </summary>
   5. private void btnVK_Click(object sender, RoutedEventArgs e)
   6. {
   7.     ShowTheVirtualKeyboard();
   8. }
   9. #endregion

这是 Closing 事件处理程序

   1.         void OnClosing(object sender, System.ComponentModel.CancelEventArgs e)
   2.         {
   3.             bool isVirtualKeyboardUp = _virtualKeyboard != null && 
                      VirtualKeyboard.IsUp;
   4.             Properties.Settings.Default.IsVKUp = isVirtualKeyboardUp;
   5.             Properties.Settings.Default.Save();
   6.         }

这只会将您 VK 的打开/关闭状态保存到您应用程序的设置中。

跟踪哪个字段具有焦点

此演示应用程序有两个文本字段,以便您可以看到如何使用 VK 为多个控件提供输入。

要实现这一点,您需要的东西很简单。我们添加了实例变量 _txtLastToHaveFocus。此变量必须始终指向当前在您窗口中拥有焦点的字段。我们如何做到这一点?

很简单。我们将处理程序挂接到所有文本字段的 GotFocus 事件,如下所示……

  private void txtBox_GotFocus(object sender, RoutedEventArgs e)
  {
      // Remember that the plain TextBox was the last to receive focus.
      _txtLastToHaveFocus = txtBox;
  }
 
  private void txtrichBox_GotFocus(object sender, RoutedEventArgs e)
  {
      // Remember that the RichTextBox was the last to receive focus.
      _txtLastToHaveFocus = txtrichBox;
  }

结果是,该变量始终指向正确的字段。

设置

我提到您需要向您的应用程序设置添加内容

Virtual-WPF/Settings.png

您可以在这里看到我设置了两个设置。布尔设置 IsVKUp,其名称可以随您喜欢。但是,指定语言的那个必须命名为“VKLayout”,否则 VK 将看不到它。

这就是如何在您自己的应用程序中使用它的全部内容。

让我们稍微谈谈设计。

虚拟键盘的设计

我非常喜欢面向对象的設計,尤其是在它能够节省时间并产生更优雅的创作时。

在这里,我们有键盘布局。这些信息可能相当详细。五十多个按键根据语言/字母表而变化,还有工具提示,移位/未移位状态下则加倍,然后您必须区分大写字母和小写字母,与区分移位/未移位符号不同。并且可能有数百种可能的键盘布局。

另一方面,其中一些有很多共同之处。而且,考虑到标准美式英语键盘的核心作用,我将键盘布局层次结构的根类建立在美国英语键盘之上,以提供默认属性。

因此,我们有了一个概念性的方案,它需要一个面向对象的类层次结构。在顶部,根类是美国键盘布局。其他一切要么从它继承,要么覆盖它。在此之下,第二层是主要变体。俄语(也许应该称为西里尔语)、西班牙语等。在此之下,可能还有它们的进一步变体。

考虑到例如西班牙语键盘只改变少数东西,但希望保持其他所有东西都与美式英语键盘一样,数据经济性就体现出来了。所以,如果我们能让西班牙语键盘继承所有美式英语键盘的功能,它只需要覆盖它想要更改的按键(及其工具提示等)。理想的 OOP 应用。

要将此方案应用到我们基于 WPF XAML 的应用程序,其中包含五十个数据绑定的按钮,请检查……

Virtual-WPF/ClassDiagram.png

如果您在 Visual Studio (VS) 中查看 KeyAssignmentSet 类,您会发现它有一个非常大的实例变量和属性集合。键盘的每个按键按钮都有一个。我将它们命名为“VK_A”、“VK_OEM1”等,以匹配 Win32 定义。

KeyAssignmentSet 类代表美式英语键盘布局。

子类 SpanishKeyAssignmentSet 只覆盖它想要更改的属性。在这种情况下,只需七个就能完成。这比提供所有 50 多个按键定义要容易得多。太棒了。

现在,要使其与我们的 XAML 设计协同工作,需要一些技巧。WPF 喜欢稳定且可供查看的视图模型。VK 本身有一个主视图模型类:KeyboardViewModel。所有按键按钮都有作为 KeyboardViewModel 成员的视图模型对象。因此,要将适当的 KeyAssignmentSet 应用于该视图模型,我们有一个单独的按键视图模型的数组,我们遍历这些数组,将 KeyAssignmentSet 中的信息分配给它们。

要做到这一点,请查看 VirtualKeyboard.xaml.cs 中的 AssignKeys 方法

作为响应,WPF 视图会自动更新以反映新的布局。

在此代码中,我使用“CodePoint”一词来指代 Unicode 标准中的一个位置,该位置由一个特定的数字表示,具体取决于您使用的字体——它会映射到产生的字形。Unicode 是一个宝贵的资源;投入了大量工作来从世界各种语言和字母的混乱中理出秩序并进行管理。在这里,我使用“shifted CodePoint”一词来指代按键按钮上半部分的字符,例如常在数字 8 键上方的星号。

这是显示按键名称的布局

Virtual-WPF/VKwKeynames100.png

从 VK 的 XAML 中可以得到一些观察结果……

   1.     xmlns:jhlib="clr-namespace:JHLib;assembly=JHLib"
   2.     xmlns:vk="clr-namespace:JHVirtualKeyboard"
   3.     mc:Ignorable="d"
   4.     jhlib:WindowSettings.SaveSize="True"

我引入了外部实用程序库的命名空间,并将其分配给 XML 命名空间前缀“jhlib”。然后,我对 JHVirtualKeyboard 程序集也做了同样的处理,并将其分配给“vk”。请注意语法是如何变化的:一个是外部程序集,另一个是本地程序集,因此不需要“;assembly=”部分。如果您不正确处理其中一项,您将花费大量时间来弄清楚为什么您的 XAML 无法识别您 trunk 中的内容。

我将 WindowStyle 属性设置为 ToolWindow,将 FontFamily 设置为 Arial Unicode MS,因为这似乎能很好地覆盖非拉丁字符的代码点。

让我们看看其中一个按键按钮的定义

<Button Name="btnSectionMark"
        Command="vk:VirtualKeyboard.KeyPressedCommand"
        CommandParameter="§"
        ToolTip="Section sign, or signum sectionis"
        Margin="6,0,2,1"
        Width="25" Height="24">§</Button>

这个按钮输入了分节符(如果您想听起来像个笨蛋,也可以称之为“signum sectionis”)。所有按键按钮都使用相同的 KeyPressedCommand,命令参数携带要注入的字符。大部分格式设置都在 Style 中,但这个按钮覆盖了 Margin、Width 和 Height 以微调外观。

目前,在 LiveWriter 中查看此 XAML 时,按钮的内容显示的是十六进制 00A7 值的语法,带有一个 ampersand、pound、x 前缀和分号后缀。但同时,我在 Firefox 中在线查看的已发布版本,显示的是实际的分节符。嗯……

我为按键按钮使用样式,这样我就不必输入大量标记……

 <Style TargetType="{x:Type Button}">
     <Setter Property="Width" Value="19"/>
     <Setter Property="Height" Value="23"/>
     <Setter Property="FontSize" Value="18"/>
     <Setter Property="Padding" Value="0"/>
     <Setter Property="FontFamily" Value="Bitstream Cyberbase, Roman" />
     <Setter Property="Margin" Value="1,0,2,1"/>
     <Setter Property="Effect">
         <Setter.Value>
             <DropShadowEffect Direction="315" Opacity="0.7"/>
         </Setter.Value>
     </Setter>
     <Style.Triggers>
         <Trigger Property="IsPressed" Value="True">
             <Setter Property="Foreground" Value="{StaticResource brushBlue}"/>
             <!– Shift the button downward and to the right slightly, 
                 to give the affect of being pushed inward. –>
             <Setter Property="Margin" Value="2,1,0,0"/>
             <Setter Property="Effect">
                 <Setter.Value>
                     <DropShadowEffect Direction="135" Opacity="0.5" ShadowDepth="2"/>
                 </Setter.Value>
             </Setter>
         </Trigger>
     </Style.Triggers>
 </Style>

此 Style 针对所有 Buttons,因此它成为该容器内所有 Buttons 的默认样式。

我不得不调整和微调尺寸、内边距和外边距,以使其看起来最好。

注意 FontFamily:“Bitstream Cyberbase, Roman”。我购买它是因为它覆盖了代码点。

此 Style 中最有趣的东西(至少对我来说)是让按键按钮在视觉上稍微脱离表面,然后在您单击它们时向下(并稍微向右)移动,从而产生可爱的 3D 效果。从阅读 XAML 可以看出,当 IsPressed 属性变为 true 时,触发器会触发。它通过设置 margin 属性来改变其位置,使其向下和向右移动,并更改 DropShadowEffect 以获得 3D 效果。

这是主按键按钮的定义……

<Button Name="btnVK_OEM_3" Grid.Column="0" Content="{Binding Path=VK_OEM_3.Text}" 
    ToolTip="{Binding Path=VK_OEM_3.ToolTip}"/>

<Button Name="btnVK_1" Grid.Column="1" Content="{Binding Path=VK_1.Text}" 
    ToolTip="{Binding Path=VK_1.ToolTip}"></Button>

<Button Name="btnVK_2" Grid.Column="2" Content="{Binding Path=VK_2.Text}" 
    ToolTip="{Binding Path=VK_2.ToolTip}"></Button>

<Button Name="btnVK_3" Grid.Column="3" Content="{Binding Path=VK_3.Text}" 
    ToolTip="{Binding Path=VK_3.ToolTip}"></Button>

<Button Name="btnVK_4" Grid.Column="4" Content="{Binding Path=VK_4.Text}" 
    ToolTip="{Binding Path=VK_4.ToolTip}"></Button>

此窗口的 DataContext 是 KeyboardViewModel。如您所见,这些 Buttons 的 Content 设置为 KeyboardViewModel 的 VK_1、VK_2 等成员的 Text 属性。每个按键按钮都实现了 INotifyPropertyChanged,以便在值更改时保持 GUI 的更新。基类 BaseViewModel 为我们实现了这一点。

ToolTip 属性也绑定到视图模型。每个 KeyViewModel 对象(即 VK_1 等)都有一个 ToolTip 属性。

对我来说,一个小的复杂之处是,Text 属性和 ToolTip 需要根据移位键是否生效而产生不同的值。

   1. public string ToolTip
   2. {
   3.     get
   4.     {
   5.         if ((s_domain.IsShiftLock || (_isLetter && s_domain.IsCapsLock)) && 
                  !String.IsNullOrEmpty(_shiftedToolTip))
   6.         {
   7.             return _shiftedToolTip;
   8.         }
   9.         else
  10.         {
  11.             return _toolTip;
  12.         }
  13.     }
  14. }

在这里,s_domain 是一个指向我们 KeyboardViewModel 的静态变量。它在未移位和已移位的 ToolTip 值之间进行选择。

Text 属性的运作方式类似,不同之处在于,如果该键的 _text 实例变量已被显式设置,它将从中选择,否则它将返回分配给该键的未移位或已移位的代码点。

显示 VK

这需要一些技巧。可能有一种更好的方法,但这是我能够实现的方法。如果您知道更好的方法,请分享。

   1. public static void ShowOrAttachTo(IVirtualKeyboardInjectable targetWindow, 
          ref VirtualKeyboard myPointerToIt)
   2. {
   3.     try
   4.     {
   5.         s_desiredTargetWindow = targetWindow;
   6.         // Evidently, for modal Windows I can't share user-focus with another
              // Window unless I first close and then recreate it.
   7.         // A shame. Seems like a waste of time. But I don't know of a
              // work-around to it (yet).
   8.         if (IsUp)
   9.         {
  10.             Console.WriteLine("VirtualKeyboard: re-attaching to a different Window.");
  11.             VirtualKeyboard.The.Closed += new EventHandler(OnTheKeyboardClosed);
  12.             VirtualKeyboard.The.Close();
  13.             myPointerToIt = null;
  14.         }
  15.         else
  16.         {
  17.             myPointerToIt = ShowIt(targetWindow);
  18.         }
  19.     }
  20.     catch (Exception x)
  21.     {
  22.         Console.WriteLine("Exception in VirtualKeyboard.ShowOrAttachTo: " + x.Message);
  23.         // Below, is what I normally use as my standard for raising
              // objections within library routines (using my own std MessageBox substitute).
  24.         //IInterlocution inter = Application.Current as IInterlocution;
  25.         //if (inter != null)
  26.         //{
  27.         //    inter.NotifyUserOfError("Well, now this is embarrassing.", 
              //    "in VirtualKeyboard.ShowOrAttachTo.", x);
  28.         //}
  29.     }
  30. }

是的,它不像我们期望的那样是一个简单的 ShowDialog 调用,对吧?

复杂之处在于:如果它已经显示,但被另一个窗口拥有,而当前窗口(您实际上要让它使用 VK 的窗口)是以模态方式启动的,它就不能简单地“接管”VK 窗口的所有权。我唯一能起作用的方法是关闭 VK,然后重新启动它。

因此,在这里我们指示 VK 关闭自身,并挂接到 Closed 事件,以便在 VK 关闭后调用另一个方法。该方法随后会重新启动 VK。

一些可用性测试表明,您的用户在尝试使用鼠标输入内容时,更喜欢一个可以自行重置的移位键。因此,单击任一移位键会将 VK 推入移位状态,然后单击任何字符会将 VK 弹出到未移位状态。但如果用户延迟,它会在十秒后自行重置。以下是实现这一点的代码……

   1. public void PutIntoShiftState()
   2. {
   3.     // Toggle the shift-lock state.
   4.     _domain.IsShiftLock = !_domain.IsShiftLock;
   5.     // If we're turning Shiftlock on, give that a 10-second timeout before
          // it resets by itself.
   6.     if (_domain.IsShiftLock)
   7.     {
   8.         ClearTimer();
   9.         _resetTimer = new DispatcherTimer(TimeSpan.FromSeconds(10),
  10.                                           DispatcherPriority.ApplicationIdle,
  11.                                           new EventHandler(OnResetTimerTick),
  12.                                           this.Dispatcher);
  13.     }
  14.     SetFocusOnTarget();
  15. }

请注意,每次用户单击 VK 中的任何内容时,VK 的窗口都会获得焦点。这不是我们想要的。因此,我们后面会调用 SetFocusOnTarget,它会将焦点重新定向回您的文本字段。

执行实际的按键字符注入

这是实际将字符插入目标文本字段的方法……

   1. protected void Inject(string sWhat)
   2. {
   3.     if (TargetWindow != null)
   4.     {
   5.         ((Window)TargetWindow).Focus();
   6.         TextBox txtTarget = TargetWindow.ControlToInjectInto as TextBox;
   7.         if (txtTarget != null)
   8.         {
   9.             txtTarget.InsertText(sWhat);
  10.         }
  11.         else
  12.         {
  13.             RichTextBox richTextBox = 
                      TargetWindow.ControlToInjectInto as RichTextBox;
  14.             if (richTextBox != null)
  15.             {
  16.                 richTextBox.InsertText(sWhat);
  17.             }
  18.             else // let's hope it's an IInjectableControl
  19.             {
  20.                 IInjectableControl targetControl = 
                          TargetWindow.ControlToInjectInto as IInjectableControl;
  21.                 if (targetControl != null)
  22.                 {
  23.                     targetControl.InsertText(sWhat);
  24.                 }
  25.             }
  26.             //else   
  // if you have other text-entry controls such as a rich-text box, include them here.

这部分可能需要仔细考虑。我为两种可能的控件提供了支持:TextBoxRichTextBox

我调用您的 ControlToInjectInto 方法来获取要注入字符的引用。我尝试通过先转换为一种类型再转换为另一种类型来查找它。

对于这两者,我都定义了一个扩展方法 InsertText,用于执行实际的文本插入。这出奇地并不简单。

为了容纳您自定义文本框的创建者,我还定义了一个接口 IInjectableControl。如果您有一个既不是 TextBox 也不是 RichTextBox 的文本字段,如果您能让您的控件实现此接口,它仍然可以工作。否则,您将不得不修改此代码才能使其为您工作。好吧,这就是一个完整源代码项目的好处。您需要在此处为您的控件编写代码,也需要在 DoBackSpace 方法中编写代码——顺便说一句,它使用内置的编辑命令 EditingCommands.Backspace 来执行 BACKSPACE 操作。实际上,直接操作文本更简单。但我希望尝试命令方法。因此,我在此处向此控件添加了一个命令绑定,使用它来执行 Backspace 操作,并将其保留,直到键盘关闭,届时我们将其清除。

扩展方法

这是 TextBoxInsertText 扩展方法,您可以在 JLib.WPFExtensions.cs 中找到它

   1. public static void InsertText(this System.Windows.Controls.TextBox textbox,
       string sTextToInsert)
   2. {
   3.     int iCaretIndex = textbox.CaretIndex;
   4.     int iOriginalSelectionLength = textbox.SelectionLength;
   5.     string sOriginalContent = textbox.Text;
   6.     textbox.SelectedText = sTextToInsert;
   7.     if (iOriginalSelectionLength > 0)
   8.     {
   9.         textbox.SelectionLength = 0;
  10.     }
  11.     textbox.CaretIndex = iCaretIndex + 1;
  12. }

是的,看起来有点冗长。

这是 RichTextBox 的 InsertText 扩展方法

   1. public static void InsertText(this System.Windows.Controls.RichTextBox richTextBox,
      string sTextToInsert)
   2. {
   3.     if (!String.IsNullOrEmpty(sTextToInsert))
   4.     {
   5.         richTextBox.BeginChange();
   6.         if (richTextBox.Selection.Text != string.Empty)
   7.         {
   8.             richTextBox.Selection.Text = string.Empty;
   9.         }
  10.         TextPointer tp = richTextBox.CaretPosition.GetPositionAtOffset(0,
                  LogicalDirection.Forward);
  11.         richTextBox.CaretPosition.InsertTextInRun(sTextToInsert);
  12.         richTextBox.CaretPosition = tp;
  13.         richTextBox.EndChange();
  14.         Keyboard.Focus(richTextBox);
  15.     }

这花了一些时间才得到一个简单的字符插入。它不像简单地追加文本那样简单:如果插入点不在末尾,您必须在插入点处插入并将所有内容向右滑动(当然,这是指数组索引)。

项目代码包括一个我通常用于 WPF 桌面应用程序的库 JhLib。它们在我系统上的位置如下:
C:\DesktopAppsVS2010\JhVirtualKeyboard
C:\DesktopAppsVS2010\Libs\JhLib

这些信息应该有助于您在磁盘上正确管理项目布局。

结论 

就这样,您拥有了一个使用 C# 和 WPF 创建的、工作的屏幕虚拟键盘。我希望这对您有用,并且您会给我您的想法和建议。我觉得 WPF 很有趣:现在它感觉如此自然,以至于我讨厌使用其他任何东西来做桌面 GUI 应用程序。但总有新的东西需要学习。

诚挚地,

James Witt Hurst

© . All rights reserved.