├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE.md ├── OnScreenKeyboard.sln ├── OnScreenKeyboard.suo ├── OnScreenKeyboard ├── App.xaml ├── App.xaml.cs ├── MainWindow.xaml ├── MainWindow.xaml.cs ├── OnScreenKeyboard.csproj └── Properties │ ├── AssemblyInfo.cs │ ├── Resources.Designer.cs │ ├── Resources.resx │ ├── Settings.Designer.cs │ └── Settings.settings ├── README.md ├── TermControls ├── Commands │ └── DelegateCommand.cs ├── Helpers │ └── GridAutoLayout.cs ├── Images │ ├── Eng-Rus.png │ ├── button-70x70.png │ ├── delete.png │ ├── enter.png │ ├── g-delete.png │ ├── g-enter.png │ ├── g-shift.png │ ├── gEng-Rus.png │ ├── gbutton-70x70.png │ ├── gprobel.png │ ├── ok.png │ ├── okG.png │ ├── probel.png │ └── shift.png ├── KeyImageProperty │ ├── KeyNotPressed.cs │ └── KeyPressed.cs ├── Models │ ├── ButtonModel.cs │ ├── KeyboardModel.cs │ └── KeyboardModelRuEng.cs ├── OnScreenKeyboard.xaml ├── OnScreenKeyboard.xaml.cs ├── Properties │ ├── AssemblyInfo.cs │ ├── Resources.Designer.cs │ ├── Resources.resx │ ├── Settings.Designer.cs │ └── Settings.settings ├── TermControls.csproj └── ViewModels │ └── KeyboardViewModel.cs └── TermControlsTests ├── KeyboardModelTests.cs ├── KeyboardViewModelTests.cs ├── Properties └── AssemblyInfo.cs ├── StubKeyboardModel.cs ├── TermControlsTests.csproj └── packages.config /.gitignore: -------------------------------------------------------------------------------- 1 | ################################################################################ 2 | # This .gitignore file was automatically created by Microsoft(R) Visual Studio. 3 | ################################################################################ 4 | 5 | /OnScreenKeyboard/bin/Debug 6 | /OnScreenKeyboard/obj/x86/Debug 7 | /TermControls/bin/Debug 8 | /TermControlsTests/obj/Debug 9 | /TermControlsTests/bin/Debug 10 | /TermControls/obj/Debug 11 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at avsenev@hotmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | I'm open for pull request. 2 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Viacheslav Avsenev 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /OnScreenKeyboard.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 2012 4 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OnScreenKeyboard", "OnScreenKeyboard\OnScreenKeyboard.csproj", "{4A57F6F1-BCBC-446D-98B9-F98CC6455C2B}" 5 | EndProject 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TermControls", "TermControls\TermControls.csproj", "{4D17507C-6002-49C6-976C-649588A2677F}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TermControlsTests", "TermControlsTests\TermControlsTests.csproj", "{C92ECA3F-1083-4509-B4B7-ED9B3C5B5F3C}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Debug|Mixed Platforms = Debug|Mixed Platforms 14 | Debug|x86 = Debug|x86 15 | Release|Any CPU = Release|Any CPU 16 | Release|Mixed Platforms = Release|Mixed Platforms 17 | Release|x86 = Release|x86 18 | EndGlobalSection 19 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 20 | {4A57F6F1-BCBC-446D-98B9-F98CC6455C2B}.Debug|Any CPU.ActiveCfg = Debug|x86 21 | {4A57F6F1-BCBC-446D-98B9-F98CC6455C2B}.Debug|Mixed Platforms.ActiveCfg = Debug|x86 22 | {4A57F6F1-BCBC-446D-98B9-F98CC6455C2B}.Debug|Mixed Platforms.Build.0 = Debug|x86 23 | {4A57F6F1-BCBC-446D-98B9-F98CC6455C2B}.Debug|x86.ActiveCfg = Debug|x86 24 | {4A57F6F1-BCBC-446D-98B9-F98CC6455C2B}.Debug|x86.Build.0 = Debug|x86 25 | {4A57F6F1-BCBC-446D-98B9-F98CC6455C2B}.Release|Any CPU.ActiveCfg = Release|x86 26 | {4A57F6F1-BCBC-446D-98B9-F98CC6455C2B}.Release|Mixed Platforms.ActiveCfg = Release|x86 27 | {4A57F6F1-BCBC-446D-98B9-F98CC6455C2B}.Release|Mixed Platforms.Build.0 = Release|x86 28 | {4A57F6F1-BCBC-446D-98B9-F98CC6455C2B}.Release|x86.ActiveCfg = Release|x86 29 | {4A57F6F1-BCBC-446D-98B9-F98CC6455C2B}.Release|x86.Build.0 = Release|x86 30 | {4D17507C-6002-49C6-976C-649588A2677F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 31 | {4D17507C-6002-49C6-976C-649588A2677F}.Debug|Any CPU.Build.0 = Debug|Any CPU 32 | {4D17507C-6002-49C6-976C-649588A2677F}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU 33 | {4D17507C-6002-49C6-976C-649588A2677F}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU 34 | {4D17507C-6002-49C6-976C-649588A2677F}.Debug|x86.ActiveCfg = Debug|Any CPU 35 | {4D17507C-6002-49C6-976C-649588A2677F}.Release|Any CPU.ActiveCfg = Release|Any CPU 36 | {4D17507C-6002-49C6-976C-649588A2677F}.Release|Any CPU.Build.0 = Release|Any CPU 37 | {4D17507C-6002-49C6-976C-649588A2677F}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU 38 | {4D17507C-6002-49C6-976C-649588A2677F}.Release|Mixed Platforms.Build.0 = Release|Any CPU 39 | {4D17507C-6002-49C6-976C-649588A2677F}.Release|x86.ActiveCfg = Release|Any CPU 40 | {C92ECA3F-1083-4509-B4B7-ED9B3C5B5F3C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 41 | {C92ECA3F-1083-4509-B4B7-ED9B3C5B5F3C}.Debug|Any CPU.Build.0 = Debug|Any CPU 42 | {C92ECA3F-1083-4509-B4B7-ED9B3C5B5F3C}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU 43 | {C92ECA3F-1083-4509-B4B7-ED9B3C5B5F3C}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU 44 | {C92ECA3F-1083-4509-B4B7-ED9B3C5B5F3C}.Debug|x86.ActiveCfg = Debug|Any CPU 45 | {C92ECA3F-1083-4509-B4B7-ED9B3C5B5F3C}.Release|Any CPU.ActiveCfg = Release|Any CPU 46 | {C92ECA3F-1083-4509-B4B7-ED9B3C5B5F3C}.Release|Any CPU.Build.0 = Release|Any CPU 47 | {C92ECA3F-1083-4509-B4B7-ED9B3C5B5F3C}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU 48 | {C92ECA3F-1083-4509-B4B7-ED9B3C5B5F3C}.Release|Mixed Platforms.Build.0 = Release|Any CPU 49 | {C92ECA3F-1083-4509-B4B7-ED9B3C5B5F3C}.Release|x86.ActiveCfg = Release|Any CPU 50 | EndGlobalSection 51 | GlobalSection(SolutionProperties) = preSolution 52 | HideSolutionNode = FALSE 53 | EndGlobalSection 54 | EndGlobal 55 | -------------------------------------------------------------------------------- /OnScreenKeyboard.suo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snmslavk/WPF-Keyboard-Control/7493603e0b4a34d084f7f6cf6ca92ad56b68fc58/OnScreenKeyboard.suo -------------------------------------------------------------------------------- /OnScreenKeyboard/App.xaml: -------------------------------------------------------------------------------- 1 |  5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /OnScreenKeyboard/App.xaml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Configuration; 4 | using System.Data; 5 | using System.Linq; 6 | using System.Windows; 7 | 8 | namespace OnScreenKeyboard 9 | { 10 | /// 11 | /// Interaction logic for App.xaml 12 | /// 13 | public partial class App : Application 14 | { 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /OnScreenKeyboard/MainWindow.xaml: -------------------------------------------------------------------------------- 1 |  6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /OnScreenKeyboard/MainWindow.xaml.cs: -------------------------------------------------------------------------------- 1 | using System.Windows; 2 | using TermControls.Commands; 3 | using System.Windows.Input; 4 | 5 | namespace OnScreenKeyboard 6 | { 7 | /// 8 | /// Interaction logic for MainWindow.xaml 9 | /// 10 | public partial class MainWindow : Window 11 | { 12 | public MainWindow() 13 | { 14 | InitializeComponent(); 15 | 16 | } 17 | 18 | public ICommand ButtonClickCommand 19 | { 20 | get { return new DelegateCommand(ButtonClick); } 21 | } 22 | 23 | 24 | private void ButtonClick(object param) 25 | { 26 | System.Windows.MessageBox.Show("EnterClick!"); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /OnScreenKeyboard/OnScreenKeyboard.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Debug 5 | x86 6 | 8.0.30703 7 | 2.0 8 | {4A57F6F1-BCBC-446D-98B9-F98CC6455C2B} 9 | WinExe 10 | Properties 11 | OnScreenKeyboard 12 | OnScreenKeyboard 13 | v4.0 14 | Client 15 | 512 16 | {60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} 17 | 4 18 | 19 | 20 | x86 21 | true 22 | full 23 | false 24 | bin\Debug\ 25 | DEBUG;TRACE 26 | prompt 27 | 4 28 | 29 | 30 | x86 31 | pdbonly 32 | true 33 | bin\Release\ 34 | TRACE 35 | prompt 36 | 4 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 4.0 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | MSBuild:Compile 56 | Designer 57 | 58 | 59 | MSBuild:Compile 60 | Designer 61 | 62 | 63 | App.xaml 64 | Code 65 | 66 | 67 | MainWindow.xaml 68 | Code 69 | 70 | 71 | 72 | 73 | Code 74 | 75 | 76 | True 77 | True 78 | Resources.resx 79 | 80 | 81 | True 82 | Settings.settings 83 | True 84 | 85 | 86 | ResXFileCodeGenerator 87 | Resources.Designer.cs 88 | 89 | 90 | SettingsSingleFileGenerator 91 | Settings.Designer.cs 92 | 93 | 94 | 95 | 96 | 97 | {4D17507C-6002-49C6-976C-649588A2677F} 98 | TermControls 99 | 100 | 101 | 102 | 109 | -------------------------------------------------------------------------------- /OnScreenKeyboard/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Resources; 3 | using System.Runtime.CompilerServices; 4 | using System.Runtime.InteropServices; 5 | using System.Windows; 6 | 7 | // General Information about an assembly is controlled through the following 8 | // set of attributes. Change these attribute values to modify the information 9 | // associated with an assembly. 10 | [assembly: AssemblyTitle("OnScreenKeyboard")] 11 | [assembly: AssemblyDescription("")] 12 | [assembly: AssemblyConfiguration("")] 13 | [assembly: AssemblyCompany("")] 14 | [assembly: AssemblyProduct("OnScreenKeyboard")] 15 | [assembly: AssemblyCopyright("Copyright © 2013")] 16 | [assembly: AssemblyTrademark("")] 17 | [assembly: AssemblyCulture("")] 18 | 19 | // Setting ComVisible to false makes the types in this assembly not visible 20 | // to COM components. If you need to access a type in this assembly from 21 | // COM, set the ComVisible attribute to true on that type. 22 | [assembly: ComVisible(false)] 23 | 24 | //In order to begin building localizable applications, set 25 | //CultureYouAreCodingWith in your .csproj file 26 | //inside a . For example, if you are using US english 27 | //in your source files, set the to en-US. Then uncomment 28 | //the NeutralResourceLanguage attribute below. Update the "en-US" in 29 | //the line below to match the UICulture setting in the project file. 30 | 31 | //[assembly: NeutralResourcesLanguage("en-US", UltimateResourceFallbackLocation.Satellite)] 32 | 33 | 34 | [assembly: ThemeInfo( 35 | ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located 36 | //(used if a resource is not found in the page, 37 | // or application resource dictionaries) 38 | ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located 39 | //(used if a resource is not found in the page, 40 | // app, or any theme specific resource dictionaries) 41 | )] 42 | 43 | 44 | // Version information for an assembly consists of the following four values: 45 | // 46 | // Major Version 47 | // Minor Version 48 | // Build Number 49 | // Revision 50 | // 51 | // You can specify all the values or you can default the Build and Revision Numbers 52 | // by using the '*' as shown below: 53 | // [assembly: AssemblyVersion("1.0.*")] 54 | [assembly: AssemblyVersion("1.0.0.0")] 55 | [assembly: AssemblyFileVersion("1.0.0.0")] 56 | -------------------------------------------------------------------------------- /OnScreenKeyboard/Properties/Resources.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by a tool. 4 | // Runtime Version:4.0.30319.17929 5 | // 6 | // Changes to this file may cause incorrect behavior and will be lost if 7 | // the code is regenerated. 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace OnScreenKeyboard.Properties 12 | { 13 | 14 | 15 | /// 16 | /// A strongly-typed resource class, for looking up localized strings, etc. 17 | /// 18 | // This class was auto-generated by the StronglyTypedResourceBuilder 19 | // class via a tool like ResGen or Visual Studio. 20 | // To add or remove a member, edit your .ResX file then rerun ResGen 21 | // with the /str option, or rebuild your VS project. 22 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] 23 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 24 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 25 | internal class Resources 26 | { 27 | 28 | private static global::System.Resources.ResourceManager resourceMan; 29 | 30 | private static global::System.Globalization.CultureInfo resourceCulture; 31 | 32 | [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] 33 | internal Resources() 34 | { 35 | } 36 | 37 | /// 38 | /// Returns the cached ResourceManager instance used by this class. 39 | /// 40 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 41 | internal static global::System.Resources.ResourceManager ResourceManager 42 | { 43 | get 44 | { 45 | if ((resourceMan == null)) 46 | { 47 | global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("OnScreenKeyboard.Properties.Resources", typeof(Resources).Assembly); 48 | resourceMan = temp; 49 | } 50 | return resourceMan; 51 | } 52 | } 53 | 54 | /// 55 | /// Overrides the current thread's CurrentUICulture property for all 56 | /// resource lookups using this strongly typed resource class. 57 | /// 58 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 59 | internal static global::System.Globalization.CultureInfo Culture 60 | { 61 | get 62 | { 63 | return resourceCulture; 64 | } 65 | set 66 | { 67 | resourceCulture = value; 68 | } 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /OnScreenKeyboard/Properties/Resources.resx: -------------------------------------------------------------------------------- 1 |  2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | text/microsoft-resx 107 | 108 | 109 | 2.0 110 | 111 | 112 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 113 | 114 | 115 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | -------------------------------------------------------------------------------- /OnScreenKeyboard/Properties/Settings.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by a tool. 4 | // Runtime Version:4.0.30319.17929 5 | // 6 | // Changes to this file may cause incorrect behavior and will be lost if 7 | // the code is regenerated. 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace OnScreenKeyboard.Properties 12 | { 13 | 14 | 15 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 16 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "10.0.0.0")] 17 | internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase 18 | { 19 | 20 | private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); 21 | 22 | public static Settings Default 23 | { 24 | get 25 | { 26 | return defaultInstance; 27 | } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /OnScreenKeyboard/Properties/Settings.settings: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build status](https://ci.appveyor.com/api/projects/status/py2u4lm82ud0m91q?svg=true)](https://ci.appveyor.com/project/snmslavk/wpf-on-screen-keyboard) 2 | [![NuGet version](https://badge.fury.io/nu/WPFTouchKeyboard.svg)](https://badge.fury.io/nu/WPFTouchKeyboard) 3 | 4 | ## WPF Touch Keyboard Control 5 | ![WPF Keyboard component](https://viacheslavavsenev.gallerycdn.vsassets.io/extensions/viacheslavavsenev/wpfkeyboard/1.0/1482143221144/208491/1/68747470733a2f2f692e6779617a6f2e636f6d2f37343435656166366139346231326236633261323636373.gif) 6 | 7 | This is a component for WPF applications 8 | 9 | ### How to use 10 | #### Getting started 11 | Use nuget console 12 | 13 | PM> Install-Package WPFTouchKeyboard 14 | 15 | Add namespace to your xaml application 16 | 17 | xmlns:TermControls="clr-namespace:TermControls;assembly=TermControls" 18 | 19 | Then use it like 20 | 21 | 22 | 23 | #### Binding 24 | Also you can bind textbox or others component to this control via standard binding 25 | 26 | 27 | 28 | #### How to use handle EnterKeyPress 29 | 30 | 31 | 32 | where `m` is name of MainWindow 33 | 34 | And now add 35 | 36 | public ICommand ButtonClickCommand 37 | { 38 | get { return new DelegateCommand(ButtonClick); } 39 | } 40 | 41 | 42 | private void ButtonClick(object param) 43 | { 44 | System.Windows.MessageBox.Show("EnterClick!"); 45 | } 46 | 47 | 48 | #### Can I help you? 49 | 50 | Of course yes! Any pull-request will be considered. 51 | 52 | You can take any issue with the label [help wanted](https://github.com/snmslavk/WPF-Keyboard-Control/labels/help%20wanted) 53 | -------------------------------------------------------------------------------- /TermControls/Commands/DelegateCommand.cs: -------------------------------------------------------------------------------- 1 | // -------------------------------------------------------------------------------------------------------------------- 2 | // 3 | // The MIT License (MIT) 4 | // Copyright(c) 2014 Viacheslav Avsenev 5 | // 6 | // 7 | // The delegate command. 8 | // 9 | // -------------------------------------------------------------------------------------------------------------------- 10 | 11 | namespace TermControls.Commands 12 | { 13 | using System; 14 | using System.Windows.Input; 15 | 16 | /// 17 | /// The delegate command. 18 | /// 19 | public class DelegateCommand : ICommand 20 | { 21 | /// 22 | /// The action. 23 | /// 24 | private readonly Action action; 25 | 26 | /// 27 | /// Initializes a new instance of the class. 28 | /// 29 | /// 30 | /// The action. 31 | /// 32 | public DelegateCommand(Action action) 33 | { 34 | this.action = action; 35 | } 36 | 37 | /// 38 | /// The can execute changed. 39 | /// 40 | public event EventHandler CanExecuteChanged 41 | { 42 | add 43 | { 44 | } 45 | 46 | remove 47 | { 48 | } 49 | } 50 | 51 | /// 52 | /// The execute. 53 | /// 54 | /// 55 | /// The parameter. 56 | /// 57 | public void Execute(object parameter) 58 | { 59 | this.action(parameter); 60 | } 61 | 62 | /// 63 | /// The can execute. 64 | /// 65 | /// 66 | /// The parameter. 67 | /// 68 | /// 69 | /// The . 70 | /// 71 | public bool CanExecute(object parameter) 72 | { 73 | return true; 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /TermControls/Helpers/GridAutoLayout.cs: -------------------------------------------------------------------------------- 1 | // -------------------------------------------------------------------------------------------------------------------- 2 | // 3 | // The MIT License (MIT) 4 | // Copyright(c) 2014 Viacheslav Avsenev 5 | // 6 | // 7 | // The grid auto layout. 8 | // 9 | // -------------------------------------------------------------------------------------------------------------------- 10 | 11 | namespace TermControls.Helpers 12 | { 13 | using System.Windows; 14 | using System.Windows.Controls; 15 | 16 | /// 17 | /// The grid auto layout. 18 | /// 19 | public class GridAutoLayout 20 | { 21 | /// 22 | /// The number of columns property. 23 | /// 24 | public static readonly DependencyProperty NumberOfColumnsProperty = 25 | DependencyProperty.RegisterAttached( 26 | "NumberOfColumns", 27 | typeof(int), 28 | typeof(GridAutoLayout), 29 | new PropertyMetadata(1, NumberOfColumnsUpdated)); 30 | 31 | /// 32 | /// The number of rows property. 33 | /// 34 | public static readonly DependencyProperty NumberOfRowsProperty = 35 | DependencyProperty.RegisterAttached( 36 | "NumberOfRows", 37 | typeof(int), 38 | typeof(GridAutoLayout), 39 | new PropertyMetadata(1, NumberOfRowsUpdated)); 40 | 41 | /// 42 | /// The get number of columns. 43 | /// 44 | /// 45 | /// The object. 46 | /// 47 | /// 48 | /// The . 49 | /// 50 | public static int GetNumberOfColumns(DependencyObject obj) 51 | { 52 | return (int)obj.GetValue(NumberOfColumnsProperty); 53 | } 54 | 55 | /// 56 | /// The set number of columns. 57 | /// 58 | /// 59 | /// The object. 60 | /// 61 | /// 62 | /// The value. 63 | /// 64 | public static void SetNumberOfColumns(DependencyObject obj, int value) 65 | { 66 | obj.SetValue(NumberOfColumnsProperty, value); 67 | } 68 | 69 | /// 70 | /// The get number of rows. 71 | /// 72 | /// 73 | /// The object. 74 | /// 75 | /// 76 | /// The . 77 | /// 78 | public static int GetNumberOfRows(DependencyObject obj) 79 | { 80 | return (int)obj.GetValue(NumberOfRowsProperty); 81 | } 82 | 83 | /// 84 | /// The set number of rows. 85 | /// 86 | /// 87 | /// The object. 88 | /// 89 | /// 90 | /// The value. 91 | /// 92 | public static void SetNumberOfRows(DependencyObject obj, int value) 93 | { 94 | obj.SetValue(NumberOfRowsProperty, value); 95 | } 96 | 97 | /// 98 | /// The number of rows updated. 99 | /// 100 | /// 101 | /// The d. 102 | /// 103 | /// 104 | /// The e. 105 | /// 106 | private static void NumberOfRowsUpdated(DependencyObject d, DependencyPropertyChangedEventArgs e) 107 | { 108 | var grid = (Grid)d; 109 | 110 | grid.RowDefinitions.Clear(); 111 | for (var i = 0; i < (int)e.NewValue; i++) 112 | { 113 | grid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Star) }); 114 | } 115 | } 116 | 117 | /// 118 | /// The number of columns updated. 119 | /// 120 | /// 121 | /// The d. 122 | /// 123 | /// 124 | /// The e. 125 | /// 126 | private static void NumberOfColumnsUpdated(DependencyObject d, DependencyPropertyChangedEventArgs e) 127 | { 128 | var grid = (Grid)d; 129 | 130 | grid.ColumnDefinitions.Clear(); 131 | for (var i = 0; i < (int)e.NewValue; i++) 132 | { 133 | grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }); 134 | } 135 | } 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /TermControls/Images/Eng-Rus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snmslavk/WPF-Keyboard-Control/7493603e0b4a34d084f7f6cf6ca92ad56b68fc58/TermControls/Images/Eng-Rus.png -------------------------------------------------------------------------------- /TermControls/Images/button-70x70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snmslavk/WPF-Keyboard-Control/7493603e0b4a34d084f7f6cf6ca92ad56b68fc58/TermControls/Images/button-70x70.png -------------------------------------------------------------------------------- /TermControls/Images/delete.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snmslavk/WPF-Keyboard-Control/7493603e0b4a34d084f7f6cf6ca92ad56b68fc58/TermControls/Images/delete.png -------------------------------------------------------------------------------- /TermControls/Images/enter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snmslavk/WPF-Keyboard-Control/7493603e0b4a34d084f7f6cf6ca92ad56b68fc58/TermControls/Images/enter.png -------------------------------------------------------------------------------- /TermControls/Images/g-delete.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snmslavk/WPF-Keyboard-Control/7493603e0b4a34d084f7f6cf6ca92ad56b68fc58/TermControls/Images/g-delete.png -------------------------------------------------------------------------------- /TermControls/Images/g-enter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snmslavk/WPF-Keyboard-Control/7493603e0b4a34d084f7f6cf6ca92ad56b68fc58/TermControls/Images/g-enter.png -------------------------------------------------------------------------------- /TermControls/Images/g-shift.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snmslavk/WPF-Keyboard-Control/7493603e0b4a34d084f7f6cf6ca92ad56b68fc58/TermControls/Images/g-shift.png -------------------------------------------------------------------------------- /TermControls/Images/gEng-Rus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snmslavk/WPF-Keyboard-Control/7493603e0b4a34d084f7f6cf6ca92ad56b68fc58/TermControls/Images/gEng-Rus.png -------------------------------------------------------------------------------- /TermControls/Images/gbutton-70x70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snmslavk/WPF-Keyboard-Control/7493603e0b4a34d084f7f6cf6ca92ad56b68fc58/TermControls/Images/gbutton-70x70.png -------------------------------------------------------------------------------- /TermControls/Images/gprobel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snmslavk/WPF-Keyboard-Control/7493603e0b4a34d084f7f6cf6ca92ad56b68fc58/TermControls/Images/gprobel.png -------------------------------------------------------------------------------- /TermControls/Images/ok.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snmslavk/WPF-Keyboard-Control/7493603e0b4a34d084f7f6cf6ca92ad56b68fc58/TermControls/Images/ok.png -------------------------------------------------------------------------------- /TermControls/Images/okG.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snmslavk/WPF-Keyboard-Control/7493603e0b4a34d084f7f6cf6ca92ad56b68fc58/TermControls/Images/okG.png -------------------------------------------------------------------------------- /TermControls/Images/probel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snmslavk/WPF-Keyboard-Control/7493603e0b4a34d084f7f6cf6ca92ad56b68fc58/TermControls/Images/probel.png -------------------------------------------------------------------------------- /TermControls/Images/shift.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snmslavk/WPF-Keyboard-Control/7493603e0b4a34d084f7f6cf6ca92ad56b68fc58/TermControls/Images/shift.png -------------------------------------------------------------------------------- /TermControls/KeyImageProperty/KeyNotPressed.cs: -------------------------------------------------------------------------------- 1 | // -------------------------------------------------------------------------------------------------------------------- 2 | // 3 | // The MIT License (MIT) 4 | // Copyright(c) 2014 Viacheslav Avsenev 5 | // 6 | // 7 | // Defines the KeyNotPressed type. 8 | // 9 | // -------------------------------------------------------------------------------------------------------------------- 10 | 11 | namespace TermControls.KeyImageProperty 12 | { 13 | using System.Windows; 14 | using System.Windows.Media; 15 | 16 | /// 17 | /// The key not pressed. 18 | /// 19 | public class KeyNotPressed 20 | { 21 | /// 22 | /// The image property. 23 | /// 24 | public static readonly DependencyProperty ImageProperty; 25 | 26 | /// 27 | /// Initializes static members of the class. 28 | /// 29 | static KeyNotPressed() 30 | { 31 | var metadata = new FrameworkPropertyMetadata((ImageSource)null); 32 | ImageProperty = DependencyProperty.RegisterAttached( 33 | "Image", 34 | typeof(ImageSource), 35 | typeof(KeyNotPressed), 36 | metadata); 37 | } 38 | 39 | /// 40 | /// The get image. 41 | /// 42 | /// 43 | /// The object. 44 | /// 45 | /// 46 | /// The . 47 | /// 48 | public static ImageSource GetImage(DependencyObject obj) 49 | { 50 | return (ImageSource)obj.GetValue(ImageProperty); 51 | } 52 | 53 | /// 54 | /// The set image. 55 | /// 56 | /// 57 | /// The object. 58 | /// 59 | /// 60 | /// The value. 61 | /// 62 | public static void SetImage(DependencyObject obj, ImageSource value) 63 | { 64 | obj.SetValue(ImageProperty, value); 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /TermControls/KeyImageProperty/KeyPressed.cs: -------------------------------------------------------------------------------- 1 | // -------------------------------------------------------------------------------------------------------------------- 2 | // 3 | // The MIT License (MIT) 4 | // Copyright(c) 2014 Viacheslav Avsenev 5 | // 6 | // 7 | // Defines the KeyPressed type. 8 | // 9 | // -------------------------------------------------------------------------------------------------------------------- 10 | 11 | namespace TermControls.KeyImageProperty 12 | { 13 | using System.Windows; 14 | using System.Windows.Media; 15 | 16 | /// 17 | /// The key pressed. 18 | /// 19 | public class KeyPressed 20 | { 21 | /// 22 | /// The image property. 23 | /// 24 | public static readonly DependencyProperty ImageProperty; 25 | 26 | /// 27 | /// Initializes static members of the class. 28 | /// 29 | static KeyPressed() 30 | { 31 | var metadata = new FrameworkPropertyMetadata((ImageSource)null); 32 | ImageProperty = DependencyProperty.RegisterAttached( 33 | "Image", 34 | typeof(ImageSource), 35 | typeof(KeyPressed), 36 | metadata); 37 | } 38 | 39 | /// 40 | /// The get image. 41 | /// 42 | /// 43 | /// The object. 44 | /// 45 | /// 46 | /// The . 47 | /// 48 | public static ImageSource GetImage(DependencyObject obj) 49 | { 50 | return (ImageSource)obj.GetValue(ImageProperty); 51 | } 52 | 53 | /// 54 | /// The set image. 55 | /// 56 | /// 57 | /// The object. 58 | /// 59 | /// 60 | /// The value. 61 | /// 62 | public static void SetImage(DependencyObject obj, ImageSource value) 63 | { 64 | obj.SetValue(ImageProperty, value); 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /TermControls/Models/ButtonModel.cs: -------------------------------------------------------------------------------- 1 | namespace TermControls.Models 2 | { 3 | using System.ComponentModel; 4 | 5 | /// 6 | /// The button model. 7 | /// 8 | public class ButtonModel : INotifyPropertyChanged 9 | { 10 | /// 11 | /// The content. 12 | /// 13 | private string content; 14 | 15 | /// 16 | /// Gets or sets the content. 17 | /// 18 | public string Content 19 | { 20 | get 21 | { 22 | return this.content; 23 | } 24 | 25 | set 26 | { 27 | this.content = value; 28 | this.OnPropertyChanged("Content"); 29 | } 30 | } 31 | 32 | /// 33 | /// Gets or sets the name. 34 | /// 35 | public string Name { get; set; } 36 | 37 | /// 38 | /// Gets or sets the column. 39 | /// 40 | public int Column { get; set; } 41 | 42 | #region INotifyPropertyChanged Members 43 | 44 | /// 45 | /// The property changed. 46 | /// 47 | public event PropertyChangedEventHandler PropertyChanged; 48 | 49 | /// 50 | /// The on property changed. 51 | /// 52 | /// 53 | /// The property name. 54 | /// 55 | private void OnPropertyChanged(string propertyName) 56 | { 57 | this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); 58 | } 59 | 60 | #endregion 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /TermControls/Models/KeyboardModel.cs: -------------------------------------------------------------------------------- 1 | namespace TermControls.Models 2 | { 3 | using System; 4 | using System.Collections.ObjectModel; 5 | using System.ComponentModel; 6 | 7 | /// 8 | /// The keyboard model. 9 | /// 10 | public class KeyboardModel : INotifyPropertyChanged 11 | { 12 | /// 13 | /// Gets or sets the content 1. 14 | /// 15 | protected string[] Content1 { get; set; } 16 | 17 | /// 18 | /// Gets or sets the content 1 shift. 19 | /// 20 | protected string[] Content1Shift { get; set; } 21 | 22 | /// 23 | /// Gets or sets the content 2. 24 | /// 25 | protected string[] Content2 { get; set; } 26 | 27 | /// 28 | /// Gets or sets the content 2 shift. 29 | /// 30 | protected string[] Content2Shift { get; set; } 31 | 32 | /// 33 | /// Gets or sets a value indicating whether is shift. 34 | /// 35 | public bool IsShift { get; set; } 36 | 37 | /// 38 | /// Gets or sets a value indicating whether is eng rus. 39 | /// 40 | public bool IsEngRus { get; set; } 41 | 42 | /// 43 | /// Gets the content. 44 | /// 45 | public string[] Content 46 | { 47 | get 48 | { 49 | if (!this.IsShift && this.IsEngRus) return this.Content1; 50 | if (this.IsShift && this.IsEngRus) return this.Content1Shift; 51 | if (!this.IsShift && !this.IsEngRus) return this.Content2; 52 | if (this.IsShift && !this.IsEngRus) return this.Content2Shift; 53 | return null; 54 | } 55 | } 56 | 57 | /// 58 | /// Gets the buttons raw 1. 59 | /// 60 | public ObservableCollection ButtonsRaw1 { get; private set; } 61 | 62 | /// 63 | /// Gets the buttons raw 2. 64 | /// 65 | public ObservableCollection ButtonsRaw2 { get; private set; } 66 | 67 | /// 68 | /// Gets the buttons raw 3. 69 | /// 70 | public ObservableCollection ButtonsRaw3 { get; private set; } 71 | 72 | /// 73 | /// Gets the buttons raw 4. 74 | /// 75 | public ObservableCollection ButtonsRaw4 { get; private set; } 76 | 77 | /// 78 | /// The text. 79 | /// 80 | private string text; 81 | 82 | /// 83 | /// Gets or sets the text. 84 | /// 85 | public string Text 86 | { 87 | get 88 | { 89 | return this.text; 90 | } 91 | set 92 | { 93 | this.text = value; 94 | this.OnPropertyChanged("Text"); 95 | } 96 | } 97 | 98 | #region Methods 99 | 100 | /// 101 | /// The get button content. 102 | /// 103 | /// 104 | /// The btn name. 105 | /// 106 | /// 107 | /// The . 108 | /// 109 | public string GetButtonContent(string btnName) 110 | { 111 | var raw = Convert.ToInt32(btnName[1].ToString()) - 1; 112 | var col = btnName.Length == 3 113 | ? Convert.ToInt32(btnName[2].ToString()) - 1 114 | : Convert.ToInt32(btnName[2] + btnName[3].ToString()) - 1; 115 | return this.Content[raw][col].ToString(); 116 | } 117 | 118 | /// 119 | /// The change buttons content. 120 | /// 121 | public void ChangeButtonsContent() 122 | { 123 | this.ChangeButtonsContent(this.ButtonsRaw1, 0); 124 | this.ChangeButtonsContent(this.ButtonsRaw2, 1); 125 | this.ChangeButtonsContent(this.ButtonsRaw3, 2); 126 | this.ChangeButtonsContent(this.ButtonsRaw4, 3); 127 | } 128 | 129 | /// 130 | /// The create buttons. 131 | /// 132 | public void CreateButtons() 133 | { 134 | this.ButtonsRaw1 = this.CreateButtons(0); 135 | this.ButtonsRaw2 = this.CreateButtons(1); 136 | this.ButtonsRaw3 = this.CreateButtons(2); 137 | this.ButtonsRaw4 = this.CreateButtons(3); 138 | } 139 | 140 | /// 141 | /// Initializes a new instance of the class. 142 | /// 143 | public KeyboardModel() 144 | { 145 | this.IsShift = false; 146 | this.IsEngRus = false; 147 | this.InitContent(); 148 | } 149 | 150 | /// 151 | /// The init content. 152 | /// 153 | public virtual void InitContent() 154 | { 155 | } 156 | 157 | /// 158 | /// The create buttons. 159 | /// 160 | /// 161 | /// The _raw. 162 | /// 163 | /// 164 | /// The . 165 | /// 166 | private ObservableCollection CreateButtons(int raw) 167 | { 168 | var buttons = new ObservableCollection(); 169 | for (var j = 1; j <= this.Content[raw].Length; j++) 170 | { 171 | var name = $"b{raw + 1}{j}"; 172 | buttons.Add(new ButtonModel { Name = name, Column = j - 1, Content = this.GetButtonContent(name) }); 173 | } 174 | return buttons; 175 | } 176 | 177 | /// 178 | /// The change buttons content. 179 | /// 180 | /// 181 | /// The _buttons. 182 | /// 183 | /// 184 | /// The _raw. 185 | /// 186 | private void ChangeButtonsContent(ObservableCollection buttons, int _raw) 187 | { 188 | for (var j = 1; j <= this.Content[_raw].Length; j++) buttons[j - 1].Content = this.GetButtonContent(buttons[j - 1].Name); 189 | } 190 | 191 | #endregion 192 | 193 | #region INotifyPropertyChanged Members 194 | 195 | /// 196 | /// The property changed. 197 | /// 198 | public event PropertyChangedEventHandler PropertyChanged; 199 | 200 | /// 201 | /// The on property changed. 202 | /// 203 | /// 204 | /// The property name. 205 | /// 206 | private void OnPropertyChanged(string propertyName) 207 | { 208 | this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); 209 | } 210 | 211 | #endregion 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /TermControls/Models/KeyboardModelRuEng.cs: -------------------------------------------------------------------------------- 1 | // -------------------------------------------------------------------------------------------------------------------- 2 | // 3 | // The MIT License (MIT) 4 | // Copyright(c) 2014 Viacheslav Avsenev 5 | // 6 | // 7 | // Defines the KeyboardModelRuEng type. 8 | // 9 | // -------------------------------------------------------------------------------------------------------------------- 10 | 11 | namespace TermControls.Models 12 | { 13 | /// 14 | /// The keyboard model russian english. 15 | /// 16 | public class KeyboardModelRuEng : KeyboardModel 17 | { 18 | /// 19 | /// Initializes a new instance of the class. 20 | /// 21 | public KeyboardModelRuEng() 22 | : base() 23 | { 24 | } 25 | 26 | /// 27 | /// The initialize content. 28 | /// 29 | public override void InitContent() 30 | { 31 | this.Content1 = new[] { "1234567890-", "йцукенгшщзхъ", "фывапролджэ", "ячсмитьбю,.?" }; 32 | this.Content1Shift = new[] { "!\"№;%:?*()-", "ЙЦУКЕНГШЩЗХЪ", "ФЫВАПРОЛДЖЭ", "ЯЧСМИТЬБЮ,.?" }; 33 | this.Content2 = new[] { "1234567890-", "qwertyuiop[]", "asdfghjkl;'", "zxcvbnm<>,.?" }; 34 | this.Content2Shift = new[] { "!@#$%^&*()-", "QWERTYUIOP{}", "ASDFGHJKL:\"", "ZXCVBNM<>,.?" }; 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /TermControls/OnScreenKeyboard.xaml: -------------------------------------------------------------------------------- 1 |  12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 |