Click here to Skip to main content
15,881,027 members
Articles / Desktop Programming / WPF

A Touch Screen Keyboard Control in WPF

Rate me:
Please Sign up or sign in to vote.
4.88/5 (46 votes)
15 Jan 2009CPOL6 min read 335.4K   16.8K   149   76
A touch screen keyboard which emulates the basic functionality of a real keyboard.

Image 1

Touch Screen Keyboard in Login Form

Introduction

For a touch screen project, the user needed a touch screen keyboard to enter information in a textbox, password box etc. So, a keyboard layout was to be implemented in WPF. Here, a custom keyboard layout is implemented using StackPanels and Buttons.

Touch Screen Keyboard is a WPF window with no style. Considerable work has to be done to make sync the keyboard window and the application window for resizing, out of screen, minimize, maximize, and window activate and deactivate issues. Ctrl, Alt, Function, and Arrow key functionalities are not given, but basic keys including Enter, BackSpace, Shift, TAB, CapsLock functionalities are implemented. Some features of this Touch Screen Keyboard are:

  • Window state sync: it synchronizes itself with window maximize, minimize.
  • It never goes out of screen in x-axis.
  • It always keeps itself in sync with the application window size.
  • It keeps itself in sync with window move.
  • No hooking: it does not use any sort of hooking (no interoperability).

Keyboard layout

Keyboard layout is created using StackPanels and Button. If you look at the keyboard layout, you will see that there are five rows. For these five rows, five StackPanels with horizontal orientation are taken. Each of these StackPanels contain the keys (Buttons) for its corresponding row. A parent StackPanel with vertical orientation contains these five StackPanels.

XML
<StackPanel Orientation="Vertical">
  <StackPanel Orientation="Horizontal" >
    //ALL the keys(Buttons) of the first row of the keyboard layout.
  </StackPanel>
  <StackPanel Orientation="Horizontal" >
    //ALL the keys(Buttons) of the Second row of the keyboard layout.
  </StackPanel>
  <StackPanel Orientation="Horizontal" >
    //ALL the keys(Buttons) of the third row of the keyboard layout.
  </StackPanel>
  <StackPanel Orientation="Horizontal" >
    //ALL the keys(Buttons) of the fourth row of the keyboard layout.
  </StackPanel>
  <StackPanel Orientation="Horizontal" >
    //ALL the keys(Buttons) of the fifth row of the keyboard layout.
  </StackPanel>
</StackPanel>

Here, all the keys are Buttons with a certain look and feel. I would like to give special thanks to Mark Heath for his article "Creating a Custom WPF Button Template in XAML". The button style is taken from there, and full credits for the buttons go to him. All the key strokes are handled using WPF commands.

Remora Pattern by Ben Constable

According to Ben Constable, Remora Pattern allows you to attach a chunk of logic to any existing element that you have. This pattern can be implemented using an Attached Dependency Property in WPF, which is shown in Ben Constable's blog. You can take a look at it here.

Here, an Attached Dependency Property is attached with an object. When the object is initiated, it goes to set the value of the Attached Dependency Property, which results in calling an Attached Dependency Property Change event. In the event handler, you can add your intended functionality, which is the additional functionality of the object.

XML
<PasswordBox k:TouchScreenKeyboard.TouchScreenKeyboard="true"  x:Name="txtPassword"  />

The code:

C#
public static readonly DependencyProperty TouchScreenKeyboardProperty =
  DependencyProperty.RegisterAttached("TouchScreenKeyboard", typeof(bool), 
  typeof(TouchScreenKeyboard), new UIPropertyMetadata(default(bool), 
  TouchScreenKeyboardPropertyChanged));

Here, a TouchScreenKeyboard attached property is exposed form the Touch Screen custom control. To get the touch screen keyboard functionality, you have to set the TouchScreenKeyboard attached property to a textbox or a password box. When this textbox or password box is initiated, it sets the value of the TouchScreenKeyboard attached property which in turn calls the TouchScreenKeyboardPropertyChanged event.

C#
static void TouchScreenKeyboardPropertyChanged(DependencyObject sender, 
                               DependencyPropertyChangedEventArgs e)
{
    FrameworkElement host = sender as FrameworkElement;
    if (host != null)
    {
        host.GotFocus += new RoutedEventHandler(OnGotFocus);
        host.LostFocus += new RoutedEventHandler(OnLostFocus);
    }
}

