Introduction
This article describes an implementation of a numeric on-screen keyboard using attached properties and UserControl
in WPF. It can be used in WPF applications just like a Popup control. And, you can easily modify this sample into a full-size on-screen keyboard for any language of your choice.
The source code was compiled and tested against Visual Studio 2008 SP1 with .NET 3.0 as target framework, and the demo EXE will work in either Windows XP or Windows Vista with .NET 3.0 installed.
The Design of the Code
Here are the main design goals at a glance:
- Enable the on-screen keyboard simply by setting the attached property
PopupKeyboard.IsEnabled="true"
on any FrameworkElement
.
- Position the control by setting the
Placement
, PlacementTarget
, PlacementRectangle
, HorizontalOffset
, and VerticalOffset
properties, just like you position a Popup
control.
- Switch the keyboard states between
Normal
and Hidden
by double-clicking the FrameworkElement
where the keyboard is attached.
- When the mouse cursor leaves the
FrameworkElement
, the keyboard will remember its last keyboard state so that it can be used later.
Using the Code
To use the control, you will need to include the files PopupKeyboard.xaml and PopupKeyboard.xaml.cs in your project and then add an xmlns
to the window.
xmlns:local="clr-namespace:VirtualKeyboard"
After this is set, the following set of attached properties will be available to any FrameworkElement
in that Window:
PopupKeyboard.Placement
PopupKeyboard.PlacementTarget
PopupKeyboard.PlacementRectangle
PopupKeyboard.HorizontalOffset
PopupKeyboard.VerticalOffset
PopupKeyboard.CustomPopupPlacementCallback
PopupKeyboard.State
PopupKeyboard.Height
PopupKeyboard.Width
PopupKeyboard.IsEnabled
You can check MSDN documentation Popup Placement Behavior on how to set up attached properties Placement
, PlacementTarget
, PlacementRectangle
, HorizontalOffset
, VerticalOffset
, and CustomPopupPlacementCallback
. The attached properties State
, Height
, and Width
set the initial keyboard state (Normal
or Hidden
), keyboard height and width respectively. And, the last attached properties IsEnabled
sets a value that indicates whether the keyboard is available.
Following is a sample on how to set these attached properties:
<TextBox
x:Name="txtEmployeeID"
local:PopupKeyboard.Placement="Bottom"
local:PopupKeyboard.PlacementTarget="{Binding ElementName=txtEmployeeID}"
local:PopupKeyboard.HorizontalOffset="20"
local:PopupKeyboard.Height="220"
local:PopupKeyboard.Width="200"
local:PopupKeyboard.IsEnabled="true"/>
Also, you can check the demo source code for more information on how to use the numeric on-screen keyboard.
How It Works
The numeric on-screen keyboard is composed mainly of two classes: PopupKeyboardUserControl
and PopupKeyboard
. The internal class PopupKeyboardUserControl
derives from UserControl
that includes XAML and all the logic for the on-screen keyboard. Class PopupKeyboard
is a static
class that has a reference to an object of PopupKeyboardUserControl
and defines all the attached properties needed to display and position the on-screen keyboard.
Next, I will briefly describe how these two classes are actually coded:
1. Keyboard Events Simulation
In order to simulate keyboard events, I use the keybd_event
function to synthesize a keystroke, and here is the syntax for the Windows API function:
[DllImport("user32.dll", CharSet = CharSet.Auto)]
private static extern void keybd_event
(byte bVk, byte bScan, uint dwFlags, UIntPtr dwExtraInfo);
For every key Button
defined in file PopupKeyboard.xaml, its Click
event is linked to event handler cmdNumericButton_Click
, which includes the logic to simulate keyboard events, for example, when you click Button
"btn010300", the following lines of code will be executed, and it simulates a user pressing the keyboard button "Number 1".
...
case "btn010300":
keybd_event(VK_1, 0, 0, (UIntPtr)0);
keybd_event(VK_1, 0, KEYEVENTF_KEYUP, (UIntPtr)0);
e.Handled = true;
break;
...
By modifying the XAML file PopupKeyboard.xaml and changing event handler cmdNumericButton_Click
, you can implement any type of on-screen keyboard of your choice, while the rest of the source codes do not need any change.
2. The Keyboard is a Popup Control
Class PopupKeyboardUserControl
defines a property called IsOpen
that controls whether the keyboard is open or not. When setting IsOpen = true
, function HookupParentPopup()
is called, which essentially creates a Popup
control and attaches the keyboard to it. You can check the relevant source code below:
public static readonly DependencyProperty IsOpenProperty =
Popup.IsOpenProperty.AddOwner(
typeof(PopupKeyboardUserControl),
new FrameworkPropertyMetadata(
false,
FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
new PropertyChangedCallback(IsOpenChanged)));
public bool IsOpen
{
get { return (bool)GetValue(IsOpenProperty); }
set { SetValue(IsOpenProperty, value); }
}
private static void IsOpenChanged(DependencyObject element,
DependencyPropertyChangedEventArgs e)
{
PopupKeyboardUserControl ctrl = (PopupKeyboardUserControl)element;
if ((bool)e.NewValue)
{
if (ctrl._parentPopup == null)
{
ctrl.HookupParentPopup();
}
}
}
private void HookupParentPopup()
{
_parentPopup = new Popup();
_parentPopup.AllowsTransparency = true;
_parentPopup.PopupAnimation = PopupAnimation.Scroll;
this.Height = this.NormalHeight;
this.Width = this.NormalWidth;
Popup.CreateRootPopup(_parentPopup, this);
}
3. Defining Attached Properties
Static
class PopupKeyboard
includes all the necessary attached properties to enable and position the numeric on-screen keyboard. As an example, I list the definition for attached property IsEnabled
as follows:
public static readonly DependencyProperty IsEnabledProperty =
DependencyProperty.RegisterAttached("IsEnabled",
typeof(bool),
typeof(PopupKeyboard),
new FrameworkPropertyMetadata(false,
new PropertyChangedCallback(PopupKeyboard.OnIsEnabledChanged)));
[AttachedPropertyBrowsableForType(typeof(FrameworkElement))]
public static bool GetIsEnabled(DependencyObject element)
{
if (element == null)
throw new ArgumentNullException("element");
return (bool)element.GetValue(IsEnabledProperty);
}
public static void SetIsEnabled(DependencyObject element, bool value)
{
if (element == null)
throw new ArgumentNullException("element");
element.SetValue(IsEnabledProperty, value);
}
private static void OnIsEnabledChanged
(DependencyObject element, DependencyPropertyChangedEventArgs e)
{
FrameworkElement frameworkElement = element as FrameworkElement;
if (frameworkElement != null)
{
if (((bool)e.NewValue == true) && ((bool)e.OldValue == false))
{
frameworkElement.AddHandler(FrameworkElement.GotKeyboardFocusEvent,
new KeyboardFocusChangedEventHandler
(frameworkElement_GotKeyboardFocus), true);
frameworkElement.AddHandler(FrameworkElement.LostKeyboardFocusEvent,
new KeyboardFocusChangedEventHandler
(frameworkElement_LostKeyboardFocus), true);
frameworkElement.AddHandler(FrameworkElement.MouseDownEvent,
new MouseButtonEventHandler(frameworkElement_MouseDown), true);
frameworkElement.AddHandler(FrameworkElement.SizeChangedEvent,
new SizeChangedEventHandler(frameworkElement_SizeChanged), true);
}
else if (((bool)e.NewValue == false) && ((bool)e.OldValue == true))
{
frameworkElement.RemoveHandler(FrameworkElement.GotKeyboardFocusEvent,
new KeyboardFocusChangedEventHandler
(frameworkElement_GotKeyboardFocus));
frameworkElement.RemoveHandler(FrameworkElement.LostKeyboardFocusEvent,
new KeyboardFocusChangedEventHandler
(frameworkElement_LostKeyboardFocus));
frameworkElement.RemoveHandler(FrameworkElement.MouseUpEvent,
new MouseButtonEventHandler(frameworkElement_MouseDown));
frameworkElement.RemoveHandler(FrameworkElement.SizeChangedEvent,
new SizeChangedEventHandler(frameworkElement_SizeChanged));
}
}
Window currentWindow = Window.GetWindow(element);
if (currentWindow != null)
{
if (((bool)e.NewValue == true) && ((bool)e.OldValue == false))
{
currentWindow.LocationChanged += currentWindow_LocationChanged;
}
else if (((bool)e.NewValue == false) && ((bool)e.OldValue == true))
{
currentWindow.LocationChanged -= currentWindow_LocationChanged;
}
}
}
When setting attached property PopupKeyboard.IsEnabled="true"
for a control, event handlers for events GotKeyboardFocus
, LostKeyboardFocus
, MouseUp
, and SizeChanged
are added to that control, and event handler for event LocationChanged
is added to the current Window. These event handlers contain the logic to enable, hide, and reposition the numeric on-screen keyboard by passing the settings from the attached properties to the relevant property settings inside the PopupKeyboardUserControl
object. Essentially, they tie together the attached properties with the internal class PopupKeyboardUserControl
.
4. Passing Values in GotKeyboardFocus and LostKeyboardFocus Event Handlers
When event GetKeyboardFocus
is fired, a new instance of PopupKeyboardUserControl
is created, and the settings from attached properties are passed in as you can see below:
private static void frameworkElement_GotKeyboardFocus
(object sender, System.Windows.Input.KeyboardFocusChangedEventArgs e)
{
FrameworkElement frameworkElement = sender as FrameworkElement;
if (frameworkElement != null)
{
if (PopupKeyboard._popupKeyboardUserControl == null)
{
_popupKeyboardUserControl = new PopupKeyboardUserControl();
_popupKeyboardUserControl.Placement =
PopupKeyboard.GetPlacement(frameworkElement);
_popupKeyboardUserControl.PlacementTarget =
PopupKeyboard.GetPlacementTarget(frameworkElement);
_popupKeyboardUserControl.PlacementRectangle =
PopupKeyboard.GetPlacementRectangle(frameworkElement);
_popupKeyboardUserControl.HorizontalOffset =
PopupKeyboard.GetHorizontalOffset(frameworkElement);
_popupKeyboardUserControl.VerticalOffset =
PopupKeyboard.GetVerticalOffset(frameworkElement);
_popupKeyboardUserControl.StaysOpen = true;
_popupKeyboardUserControl.CustomPopupPlacementCallback =
PopupKeyboard.GetCustomPopupPlacementCallback(frameworkElement);
_popupKeyboardUserControl.State = PopupKeyboard.GetState(frameworkElement);
_popupKeyboardUserControl.NormalHeight =
PopupKeyboard.GetHeight(frameworkElement);
_popupKeyboardUserControl.NormalWidth =
PopupKeyboard.GetWidth(frameworkElement);
if (PopupKeyboard.GetState(frameworkElement) == KeyboardState.Normal)
PopupKeyboard._popupKeyboardUserControl.IsOpen = true;
}
}
}
Similarly, when event LostKeyboardFocus
is fired, the latest setting for property State
(either Normal
or Hidden
) is saved, and keyboard is closed by setting IsOpen = false
:
private static void frameworkElement_LostKeyboardFocus
(object sender, System.Windows.Input.KeyboardFocusChangedEventArgs e)
{
FrameworkElement frameworkElement = sender as FrameworkElement;
if (frameworkElement != null)
{
if (PopupKeyboard._popupKeyboardUserControl != null)
{
PopupKeyboard.SetState(frameworkElement, _popupKeyboardUserControl.State);
PopupKeyboard._popupKeyboardUserControl.IsOpen = false;
PopupKeyboard._popupKeyboardUserControl = null;
}
}
}
Feedback
That's it. I hope you'll find this article helpful and instructive, and please vote if you like it.
History
- Dec. 20, 2008 - Initial release