Click here to Skip to main content
15,867,453 members
Articles / Desktop Programming / WPF
Article

Creating On-Screen Keyboard Using Attached Behavior in WPF

Rate me:
Please Sign up or sign in to vote.
4.74/5 (29 votes)
19 Dec 2008CPOL4 min read 107.7K   6K   54   29
An implementation of a numeric on-screen keyboard using attached behaviors in WPF
Image 1

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.

XML
xmlns:local="clr-namespace:VirtualKeyboard"

After this is set, the following set of attached properties will be available to any FrameworkElement in that Window:

C#
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:

XML
<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:

C#
[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".

C#
...
// Number 1
case "btn010300":
    keybd_event(VK_1, 0, 0, (UIntPtr)0);
    keybd_event(VK_1, 0, KEYEVENTF_KEYUP, (UIntPtr)0);
    // event already handled
    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:

C#
/// <summary>
/// IsOpen
/// </summary>
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); }
}

/// <summary>
/// PropertyChangedCallback method for IsOpen Property
/// </summary>
/// <param name=""element""></param>
/// <param name=""e""></param>
private static void IsOpenChanged(DependencyObject element, 
				DependencyPropertyChangedEventArgs e)
{
    PopupKeyboardUserControl ctrl = (PopupKeyboardUserControl)element;

    if ((bool)e.NewValue)
    {
        if (ctrl._parentPopup == null)
        {
            ctrl.HookupParentPopup();
        }
    }
}

/// <summary>
/// Create the Popup and attach the CustomControl to it.
/// </summary>
private void HookupParentPopup()
{
    _parentPopup = new Popup();

    _parentPopup.AllowsTransparency = true;
    _parentPopup.PopupAnimation = PopupAnimation.Scroll;

    // Set Height and Width
    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:

C#
/// <summary>
/// IsEnabled
/// </summary>
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);
}

/// <summary>
/// PropertyChangedCallback method for IsEnabled Attached Property
/// </summary>
/// <param name=""element""></param>
/// <param name=""e""></param>
private static void OnIsEnabledChanged
	(DependencyObject element, DependencyPropertyChangedEventArgs e)
{
    FrameworkElement frameworkElement = element as FrameworkElement;

    // Attach & detach handlers for events GotKeyboardFocus, 
    // LostKeyboardFocus, MouseDown, and SizeChanged
    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);

    // Attach or detach handler for event LocationChanged
    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:

C#
/// <summary>
/// Event handler for GotKeyboardFocus
/// </summary>
/// <param name=""sender""></param>
/// <param name=""e""></param>
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();

            // Set all the necessary properties
            _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:

C#
/// <summary>
/// Event handler for LostKeyboardFocus
/// </summary>
/// <param name=""sender""></param>
/// <param name=""e""></param>
private static void frameworkElement_LostKeyboardFocus
	(object sender, System.Windows.Input.KeyboardFocusChangedEventArgs e)
{
    FrameworkElement frameworkElement = sender as FrameworkElement;

    if (frameworkElement != null)
    {
        if (PopupKeyboard._popupKeyboardUserControl != null)
        {
            // Retrieves the setting for the State property
            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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior)
United States United States
Weidong has been an information system professional since 1990. He has a Master's degree in Computer Science, and is currently a MCSD .NET

Comments and Discussions

 
GeneralMy vote of 2 Pin
Ian Ilo29-May-19 2:47
Ian Ilo29-May-19 2:47 
QuestionControl race condition when you click from dropdown close to button Pin
btechnet10-Jan-18 10:42
btechnet10-Jan-18 10:42 
QuestionVB Version Pin
Garethb_8311-Sep-13 3:58
Garethb_8311-Sep-13 3:58 
AnswerRe: VB Version Pin
Weidong Shen12-Sep-13 1:16
Weidong Shen12-Sep-13 1:16 
GeneralRe: VB Version Pin
Garethb_8312-Sep-13 1:35
Garethb_8312-Sep-13 1:35 
GeneralRe: VB Version Pin
Weidong Shen14-Sep-13 18:59
Weidong Shen14-Sep-13 18:59 
GeneralRe: VB Version Pin
Garethb_8316-Sep-13 2:36
Garethb_8316-Sep-13 2:36 
GeneralRe: VB Version Pin
Garethb_8316-Sep-13 3:17
Garethb_8316-Sep-13 3:17 
QuestionGreat Article!!! Need Some Help Please Pin
Ron Mittelman26-Nov-12 12:54
Ron Mittelman26-Nov-12 12:54 
AnswerRe: Great Article!!! Need Some Help Please Pin
Weidong Shen27-Nov-12 3:30
Weidong Shen27-Nov-12 3:30 
GeneralRe: Great Article!!! Need Some Help Please Pin
Ron Mittelman27-Nov-12 6:38
Ron Mittelman27-Nov-12 6:38 
Thanks for the quick answer. I agree, it's the control itself that should filter entries. I was simply asking how to intercept the point in your code that actually fills in the control's text.

In my case, the IntegerUpDown won't work because some text boxes want to hold integer data, but some want to hold decimal data.

I have figured out how to solve this issue. My NumericUpDown control uses a class behind it, including a property for the TextBox value. Since the TextBox is bound to that property, it updates properly whether you set the property value in code or directly in the text box, including using your keypad control. I use the PreviewKeyDown of the TextBox to allow only numeric, minus, period, backspace, and since you provide left and right, those as well. Then I use the PreviewTextInput to handle decimal point and minus sign entries.

It would be nice if I could dismiss the dialog with an escape key for convenience. Do you have an easy way to do that?

In any case, thanks again for your control. It's a very unusual and user-friendly way of accomplishing the task.
GeneralRe: Great Article!!! Need Some Help Please Pin
Weidong Shen27-Nov-12 11:27
Weidong Shen27-Nov-12 11:27 
GeneralRe: Great Article!!! Need Some Help Please Pin
Ron Mittelman28-Nov-12 6:59
Ron Mittelman28-Nov-12 6:59 
GeneralRe: Great Article!!! Need Some Help Please Pin
Ron Mittelman28-Nov-12 7:13
Ron Mittelman28-Nov-12 7:13 
GeneralRe: Great Article!!! Need Some Help Please Pin
Weidong Shen28-Nov-12 14:17
Weidong Shen28-Nov-12 14:17 
GeneralRe: Great Article!!! Need Some Help Please Pin
Ron Mittelman29-Nov-12 6:35
Ron Mittelman29-Nov-12 6:35 
GeneralRe: Great Article!!! Need Some Help Please Pin
Weidong Shen30-Nov-12 14:01
Weidong Shen30-Nov-12 14:01 
QuestionValidation when the keypad closes. Pin
Preston Phillips9-Sep-11 4:40
Preston Phillips9-Sep-11 4:40 
AnswerRe: Validation when the keypad closes. Pin
Weidong Shen9-Sep-11 5:36
Weidong Shen9-Sep-11 5:36 
GeneralRe: Validation when the keypad closes. Pin
Preston Phillips9-Sep-11 6:05
Preston Phillips9-Sep-11 6:05 
GeneralRe: Validation when the keypad closes. Pin
Weidong Shen9-Sep-11 16:10
Weidong Shen9-Sep-11 16:10 
QuestionSend key is slow Pin
diemtrang30-May-11 0:05
diemtrang30-May-11 0:05 
AnswerRe: Send key is slow Pin
Weidong Shen31-May-11 8:14
Weidong Shen31-May-11 8:14 
QuestionHow about SendKeys API? Pin
Georgi Atanasov20-Dec-08 6:17
Georgi Atanasov20-Dec-08 6:17 
AnswerRe: How about SendKeys API? Pin
Weidong Shen20-Dec-08 6:27
Weidong Shen20-Dec-08 6:27 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.