In TouchScreenKeyboardPropertyChanged, we add functionality in the focus and lost focus events. Here, host is a textbox or password box in which you are setting the TouchScreenKeyboard attached property. In the ONGOTFocus event handler, you will show the touch screen keyboard to enter keys, and in the UNFocus event handler, the keyboard will disappaer.

The code

How does it move a parent window and a keyboard window together?

When a WPF window moves , it fires the LocationChanged event. In the code to find the parent window of the virtual keyboard window, the following code is written in the focus event of host:

C#
FrameworkElement ct = host;
while (true)
{
    if (ct is Window)
    {
        ((Window)ct).LocationChanged += 
           new EventHandler(TouchScreenKeyboard_LocationChanged);
        break;
    }
    ct = (FrameworkElement)ct.Parent;
}

After getting the parent window, the LocationChanged event of the parent window is subscribed to. Now, whenever the parent window moves, it will invoke the LocationChanged event handler method and the location of the TouchScreenWindow will be updated accordingly.

How does it synchronize the keyboard window with the parent window maximize?

When the parent window is maximized, the LayoutUpdate event is fired by each of the child controls. So, the LayoutUpdate event of the textbox or the password box which is host is subscribed in the following code in the focus event:

C#
host.LayoutUpdated += new EventHandler(tb_LayoutUpdated);

Now, whenever the parent window is maximized, it will invoke the LayoutUpdated event handler method and the location of the TouchScreenWindow will be updated accordingly.

How does it synchronize keyboard window with the parent window resize?

When the parent window is resized, the LayoutUpdate event is fired by each of the child controls. So, the LayoutUpdate event of the textbox or password box which is host is subscribed in the following code in the focus event:

C#
host.LayoutUpdated += new EventHandler(tb_LayoutUpdated);

Now, whenever the parent window is resized, it will invoke the LayoutUpdated event handler method and the location of the TouchScreenWindow will be updated accordingly.

How does it make the host control's border red and background yellow?

The host control's border is made red when it gets focus. So, in the focus event of host, the following code is written:

C#
_PreviousTextBoxBackgroundBrush = host.Background;
_PreviousTextBoxBorderBrush = host.BorderBrush;
_PreviousTextBoxBorderThickness = host.BorderThickness;

host.Background = Brushes.Yellow;
host.BorderBrush = Brushes.Red;
host.BorderThickness = new Thickness(4);

The border color, background, and thickness is changed in this event. Before making changes, the border’s property values are saved so that at a later time it can restore its original look. In the unfocused event, it restores its original look.

How does it restrict the touch screen keyboard WPF window to go outside of the screen in x-axis?

SystemParameters.VirtualScreenWidth returns the width of the virtual screen in pixels. Now we know the lower boundary is 0 and the upper boundary is SystemParameters.VirtualScreenWidth. So, all it has to do is to go through some logic. The logic is written in the following code:

C#
if (WidthTouchKeyboard + Actualpoint.X > SystemParameters.VirtualScreenWidth)
{
    double difference = WidthTouchKeyboard + Actualpoint.X - 
                        SystemParameters.VirtualScreenWidth;
    _InstanceObject.Left = Actualpoint.X - difference;
}
else if (!(Actualpoint.X > 1))
{
    _InstanceObject.Left = 1;
}
else
    _InstanceObject.Left = Actualpoint.X;

What the code does here is it checks whether the leftmost x-axis vale and the rightmost x-axis value of the touch screen keyboard are out of the screen or not. If it is out of the screen, then it resets it to appropriate value. Otherwise, it does nothing.

How do I position the child window to the exact position of the host control?

When the host control is focused, it fires the focus event. In the focus event, we get the location of the host control. Then, we set the touch screen keyboard window's location accordingly.

How the touch screen keyboard window is always on top of the host window and does not make problems with other windows?

Setting the touch screen keyboard window to the topmost will not work. Because in that case, the touch screen keyboard will always remain on top of all the other windows in the machine. To solve the problem, we take the help of the Activatewindow and Deactivatewindow events of the host window. When he host window is activated, it fires the Activated event, and when the host window is deactivated, it fires the Deactivated event.

