Silverlight 中的虚拟键盘 (TabTip)






4.20/5 (3投票s)
使用提升的信任在 Silverlight 中实现对虚拟键盘 (TabTip) 的支持
引言
有时候你不得不做一些奇怪的事情。在这种情况下,我面临着在 Windows 8/8.1 的 Silverlight 中实现 TabTip(虚拟键盘)支持的任务。我对此几乎找不到任何资料,所以我自己实现了解决方案,并在本文中将描述这项任务带来的挑战以及我处理它们的方式。
先决条件
- 提升信任。
在本文中,我将积极使用 COM 自动化以及提升信任赋予我们的其他一些功能。无论是在浏览器内还是在浏览器外(我个人在浏览器内启用了它),这都没有太大关系,但没有提升信任,处理 TabTip 几乎是不可能的。如何启用提升信任,你可以在任何一本严肃的 Silverlight 书籍中找到,或者只需运用你的谷歌搜索技能,所以我不会在这里介绍,但如果你好奇,它相当容易。
- Microsoft.CSharp
我们需要添加对 Microsoft.CSharp 的引用,因为要使用 COM 自动化,我们需要使用 `dynamic` 关键字。
- System.Windows.Interactivity
主要逻辑将封装在自定义行为中,为此我们需要 System.Windows.Interactivity 引用。
行为
为了实现一次编写代码并在需要 TabTip 的每个控件上使用它,我选择通过自定义行为来实现。类的声明如下:
public class ControlTabTipBehavior : Behavior<Control>
正如你应该知道的,在行为中,我们需要重写两个方法,即 `OnAttached` 和 `OnDetaching`。
protected override void OnAttached()
{
if (Application.Current.HasElevatedPermissions && AutomationFactory.IsAvailable)
{
AssociatedObject.LostFocus += AssociatedObject_LostFocus;
AssociatedObject.GotFocus += AssociatedObject_GotFocus;
}
}
protected override void OnDetaching()
{
if (Application.Current.HasElevatedPermissions && AutomationFactory.IsAvailable)
{
base.OnDetaching();
AssociatedObject.LostFocus -= AssociatedObject_LostFocus;
AssociatedObject.GotFocus -= AssociatedObject_GotFocus;
}
}
代码将在文章中发生变化,但你应该从这些方法中了解到的主要内容是,事情将在 `GotFocus` 和 `LostFocus` 事件处理程序中发生。
打开 TabTip
我们将使用 COM 自动化和 `Shell.Application` 对象的 `ShellExecute` 方法来打开 TabTip。
// This path is the same for both 32 and 64-bit installs of Windows static string TabTipFilePath = @"C:\Program Files\Common Files\microsoft shared\ink\TabTip.exe"; static dynamic shellApplication = null; static dynamic ShellApplication { get { return (shellApplication != null) ? shellApplication : shellApplication = GetShellApplication(); } } private static dynamic GetShellApplication() { return AutomationFactory.CreateObject("Shell.Application"); } private static void OpenTabTip() { ShellApplication.ShellExecute(TabTipFilePath, "", "", "open", 1); }
关闭 TabTip
要关闭 TabTip,我们将使用一些 P/Invoke 技巧。
[DllImport("user32.dll")]
public static extern IntPtr FindWindow(String sClassName, String sAppName);
[return: MarshalAs(UnmanagedType.Bool)]
[DllImport("user32.dll", SetLastError = true)]
public static extern bool PostMessage(int hWnd, uint Msg, int wParam, int lParam);
private static void CloseTabTip()
{
uint WM_SYSCOMMAND = 274;
uint SC_CLOSE = 61536;
IntPtr KeyboardWnd = FindWindow("IPTip_Main_Window", null);
PostMessage(KeyboardWnd.ToInt32(), WM_SYSCOMMAND, (int)SC_CLOSE, 0);
}
检查键盘是否已连接
正如你可能猜到的,`OpenTabTip` 将在 `GotFocus` 事件处理程序中调用,但我们是否每次 `AssociatedObject` 获得焦点时都要打开 TabTip?我的猜测是,用户在未连接硬件键盘时才会需要 TabTip,否则它应该保持关闭状态。因此,需要以下代码:
static bool HardwareKeyboardConnected;
static dynamic wmiService = null;
static dynamic WMIService { get { return (wmiService != null) ? wmiService : wmiService = GetWMIService(); } }
private static dynamic GetWMIService()
{
using (dynamic SWbemLocator = AutomationFactory.CreateObject("WbemScripting.SWbemLocator"))
{
SWbemLocator.Security_.ImpersonationLevel = 3;
SWbemLocator.Security_.AuthenticationLevel = 4;
return SWbemLocator.ConnectServer(".", @"root\cimv2");
}
}
private static void CheckIfHardwareKeyboardConnected()
{
new Thread(() =>
{
dynamic keyboards = WMIService.ExecQuery(@"Select * from Win32_Keyboard");
if (keyboards.Count() == 0)
HardwareKeyboardConnected = false;
else if (keyboards.Count() == 1) // Check for Tablets
{
foreach (dynamic keyboard in keyboards)
HardwareKeyboardConnected = !(keyboard.Description == "Keyboard HID");
}
else
HardwareKeyboardConnected = true;
}).Start();
}
这段代码需要一些解释。
首先,我在 `CheckIfHardwareKeyboardConnected` 方法中启动了一个新线程。这是必需的,因为查询 `Win32_Keyboard` 需要一些时间来执行,而且 `CheckIfHardwareKeyboardConnected` 不会只调用一次。我们希望在 `AssociatedObject_GotFocus` 中调用它,因为用户可以随时连接或断开键盘。我们还将在第一个控件附加时调用一次。
其次,以下代码可能会引起一些疑问:
else if (keyboards.Count() == 1) // Check for Tablets
{
foreach (dynamic keyboard in keyboards)
HardwareKeyboardConnected = !(keyboard.Description == "Keyboard HID");
}
我发现 Windows 平板电脑上有一个键盘始终存在。我不太清楚为什么会这样,但我们必须处理它。因此,需要这段代码。我通过其描述来区分这个键盘,但你可能需要检查你的语言中该描述是否与我写的相同(“Keyboard HID”)。
计算和动画
现在我们可以当 `AssociatedObject` 获得焦点时打开 TabTip,并在失去焦点时关闭它,这很酷。但这真的足够吗?在平板电脑上,TabTip 占据了屏幕的一半,而屏幕下半部分的所有内容将不可见。那么我们该怎么办?计算和动画!
获取 `AssociatedObject` 的视觉根
首先,我们需要知道 `AssociatedObject` 在哪里,即它是在 `Application.Current.RootVisual` 中还是在某种子窗口中。
public static DependencyObject GetVisualRoot(this DependencyObject element)
{
DependencyObject RootCandidate = VisualTreeHelper.GetParent(element);
if (RootCandidate != null)
return RootCandidate.GetVisualRoot();
else return element;
}
计算 `AssociatedObject` 的 Y 轴偏移量
现在是时候计算我们的 `AssociatedObject` 在 Y 轴上的确切位置,它是否可见,如果不可见,我们需要移动视觉根多少才能使其可见。
static double RootVisualYOffset = 0;
private double GetNewRootVisualYOffsetForAssociatedObject()
{
double NewRootVisualYOffset = RootVisualYOffset;
double VisibleAreaTop = 0 - RootVisualYOffset;
double VisibleAreaBottom = ((Application.Current.RootVisual as FrameworkElement).ActualHeight / 2) - RootVisualYOffset - 10;
GeneralTransform gt = AssociatedObject.TransformToVisual(AssociatedObject.GetVisualRoot() as FrameworkElement);
Point offset = gt.Transform(new Point(0, 0));
double AssociatedObjectTop = offset.Y;
double AssociatedObjectBottom = AssociatedObjectTop + AssociatedObject.ActualHeight;
if ((AssociatedObjectBottom <= VisibleAreaBottom) && (AssociatedObjectTop >= VisibleAreaTop))
return RootVisualYOffset;
else if (AssociatedObjectBottom > VisibleAreaBottom)
{
double delta = (VisibleAreaBottom - AssociatedObjectBottom - 10);
if (AssociatedObjectTop > (VisibleAreaTop - delta))
NewRootVisualYOffset = RootVisualYOffset + delta;
else
NewRootVisualYOffset = RootVisualYOffset + (VisibleAreaTop - AssociatedObjectTop + 10);
}
else if (AssociatedObjectTop < VisibleAreaTop)
NewRootVisualYOffset = RootVisualYOffset + (VisibleAreaTop - AssociatedObjectTop + 10);
return NewRootVisualYOffset;
}
这里我们做了一些简单的数学计算,但有一些事情我想指出。
可见区域的顶部和底部的计算是基于浏览器全屏且你的应用程序占用所有可用空间的假设。还有一个假设是,这个 TabTip 功能是针对平板电脑的,平板电脑相对较小,而且 TabTip 占据了屏幕的一半。
这些假设在我的情况下效果很好,但不可否认它不是一个理想的解决方案。如果你知道如何确定应用程序的哪个区域实际上位于 TabTip 下方,请发表评论或以任何你方便的方式联系我。
动画
现在我们需要实际移动视觉根以使用户满意。所以我们将创建一个 `Storyboard`,将其放入 `VisualRoot.Resources` 字典中,然后启动动画。
private static Storyboard GetMoveRootVisualStoryboard(FrameworkElement VisualRoot)
{
if (VisualRoot.Resources.Contains("MoveRootVisualStoryboard"))
return VisualRoot.Resources["MoveRootVisualStoryboard"] as Storyboard;
else
{
Storyboard MoveRootVisualStoryboard = new Storyboard();
MoveRootVisualStoryboard.Duration = new Duration(TimeSpan.FromSeconds(0.35));
DoubleAnimation TranslateTransformAnimation = new DoubleAnimation();
TranslateTransformAnimation.EasingFunction = new CircleEase { EasingMode = EasingMode.EaseOut };
TranslateTransformAnimation.Duration = new Duration(TimeSpan.FromSeconds(0.35));
MoveRootVisualStoryboard.Children.Add(TranslateTransformAnimation);
VisualRoot.RenderTransform = new TranslateTransform();
Storyboard.SetTarget(TranslateTransformAnimation, VisualRoot);
Storyboard.SetTargetProperty(TranslateTransformAnimation, new PropertyPath("(UIElement.RenderTransform).(TranslateTransform.Y)"));
VisualRoot.Resources.Add("MoveRootVisualStoryboard", MoveRootVisualStoryboard);
return MoveRootVisualStoryboard;
}
}
private void MoveRootVisual(double To)
{
Storyboard MoveRootVisualStoryboard = GetMoveRootVisualStoryboard(AssociatedObject.GetVisualRoot() as FrameworkElement);
(MoveRootVisualStoryboard.Children.First() as DoubleAnimation).To = To;
RootVisualYOffset = To;
MoveRootVisualStoryboard.Begin();
}
其他注意事项
主要逻辑在那里,但一如既往,还需要做更多的事情,才能使用户获得最佳体验。
跟踪焦点
在 `AssociatedObject_LostFocus` 中,我们将调用 `MoveRootVisual(0)` 将布局移回原始位置,但如果焦点丢失只是因为另一个 `AssociatedObject` 获得了它怎么办?在这种情况下,我们将看到 TabTip 快速向下然后向上移动,从而惹恼我们的用户。为了避免这种情况,我们将编写以下代码:
static bool TextBoxFocused;
void AssociatedObject_LostFocus(object sender, RoutedEventArgs e)
{
if (!HardwareKeyboardConnected)
{
TextBoxFocused = false;
new Thread(() =>
{
// checking if another control got focus
Thread.Sleep(100);
if (TextBoxFocused == false)
{
CloseTabTip();
Dispatcher.BeginInvoke(() => MoveRootVisual(0));
}
}).Start();
}
}
`TextBoxFocused` 将在 `AssociatedObject_GotFocus` 中设置为 `true`。在 `AssociatedObject_LostFocus` 中,我们等待 100 毫秒,然后检查是否有任何 `AssociatedObject` 现在获得了焦点。
TabTip 关闭事件
考虑一下会发生什么,如果 `AssociatedObject` 获得焦点,TabTip 打开,视觉根移动,然后用户手动关闭 TabTip。`AssociatedObject_LostFocus` 不会被调用,视觉根将停留在它被移动到的位置。
因此,我们需要知道 TabTip 何时关闭,即使是用户关闭的。为了实现这一点,我们将在 `AssociatedObject_GotFocus` 中调用 `StartTimerForKeyboardClosedEvent` 方法。
static Timer timer;
[DllImport("user32.dll", SetLastError = true)]
static extern UInt32 GetWindowLong(IntPtr hWnd, int nIndex);
public const int GWL_STYLE = -16;
public const uint KeyboardClosedStyle = 2617245696;
private void StartTimerForKeyboardClosedEvent()
{
if (timer == null)
{
timer = new Timer((obj) =>
{
IntPtr KeyboardWnd = FindWindow("IPTip_Main_Window", null);
if (KeyboardWnd.ToInt32() == 0 || GetWindowLong(KeyboardWnd, GWL_STYLE) == KeyboardClosedStyle)
{
Dispatcher.BeginInvoke(() => MoveRootVisual(0));
timer.Dispose();
timer = null;
}
}, null, 700, 50);
}
}
在这里,我们启动一个计时器,并检查我们是否可以找到 TabTip 窗口,如果可以,它的样式是什么。
检查 TabTip 是否已停靠
TabTip 可以被停靠,这对我们的目的来说不是很好,因为当 TabTip 被停靠时,它下方的所有内容都不会渲染。所以当我们移动我们的视觉根时,我们会看到一些空白区域,原本应该显示我们的控件。
这个问题没有真正的好办法,但我们至少可以做的是警告我们的用户。
static bool tabTipDockedWarningShowed = false;
static dynamic wScriptShell = null;
static dynamic WScriptShell { get { return (wScriptShell != null) ? wScriptShell : wScriptShell = GetWScriptShell(); } }
private static dynamic GetWScriptShell()
{
return AutomationFactory.CreateObject("WScript.Shell");
}
private void CheckIfTabTipDockedAndShowWarnng()
{
if (tabTipDockedWarningShowed == false)
new Thread(() =>
{
if (WScriptShell.RegRead(@"HKCU\Software\Microsoft\TabletTip\1.7\EdgeTargetDockedState") == 1)
{
tabTipDockedWarningShowed = true;
Dispatcher.BeginInvoke(() => MessageBox.Show("It is not recomended to use virtual keyboard in docked state!"));
}
}).Start();
}
AssociatedObject_GotFocus
现在我可以最后展示 `AssociatedObject_GotFocus` 方法的代码了。
void AssociatedObject_GotFocus(object sender, RoutedEventArgs e)
{
AssociatedObject.Focus(); //to avoid losing focus
CheckIfHardwareKeyboardConnected();
if (!HardwareKeyboardConnected)
{
TextBoxFocused = true;
CheckIfTabTipDockedAndShowWarnng();
OpenTabTip();
MoveRootVisual(GetNewRootVisualYOffsetForAssociatedObject());
StartTimerForKeyboardClosedEvent();
}
}
在这个方法中,只有一行代码还没有解释,那就是:
AssociatedObject.Focus(); //to avoid losing focus
将焦点设置到刚刚获得焦点的控件上可能看起来违反直觉,但当用户点击(或轻触)一个 `TextBox` 并且他恰好点击到元素边框附近时,焦点会获得然后立即丢失。
为了避免这种不便,我们将用这行代码来加强焦点。
处置 COM 对象
你可能已经注意到,在本文中声明了三个使用 `static` 关键字的 COM 对象。这意味着我们只希望在行为从最后一个 `AssociatedObject` 分离时才处理掉它们。为了做到这一点,我们将增强我们的 `OnAttached` 和 `OnDetaching` 方法。
static int numberOfAttachedElements = 0;
protected override void OnAttached()
{
if (Application.Current.HasElevatedPermissions && AutomationFactory.IsAvailable)
{
numberOfAttachedElements++;
AssociatedObject.LostFocus += AssociatedObject_LostFocus;
AssociatedObject.GotFocus += AssociatedObject_GotFocus;
}
}
protected override void OnDetaching()
{
if (Application.Current.HasElevatedPermissions && AutomationFactory.IsAvailable)
{
base.OnDetaching();
AssociatedObject.LostFocus -= AssociatedObject_LostFocus;
AssociatedObject.GotFocus -= AssociatedObject_GotFocus;
numberOfAttachedElements--;
if (numberOfAttachedElements == 0)
{
if (shellApplication != null)
shellApplication.Dispose();
if (wmiService != null)
wmiService.Dispose();
if (wScriptShell != null)
wScriptShell.Dispose();
}
}
}
正如你所见,这是通过使用 `numberOfAttachedElements` 静态计数器实现的。
附加属性
现在你可以通过简单地将 TabTip 行为附加到任何你想要的控件上来使用它。但真相是,一位女士明智地说:“没人有时间这样做!”。所以我们想做的是创建一个附加属性,它将为我们附加行为。之后,我们可以在样式中设置一次这个属性,并在整个应用程序中获得这个功能。这种技术很普遍,所以我不会在文章中提供代码,但它包含在附加的源代码中。我只会写如何在你样式定义中的这个附加属性的单行代码看起来像:
<Setter Property="Behaviours:ControlAttachedBehavior.ControlTabTipBehaviorEnabled" Value="True"/>