C#
FrameworkElement ct = host;
while (true)
{
    if (ct is Window)
    {
        ((Window)ct).Activated += new EventHandler(TouchScreenKeyboard_Activated);
        ((Window)ct).Deactivated += new EventHandler(TouchScreenKeyboard_Deactivated);
        break;
    }
    ct = (FrameworkElement)ct.Parent;
}

It subscribes to the Activated and Deactivated events of the parent window. In the Activated event, the touch screen keyboard is made topmost, and in the Deactivated event, it resets it. Here is the code for this functionality:

C#
static void TouchScreenKeyboard_Deactivated(object sender, EventArgs e)
{
    if (_InstanceObject != null)
    {
        _InstanceObject.Topmost = false;
    }
}

static void TouchScreenKeyboard_Activated(object sender, EventArgs e)
{
    if (_InstanceObject != null)
    {
        _InstanceObject.Topmost = true;
    }
}

Sample code

Here, a project has been attached which shows the touch screen keyboard control in action.

Conclusion

Thanks for reading this write up. I hope that this write up will be helpful for some people. If you guys have any questions, I would love to answer.

References

History

  • Initial release – 16/01/09.

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) CP
Australia Australia
I am an Independent Contractor in Brisbane, Australia. For me, programming is a passion first, a hobby second, and a career third.

My Blog: http://weblogs.asp.net/razan/






Comments and Discussions

 
GeneralMy vote of 5 Pin
Member 1376774419-Oct-19 10:35
Member 1376774419-Oct-19 10:35 
GeneralChanges to make the keyboard more flexible and dynamic Pin
Stacy Dudovitz17-Dec-18 6:40
professionalStacy Dudovitz17-Dec-18 6:40 
First, let me just say this is an amazing piece of work! Its obvious you put in a great deal of thought and effort into this. Kudos!

I ran into several issues while trying to integrate this into a project I am working on. Most notable of the problems were the following:

1. Difficult to change the sizing dynamically
2. Consumption of resources (memory)
3. Handling of global events (WPF routing)
4. Displaying the text caret

To start, the XAML implementation uses many panel resources (StackPanel) and hard codes the sizes of the Buttons. This makes it difficult to resize the keyboard, and forces the size of the fonts inside of the buttons:

<StackPanel Orientation="Vertical">

     <StackPanel Orientation="Horizontal" >

         <Button Style="{StaticResource InformButton}"
                 Command="local:TouchScreenKeyboard.CmdTlide"
                 VerticalContentAlignment="Top"
                 HorizontalContentAlignment="Left"
                 Width="50"
                 Height="50">
             <StackPanel Orientation="Vertical" HorizontalAlignment="Left" VerticalAlignment="Top" >
                 <TextBlock    HorizontalAlignment="Left" VerticalAlignment="Top"   >~</TextBlock>
                 <TextBlock HorizontalAlignment="Left" VerticalAlignment="Bottom"   >`</TextBlock>
             </StackPanel>
         </Button>


I changed the XAML to instead use Grids, and to use "Auto", "*" for size, "Stretch" and ViewBox for the key content:
<Border BorderBrush="Black" BorderThickness="5" CornerRadius="10">
     <StackPanel Orientation="Horizontal">
         <Border BorderBrush="White" BorderThickness="2" CornerRadius="10">

             <Grid
                 HorizontalAlignment="Stretch"
                 VerticalAlignment="Stretch"
                 Width="{TemplateBinding KbdWidth}"
                 Height="{TemplateBinding KbdHeight}"
                 >
                 <Grid.RowDefinitions>
                     <RowDefinition Height="*"/>
                     <RowDefinition Height="*"/>
                     <RowDefinition Height="*"/>
                     <RowDefinition Height="*"/>
                     <RowDefinition Height="*"/>
                 </Grid.RowDefinitions>

                 <!-- `1234567890-= BkSpc -->
                 <Grid x:Name="Row0"
                       Grid.Row="0"
                       >
                     <Grid.ColumnDefinitions>
                         <ColumnDefinition Width="*"/>
                         <ColumnDefinition Width="*"/>
                         <ColumnDefinition Width="*"/>
                         <ColumnDefinition Width="*"/>
                         <ColumnDefinition Width="*"/>
                         <ColumnDefinition Width="*"/>
                         <ColumnDefinition Width="*"/>
                         <ColumnDefinition Width="*"/>
                         <ColumnDefinition Width="*"/>
                         <ColumnDefinition Width="*"/>
                         <ColumnDefinition Width="*"/>
                         <ColumnDefinition Width="*"/>
                         <ColumnDefinition Width="*"/>
                         <ColumnDefinition Width="2*"/>
                     </Grid.ColumnDefinitions>

                     <Button Grid.Column="0"
                         Style="{StaticResource InformButton}" Command="absngGUI:TouchScreenKeyboard.CmdTlide"
                             VerticalContentAlignment="Top"
                             HorizontalContentAlignment="Left"
                             HorizontalAlignment="Stretch"
                             VerticalAlignment="Stretch"
                             >
                         <Viewbox>
                             <StackPanel Orientation="Vertical" HorizontalAlignment="Left" VerticalAlignment="Top" >
                                 <TextBlock HorizontalAlignment="Left" VerticalAlignment="Top" Text="~" />
                                 <TextBlock HorizontalAlignment="Left" VerticalAlignment="Bottom" Text="`" />
                             </StackPanel>
                         </Viewbox>
                     </Button>


This made it much easier to code up the entire keyboard such that I could then resize it to any size I like. You will notice the binding to KbdWidth and KbdHeight... I added those properties here:

#region KbdWidth property

/// <summary>
/// This is the Control property that we expose to the user.
/// </summary>
[Category("TouchScreenKeyboard")]
public double KbdWidth
{
    get { return (double)GetValue(KbdWidthProperty); }
    set { SetValue(KbdWidthProperty, value); }
}

/// <summary>
///
/// </summary>
public static readonly DependencyProperty KbdWidthProperty =
    DependencyProperty.Register("KbdWidth", typeof(double), typeof(TouchScreenKeyboard),
        new PropertyMetadata(DefaultKbdWidth));

/// <summary>
///
/// </summary>
private const double DefaultKbdWidth = 480;

#endregion

#region KbdHeight property

/// <summary>
/// This is the Control property that we expose to the user.
/// </summary>
[Category("TouchScreenKeyboard")]
public double KbdHeight
{
    get { return (double)GetValue(KbdHeightProperty); }
    set { SetValue(KbdHeightProperty, value); }
}

/// <summary>
///
/// </summary>
public static readonly DependencyProperty KbdHeightProperty =
    DependencyProperty.Register("KbdHeight", typeof(double), typeof(TouchScreenKeyboard),
        new PropertyMetadata(DefaultKbdHeight));

/// <summary>
///
/// </summary>
private const double DefaultKbdHeight = 180;

#endregion


They are set here:

/// <summary>
/// Keyboard width
/// </summary>
private static double s_widthTouchKeyboard = 700;
public static double WidthTouchKeyboard
{
    get { return s_widthTouchKeyboard; }
    set { s_widthTouchKeyboard = value; }
}

/// <summary>
/// Keyboard height
/// </summary>
private static double s_heightTouchKeyboard = 250;
public static double HeightTouchKeyboard
{
    get { return s_heightTouchKeyboard; }
    set { s_heightTouchKeyboard = value; }
}

/// <inheritdoc />
/// <summary>
/// Ctor
/// </summary>
public TouchScreenKeyboard()
{
    Width = WidthTouchKeyboard;
    KbdWidth = WidthTouchKeyboard - KeyboardGutter;
    Height = HeightTouchKeyboard;
    KbdHeight = HeightTouchKeyboard - KeyboardGutter;

    GotFocus += (sender, args) =>
    {
        DisplayCaret();
    };
}


One additional benefit of using Grid is that it greatly reduces the use of StackPanel, which not only reduces the number of objects to manage, it also reduces the load on the layout handler.

Speaking of the layout handler, with regards to your use of the LayoutUpdated event, Microsoft explicitly warns against using and/or relying on this global event, and instead pushes developers to use the SizeChanged event instead:

"Use SizeChanged events to respond to layout changes

The FrameworkElement class exposes two similar events for responding to layout changes: LayoutUpdated and SizeChanged. You might be using one of these events to receive notification when an element is resized during layout. The semantics of the two events are different, and there are important performance considerations in choosing between them.

For good performance, SizeChanged is almost always the right choice. SizeChanged has intuitive semantics. It is raised during layout when the size of the FrameworkElement has been updated.

LayoutUpdated is also raised during layout, but it has global semantics—it is raised on every element whenever any element is updated. It is typical to only do local processing in the event handler, in which case the code is run more often than needed. Use LayoutUpdated only if you need to know when an element is repositioned without changing size (which is uncommon).
"

// hook the z-order layout event
host.SizeChanged += TouchScreenKeyboard_SizeChanged;


It's also a good idea to display the text caret in the target text control when the keyboard is popped up. To do that, I refactored your code to be one of three types of text modifiers:

/// <summary>
/// Adds keyboard input without any sticky key state check.
/// Do perform a max field check before adding input.
/// </summary>
/// <param name="input"></param>
private static void AddKeyboardInputWithoutChecks(char input)
{
    // check if we reach the maximum field size
    if (TouchScreenTextLen >= TouchScreenTextMaxLen)
    {
        DisplayCaret();
        return;
    }

    // add character
    TouchScreenText += char.ToLower(input).ToString();
}

/// <summary>
/// Adds keyboard input for the target key after checking the state of the SHIFT key.
/// Do perform a max field check before adding input.
/// </summary>
/// <param name="inputUnshifted"></param>
/// <param name="inputShifted"></param>
// ReSharper disable once IdentifierTypo
private static void AddKeyboardInputWithShiftCheck(char inputUnshifted, char inputShifted)
{
    // check if we reach the maximum field size
    if (TouchScreenTextLen >= TouchScreenTextMaxLen)
    {
        DisplayCaret();
        return;
    }

    // add character, taking sticky keys into account
    if (!ShiftFlag)
    {
        TouchScreenText += inputUnshifted;
    }
    else
    {
        TouchScreenText += inputShifted;
        ShiftFlag = false;
    }
}

/// <summary>
/// Adds keyboard input for the target key after checking the state of the SHIFT key
/// and the CAPS LOCK key.
/// Do perform a max field check before adding input.
/// </summary>
/// <param name="input"></param>
private static void AddKeyboardInputWithShiftCapsLockCheck(char input)
{
    // check if we reach the maximum field size
    if (TouchScreenTextLen >= TouchScreenTextMaxLen)
    {
        DisplayCaret();
        return;
    }

    // add character, taking sticky keys into account
    if (CapsLockFlag)
    {
        if (ShiftFlag)
        {
            TouchScreenText += char.ToLower(input).ToString();
            ShiftFlag = false;
        }
        else
        {
            TouchScreenText += char.ToUpper(input).ToString();
        }
    }
    else
    {
        if (!ShiftFlag)
        {
            TouchScreenText += char.ToLower(input).ToString();
        }
        else
        {
            TouchScreenText += char.ToUpper(input).ToString();
            ShiftFlag = false;
        }
    }
}


...snip...

            else if (e.Command == CmdO)
            {
                AddKeyboardInputWithShiftCapsLockCheck('O');
            }
            else if (e.Command == CmdP)
            {
                AddKeyboardInputWithShiftCapsLockCheck('P');
            }
            else if (e.Command == CmdOpenCrulyBrace)
            {
	            AddKeyboardInputWithShiftCheck('[', '{');
            }

...snip...


Additional:

One more item that required a fix... the code to calculate the parent window can fail, and this has to do with VisualTree/LogicalTree issues. The original code was failing:

FrameworkElement ct = host;
 while (true)
 {
     if (ct is Window)
     {
         ((Window)ct).LocationChanged += new EventHandler(TouchScreenKeyboard_LocationChanged);
         ((Window)ct).Activated += new EventHandler(TouchScreenKeyboard_Activated);
         ((Window)ct).Deactivated += new EventHandler(TouchScreenKeyboard_Deactivated);
         break;
     }
     ct = (FrameworkElement)ct.Parent;
 }


I changed it to be:

			// hook the window location change and activation notification events
	        var window = VisualTreeHelpers.FindAncestor<Window>(host);
	        if (window != null)
	        {
		        window.LocationChanged += TouchScreenKeyboard_LocationChanged;
		        window.Activated += TouchScreenKeyboard_Activated;
		        window.Deactivated += TouchScreenKeyboard_Deactivated;
#if ALLOW_ESC
				window.PreviewKeyDown += TouchScreenKeyboard_PreviewKeyDown;
#endif
			}


Where VisualTreeHelpers.FindAncestor is defined as :

/// <summary>
/// Returns the first ancestor of specified type
/// </summary>
public static T FindAncestor<T>(DependencyObject current) where T : DependencyObject
{
    current = VisualTreeHelper.GetParent(current);

    while (current != null)
    {
        if (current is T)
        {
            return (T)current;
        }
        current = VisualTreeHelper.GetParent(current);
    };
    return null;
}


Message me if you want the whole source. Smile | :)
GeneralRe: Changes to make the keyboard more flexible and dynamic Pin
Member 45505871-Jan-19 0:30
Member 45505871-Jan-19 0:30 
GeneralRe: Changes to make the keyboard more flexible and dynamic Pin
Member 1447463111-Jun-19 9:00
Member 1447463111-Jun-19 9:00 
QuestionIt does not work with MaskedTextBox Why ? Pin
uzimlm29-Jul-17 9:13
uzimlm29-Jul-17 9:13 
QuestionTo call the touchscreen in C# code instead of xaml Pin
Member 1235535414-Feb-17 1:18
Member 1235535414-Feb-17 1:18 
QuestionNuget Package WPF Touch Keyboard control Pin
Viacheslav Avsenev23-Mar-16 2:10
Viacheslav Avsenev23-Mar-16 2:10 
QuestionChange Theme Pin
kyo4716-Mar-15 3:12
kyo4716-Mar-15 3:12 
QuestionHaving two separate keyboards Pin
hajiloh6-Dec-13 23:23
hajiloh6-Dec-13 23:23 
QuestionKeybord see in all view Pin
GRANTOUTOU29-Sep-13 23:22
GRANTOUTOU29-Sep-13 23:22 
QuestionPAGE Pin
kajanthan kathir14-May-13 22:25
kajanthan kathir14-May-13 22:25 
Question[My vote of 2] Link to a better open source wpf on screen keyboard... Pin
Member 36428303-Jan-13 8:45
Member 36428303-Jan-13 8:45 
AnswerRe: [My vote of 2] Link to a better open source wpf on screen keyboard... Pin
Marc Clifton23-Jan-14 8:31
mvaMarc Clifton23-Jan-14 8:31 
GeneralMy vote of 4 Pin
kaminianand26-Dec-12 0:35
kaminianand26-Dec-12 0:35 
QuestionNeed your help for keyboard customization Pin
Member 264608628-Aug-12 4:42
Member 264608628-Aug-12 4:42 
GeneralAwesome Job!!! Pin
vmadhukar12-Jun-12 22:21
vmadhukar12-Jun-12 22:21 
GeneralRe: Awesome Job!!! Pin
Razan Paul (Raju)13-Jun-12 19:24
Razan Paul (Raju)13-Jun-12 19:24 
QuestionHow to put your Touch Screen Keyboard Control in a static position in the from Pin
lpbinh22-Feb-12 16:52
lpbinh22-Feb-12 16:52 
AnswerRe: How to put your Touch Screen Keyboard Control in a static position in the from Pin
mariindus6-Mar-12 20:25
mariindus6-Mar-12 20:25 
QuestionProblam With Height in TSK Pin
jilanisk0930-Nov-11 2:53
jilanisk0930-Nov-11 2:53 
AnswerRe: Problam With Height in TSK Pin
rhochreiter12-Mar-13 1:10
rhochreiter12-Mar-13 1:10 
QuestionUsing keyboard on usercontrol Pin
sanjay Choudhary17-Oct-11 0:04
sanjay Choudhary17-Oct-11 0:04 
GeneralKey Filter Pin
Paul Palumbo23-May-11 15:25
Paul Palumbo23-May-11 15:25 
Questionmemory leak Pin
somilong26-Apr-11 23:38
somilong26-Apr-11 23:38 
Generalthanks for source code Pin
avitambe25-Feb-11 23:56
avitambe25-Feb-11 23:56 

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.