├── .gitignore ├── CurrencyTextBoxSample.png ├── LICENSE ├── Logo.docx ├── Logo.png ├── README.md ├── example.gif └── src ├── CurrencyTextBoxControl.sln ├── CurrencyTextBoxControl ├── CurrencyTextBox.cs ├── CurrencyTextBox.csproj ├── KeyValidator.cs ├── Properties │ ├── AssemblyInfo.cs │ ├── Resources.Designer.cs │ ├── Resources.resx │ ├── Settings.Designer.cs │ └── Settings.settings ├── Themes │ └── Generic.xaml └── license.txt └── CurrencyTextBoxExample ├── App.xaml ├── App.xaml.cs ├── CurrencyTextBox.Sample.csproj ├── MainWindow.xaml ├── MainWindow.xaml.cs └── Properties ├── AssemblyInfo.cs ├── Resources.Designer.cs ├── Resources.resx ├── Settings.Designer.cs └── Settings.settings /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | x64/ 19 | x86/ 20 | bld/ 21 | [Bb]in/ 22 | [Oo]bj/ 23 | 24 | # Visual Studio 2015 cache/options directory 25 | .vs/ 26 | # Uncomment if you have tasks that create the project's static files in wwwroot 27 | #wwwroot/ 28 | 29 | # MSTest test Results 30 | [Tt]est[Rr]esult*/ 31 | [Bb]uild[Ll]og.* 32 | 33 | # NUNIT 34 | *.VisualState.xml 35 | TestResult.xml 36 | 37 | # Build Results of an ATL Project 38 | [Dd]ebugPS/ 39 | [Rr]eleasePS/ 40 | dlldata.c 41 | 42 | # DNX 43 | project.lock.json 44 | artifacts/ 45 | 46 | *_i.c 47 | *_p.c 48 | *_i.h 49 | *.ilk 50 | *.meta 51 | *.obj 52 | *.pch 53 | *.pdb 54 | *.pgc 55 | *.pgd 56 | *.rsp 57 | *.sbr 58 | *.tlb 59 | *.tli 60 | *.tlh 61 | *.tmp 62 | *.tmp_proj 63 | *.log 64 | *.vspscc 65 | *.vssscc 66 | .builds 67 | *.pidb 68 | *.svclog 69 | *.scc 70 | 71 | # Chutzpah Test files 72 | _Chutzpah* 73 | 74 | # Visual C++ cache files 75 | ipch/ 76 | *.aps 77 | *.ncb 78 | *.opendb 79 | *.opensdf 80 | *.sdf 81 | *.cachefile 82 | 83 | # Visual Studio profiler 84 | *.psess 85 | *.vsp 86 | *.vspx 87 | *.sap 88 | 89 | # TFS 2012 Local Workspace 90 | $tf/ 91 | 92 | # Guidance Automation Toolkit 93 | *.gpState 94 | 95 | # ReSharper is a .NET coding add-in 96 | _ReSharper*/ 97 | *.[Rr]e[Ss]harper 98 | *.DotSettings.user 99 | 100 | # JustCode is a .NET coding add-in 101 | .JustCode 102 | 103 | # TeamCity is a build add-in 104 | _TeamCity* 105 | 106 | # DotCover is a Code Coverage Tool 107 | *.dotCover 108 | 109 | # NCrunch 110 | _NCrunch_* 111 | .*crunch*.local.xml 112 | nCrunchTemp_* 113 | 114 | # MightyMoose 115 | *.mm.* 116 | AutoTest.Net/ 117 | 118 | # Web workbench (sass) 119 | .sass-cache/ 120 | 121 | # Installshield output folder 122 | [Ee]xpress/ 123 | 124 | # DocProject is a documentation generator add-in 125 | DocProject/buildhelp/ 126 | DocProject/Help/*.HxT 127 | DocProject/Help/*.HxC 128 | DocProject/Help/*.hhc 129 | DocProject/Help/*.hhk 130 | DocProject/Help/*.hhp 131 | DocProject/Help/Html2 132 | DocProject/Help/html 133 | 134 | # Click-Once directory 135 | publish/ 136 | 137 | # Publish Web Output 138 | *.[Pp]ublish.xml 139 | *.azurePubxml 140 | # TODO: Comment the next line if you want to checkin your web deploy settings 141 | # but database connection strings (with potential passwords) will be unencrypted 142 | *.pubxml 143 | *.publishproj 144 | 145 | # NuGet Packages 146 | *.nupkg 147 | # The packages folder can be ignored because of Package Restore 148 | **/packages/* 149 | # except build/, which is used as an MSBuild target. 150 | !**/packages/build/ 151 | # Uncomment if necessary however generally it will be regenerated when needed 152 | #!**/packages/repositories.config 153 | # NuGet v3's project.json files produces more ignoreable files 154 | *.nuget.props 155 | *.nuget.targets 156 | 157 | # Microsoft Azure Build Output 158 | csx/ 159 | *.build.csdef 160 | 161 | # Microsoft Azure Emulator 162 | ecf/ 163 | rcf/ 164 | 165 | # Microsoft Azure ApplicationInsights config file 166 | ApplicationInsights.config 167 | 168 | # Windows Store app package directory 169 | AppPackages/ 170 | BundleArtifacts/ 171 | 172 | # Visual Studio cache files 173 | # files ending in .cache can be ignored 174 | *.[Cc]ache 175 | # but keep track of directories ending in .cache 176 | !*.[Cc]ache/ 177 | 178 | # Others 179 | ClientBin/ 180 | ~$* 181 | *~ 182 | *.dbmdl 183 | *.dbproj.schemaview 184 | *.pfx 185 | *.publishsettings 186 | node_modules/ 187 | orleans.codegen.cs 188 | 189 | # RIA/Silverlight projects 190 | Generated_Code/ 191 | 192 | # Backup & report files from converting an old project file 193 | # to a newer Visual Studio version. Backup files are not needed, 194 | # because we have git ;-) 195 | _UpgradeReport_Files/ 196 | Backup*/ 197 | UpgradeLog*.XML 198 | UpgradeLog*.htm 199 | 200 | # SQL Server files 201 | *.mdf 202 | *.ldf 203 | 204 | # Business Intelligence projects 205 | *.rdl.data 206 | *.bim.layout 207 | *.bim_*.settings 208 | 209 | # Microsoft Fakes 210 | FakesAssemblies/ 211 | 212 | # GhostDoc plugin setting file 213 | *.GhostDoc.xml 214 | 215 | # Node.js Tools for Visual Studio 216 | .ntvs_analysis.dat 217 | 218 | # Visual Studio 6 build log 219 | *.plg 220 | 221 | # Visual Studio 6 workspace options file 222 | *.opt 223 | 224 | # Visual Studio LightSwitch build output 225 | **/*.HTMLClient/GeneratedArtifacts 226 | **/*.DesktopClient/GeneratedArtifacts 227 | **/*.DesktopClient/ModelManifest.xml 228 | **/*.Server/GeneratedArtifacts 229 | **/*.Server/ModelManifest.xml 230 | _Pvt_Extensions 231 | 232 | # Paket dependency manager 233 | .paket/paket.exe 234 | 235 | # FAKE - F# Make 236 | .fake/ 237 | -------------------------------------------------------------------------------- /CurrencyTextBoxSample.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abbaye/WpfCurrencyTextbox/621964db83c7bd88f368121669d54fdf5b37370a/CurrencyTextBoxSample.png -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Adam Anderson, Derek Tremblay 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 | -------------------------------------------------------------------------------- /Logo.docx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abbaye/WpfCurrencyTextbox/621964db83c7bd88f368121669d54fdf5b37370a/Logo.docx -------------------------------------------------------------------------------- /Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abbaye/WpfCurrencyTextbox/621964db83c7bd88f368121669d54fdf5b37370a/Logo.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![example](Logo.png?raw=true) 2 | 3 | [![NuGet](https://img.shields.io/badge/Nuget-1.4.3-brightgreen.svg)](https://www.nuget.org/packages/CurrencyTextBox/) 4 | [![NetFramework](https://img.shields.io/badge/.Net%20Framework-4.5-green.svg)](https://www.microsoft.com/net/download/windows) 5 | [![License](https://img.shields.io/badge/License-MIT-blue.svg)](https://github.com/abbaye/wpf-currency-textbox/blob/ExpendedFunctionality/LICENSE) 6 | 7 | A Wpf TextBox for entering a currency/numeric/percent value, similar to how a cash register works. 8 | 9 | ## Samples of use screenshot 10 | ![example](CurrencyTextBoxSample.png?raw=true) 11 | 12 | ## Features 13 | - Numbers typed are pushed in from the right. If we start with the default value 0.00, and start typing the numbers 123, the value updates as such: 0.00 => 0.01 => 0.12 => 1.23 14 | - You can set Maximum and Minimum value. 15 | - If we press the backspace key, the numbers are shifted right: 1.23 => 0.12 => 0.01 => 0.00 16 | - If we press the delete key, the value is reset to 0.00. 17 | - If we press the minus key, the value becomes negative. 18 | - If we press down/up key, the value are increase/decrease 19 | - Support stringformat C to C6. 20 | - Support stringformat N to N6. 21 | - Support stringformat P to P6. 22 | - Copy and paste decimal value. ctrl+c / ctrl+v 23 | - Undo/Redo value with default ctrl+z / ctrl+y 24 | - This control's template can be customized to change the appearance. 25 | - Supports data validation. 26 | - Auto color (Red/Black) when negative or positive value 27 | - ... 28 | 29 | ## Release Notes 30 | New in version 1.4.0 31 | - ADD support of percent string format (P to P6) 32 | 33 | New in version 1.3.1 34 | - BUG FIX When popup closed the window do not freeze 35 | - Little desing update un popup 36 | - Add PopupClosed event 37 | 38 | New in version 1.3.0 39 | - Add support of Undolimit 40 | - Add functionality ADD/REMOVE popup (Enter Key when CanShowAddPanel = true) 41 | 42 | New in version 1.2.0 43 | - Add support of numeric value (Stringformat N to N6) 44 | 45 | New in version 1.1.1 46 | - Validate MinimumValue / MaximumValue dependency properties 47 | - Validate StringFormat dependency property 48 | 49 | New in version 1.1.0 50 | - Add support of Undo/Redo 51 | - Add Copy/Paste 52 | 53 | ## How to use 54 | Add a reference to `CurrencyTextBoxControl.dll` from your project, then add the following namespace to your XAML: 55 | 56 | ```xaml 57 | xmlns:currency="clr-namespace:CurrencyTextBoxControl;assembly=CurrencyTextBoxControl" 58 | ``` 59 | 60 | Insert the control like this: 61 | 62 | ```xaml 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 77 | 78 | 79 | ``` 80 | -------------------------------------------------------------------------------- /example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abbaye/WpfCurrencyTextbox/621964db83c7bd88f368121669d54fdf5b37370a/example.gif -------------------------------------------------------------------------------- /src/CurrencyTextBoxControl.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.29613.14 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CurrencyTextBox", "CurrencyTextBoxControl\CurrencyTextBox.csproj", "{144A2EE7-09EC-4618-BA94-6593205962C9}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CurrencyTextBox.Sample", "CurrencyTextBoxExample\CurrencyTextBox.Sample.csproj", "{13F3D236-4FC1-433F-9F2E-E0932035A5AD}" 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 | {144A2EE7-09EC-4618-BA94-6593205962C9}.Debug|Any CPU.ActiveCfg = Release|Any CPU 21 | {144A2EE7-09EC-4618-BA94-6593205962C9}.Debug|Any CPU.Build.0 = Release|Any CPU 22 | {144A2EE7-09EC-4618-BA94-6593205962C9}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU 23 | {144A2EE7-09EC-4618-BA94-6593205962C9}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU 24 | {144A2EE7-09EC-4618-BA94-6593205962C9}.Debug|x86.ActiveCfg = Debug|Any CPU 25 | {144A2EE7-09EC-4618-BA94-6593205962C9}.Release|Any CPU.ActiveCfg = Release|Any CPU 26 | {144A2EE7-09EC-4618-BA94-6593205962C9}.Release|Any CPU.Build.0 = Release|Any CPU 27 | {144A2EE7-09EC-4618-BA94-6593205962C9}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU 28 | {144A2EE7-09EC-4618-BA94-6593205962C9}.Release|Mixed Platforms.Build.0 = Release|Any CPU 29 | {144A2EE7-09EC-4618-BA94-6593205962C9}.Release|x86.ActiveCfg = Release|Any CPU 30 | {13F3D236-4FC1-433F-9F2E-E0932035A5AD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 31 | {13F3D236-4FC1-433F-9F2E-E0932035A5AD}.Debug|Any CPU.Build.0 = Debug|Any CPU 32 | {13F3D236-4FC1-433F-9F2E-E0932035A5AD}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU 33 | {13F3D236-4FC1-433F-9F2E-E0932035A5AD}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU 34 | {13F3D236-4FC1-433F-9F2E-E0932035A5AD}.Debug|x86.ActiveCfg = Debug|Any CPU 35 | {13F3D236-4FC1-433F-9F2E-E0932035A5AD}.Debug|x86.Build.0 = Debug|Any CPU 36 | {13F3D236-4FC1-433F-9F2E-E0932035A5AD}.Release|Any CPU.ActiveCfg = Release|Any CPU 37 | {13F3D236-4FC1-433F-9F2E-E0932035A5AD}.Release|Any CPU.Build.0 = Release|Any CPU 38 | {13F3D236-4FC1-433F-9F2E-E0932035A5AD}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU 39 | {13F3D236-4FC1-433F-9F2E-E0932035A5AD}.Release|Mixed Platforms.Build.0 = Release|Any CPU 40 | {13F3D236-4FC1-433F-9F2E-E0932035A5AD}.Release|x86.ActiveCfg = Release|Any CPU 41 | {13F3D236-4FC1-433F-9F2E-E0932035A5AD}.Release|x86.Build.0 = Release|Any CPU 42 | EndGlobalSection 43 | GlobalSection(SolutionProperties) = preSolution 44 | HideSolutionNode = FALSE 45 | EndGlobalSection 46 | GlobalSection(ExtensibilityGlobals) = postSolution 47 | SolutionGuid = {9D14C0C5-4885-4C52-8533-417678AC2954} 48 | EndGlobalSection 49 | EndGlobal 50 | -------------------------------------------------------------------------------- /src/CurrencyTextBoxControl/CurrencyTextBox.cs: -------------------------------------------------------------------------------- 1 | // Fork from mtusk CurrencyTextBox 2 | // https://github.com/mtusk/wpf-currency-textbox 3 | // 4 | // Fork 2016-2019 by Derek Tremblay (Abbaye) 5 | // derektremblay666@gmail.com 6 | 7 | using System; 8 | using System.Collections.Generic; 9 | using System.Globalization; 10 | using System.Windows; 11 | using System.Windows.Controls; 12 | using System.Windows.Controls.Primitives; 13 | using System.Windows.Data; 14 | using System.Windows.Input; 15 | using System.Windows.Markup; 16 | using System.Windows.Media; 17 | 18 | namespace CurrencyTextBoxControl 19 | { 20 | [ContentProperty("Number")] 21 | public class CurrencyTextBox : TextBox 22 | { 23 | #region Global variables / Event 24 | 25 | private readonly List _undoList = new List(); 26 | private readonly List _redoList = new List(); 27 | private Popup _popup; 28 | private Label _popupLabel; 29 | private decimal _numberBeforePopup; 30 | 31 | //Event 32 | public event EventHandler PopupClosed; 33 | public event EventHandler NumberChanged; 34 | #endregion Global variables 35 | 36 | #region Constructor 37 | static CurrencyTextBox() => 38 | DefaultStyleKeyProperty.OverrideMetadata( 39 | typeof(CurrencyTextBox), 40 | new FrameworkPropertyMetadata(typeof(CurrencyTextBox))); 41 | 42 | public override void OnApplyTemplate() 43 | { 44 | base.OnApplyTemplate(); 45 | 46 | // Bind Text to Number with the specified StringFormat 47 | var textBinding = new Binding 48 | { 49 | Path = new PropertyPath(nameof(Number)), 50 | RelativeSource = new RelativeSource(RelativeSourceMode.Self), 51 | UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged, 52 | StringFormat = StringFormat, 53 | ConverterCulture = Culture 54 | }; 55 | 56 | BindingOperations.SetBinding(this, TextProperty, textBinding); 57 | 58 | // Disable copy/paste 59 | DataObject.AddCopyingHandler(this, CopyPasteEventHandler); 60 | DataObject.AddPastingHandler(this, CopyPasteEventHandler); 61 | 62 | //Events 63 | CaretIndex = Text.LastIndexOfAny(new[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' }) + 1; 64 | 65 | PreviewKeyDown += TextBox_PreviewKeyDown; 66 | PreviewMouseDown += TextBox_PreviewMouseDown; 67 | PreviewMouseUp += TextBox_PreviewMouseUp; 68 | TextChanged += TextBox_TextChanged; 69 | 70 | //Disable contextmenu 71 | ContextMenu = null; 72 | 73 | //Set Touch Keyboard to Numeric Pad - pbrombach@gmail.com 24-12-2019 74 | InputScope scope = new InputScope(); 75 | scope.Names.Add(new InputScopeName 76 | { 77 | NameValue = InputScopeNameValue.Number 78 | }); 79 | InputScope = scope; 80 | 81 | } 82 | #endregion Constructor 83 | 84 | #region Dependency Properties 85 | 86 | public static readonly DependencyProperty CultureProperty = DependencyProperty.Register( 87 | nameof(Culture), typeof(CultureInfo), typeof(CurrencyTextBox), 88 | new PropertyMetadata(CultureInfo.CurrentCulture, CulturePropertyChanged)); 89 | 90 | private static void CulturePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) 91 | { 92 | var textBinding = new Binding 93 | { 94 | Path = new PropertyPath("Number"), 95 | RelativeSource = new RelativeSource(RelativeSourceMode.Self), 96 | UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged, 97 | StringFormat = (string)d.GetValue(StringFormatProperty), 98 | ConverterCulture = (CultureInfo)e.NewValue 99 | }; 100 | 101 | BindingOperations.SetBinding(d, TextProperty, textBinding); 102 | } 103 | 104 | public CultureInfo Culture 105 | { 106 | get => (CultureInfo)GetValue(CultureProperty); 107 | set => SetValue(CultureProperty, value); 108 | } 109 | 110 | public static readonly DependencyProperty NumberProperty = DependencyProperty.Register( 111 | nameof(Number), typeof(decimal), typeof(CurrencyTextBox), 112 | new FrameworkPropertyMetadata(0M, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, 113 | NumberPropertyChanged, NumberPropertyCoerceValue), NumberPropertyValidated); 114 | 115 | private static bool NumberPropertyValidated(object value) => value is decimal; 116 | 117 | private static object NumberPropertyCoerceValue(DependencyObject d, object baseValue) 118 | { 119 | if (!(d is CurrencyTextBox ctb)) return baseValue; 120 | 121 | var value = (decimal)baseValue; 122 | 123 | //Check maximum value 124 | if (value > ctb.MaximumValue && ctb.MaximumValue > 0) 125 | return ctb.MaximumValue; 126 | 127 | if (value < ctb.MinimumValue && ctb.MinimumValue < 0) 128 | return ctb.MinimumValue; 129 | 130 | return value; 131 | } 132 | 133 | private static void NumberPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) 134 | { 135 | if (!(d is CurrencyTextBox ctb)) return; 136 | 137 | //Update IsNegative 138 | ctb.SetValue(IsNegativeProperty, ctb.Number < 0); 139 | 140 | //Launch event 141 | ctb.NumberChanged?.Invoke(ctb, new EventArgs()); 142 | } 143 | 144 | public decimal Number 145 | { 146 | get => (decimal)GetValue(NumberProperty); 147 | set => SetValue(NumberProperty, value); 148 | } 149 | 150 | public static readonly DependencyProperty IsNegativeProperty = 151 | DependencyProperty.Register(nameof(IsNegative), typeof(bool), typeof(CurrencyTextBox), new PropertyMetadata(false)); 152 | 153 | public bool IsNegative => (bool)GetValue(IsNegativeProperty); 154 | 155 | public bool IsCalculPanelMode 156 | { 157 | get => (bool)GetValue(IsCalculPanelModeProperty); 158 | set => SetValue(IsCalculPanelModeProperty, value); 159 | } 160 | 161 | // Using a DependencyProperty as the backing store for IsCalculPanelMode. This enables animation, styling, binding, etc... 162 | public static readonly DependencyProperty IsCalculPanelModeProperty = 163 | DependencyProperty.Register(nameof(IsCalculPanelMode), typeof(bool), typeof(CurrencyTextBox), new PropertyMetadata(false)); 164 | 165 | public bool CanShowAddPanel 166 | { 167 | get => (bool)GetValue(CanShowAddPanelProperty); 168 | set => SetValue(CanShowAddPanelProperty, value); 169 | } 170 | 171 | /// 172 | /// Set for enabling the calcul panel 173 | /// 174 | public static readonly DependencyProperty CanShowAddPanelProperty = 175 | DependencyProperty.Register(nameof(CanShowAddPanel), typeof(bool), typeof(CurrencyTextBox), new PropertyMetadata(false)); 176 | 177 | public static readonly DependencyProperty MaximumValueProperty = 178 | DependencyProperty.Register(nameof(MaximumValue), typeof(decimal), typeof(CurrencyTextBox), 179 | new FrameworkPropertyMetadata(0M, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, 180 | MaximumValuePropertyChanged, MaximumCoerceValue), MaximumValidateValue); 181 | 182 | private static bool MaximumValidateValue(object value) => (decimal)value <= decimal.MaxValue / 2; 183 | 184 | private static object MaximumCoerceValue(DependencyObject d, object baseValue) 185 | { 186 | var ctb = d as CurrencyTextBox; 187 | 188 | if (ctb.MaximumValue > decimal.MaxValue / 2) 189 | return decimal.MaxValue / 2; 190 | 191 | return baseValue; 192 | } 193 | 194 | private static void MaximumValuePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) 195 | { 196 | var ctb = d as CurrencyTextBox; 197 | 198 | if (ctb.Number > (decimal)e.NewValue) 199 | ctb.Number = (decimal)e.NewValue; 200 | } 201 | 202 | public decimal MaximumValue 203 | { 204 | get => (decimal)GetValue(MaximumValueProperty); 205 | set => SetValue(MaximumValueProperty, value); 206 | } 207 | 208 | public decimal MinimumValue 209 | { 210 | get => (decimal)GetValue(MinimumValueProperty); 211 | set => SetValue(MinimumValueProperty, value); 212 | } 213 | 214 | public static readonly DependencyProperty MinimumValueProperty = 215 | DependencyProperty.Register(nameof(MinimumValue), typeof(decimal), typeof(CurrencyTextBox), 216 | new FrameworkPropertyMetadata(0M, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, 217 | MinimumValuePropertyChanged, MinimumCoerceValue), MinimumValidateValue); 218 | 219 | private static bool MinimumValidateValue(object value) => 220 | (decimal)value >= decimal.MinValue / 2; //&& (decimal)value <= 0; 221 | 222 | private static object MinimumCoerceValue(DependencyObject d, object baseValue) 223 | { 224 | var ctb = d as CurrencyTextBox; 225 | 226 | if (ctb.MinimumValue < decimal.MinValue / 2) 227 | return decimal.MinValue / 2; 228 | 229 | return baseValue; 230 | } 231 | 232 | private static void MinimumValuePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) 233 | { 234 | var ctb = d as CurrencyTextBox; 235 | 236 | if (ctb.Number < (decimal)e.NewValue) 237 | ctb.Number = (decimal)e.NewValue; 238 | } 239 | 240 | public static readonly DependencyProperty StringFormatProperty = DependencyProperty.Register( 241 | nameof(StringFormat), typeof(string), typeof(CurrencyTextBox), 242 | new FrameworkPropertyMetadata("C2", FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, 243 | StringFormatPropertyChanged, StringFormatCoerceValue), StringFormatValidateValue); 244 | 245 | private static object StringFormatCoerceValue(DependencyObject d, object baseValue) => 246 | ((string)baseValue).ToUpper(); 247 | 248 | /// 249 | /// Validate the StringFormat 250 | /// 251 | private static bool StringFormatValidateValue(object value) 252 | { 253 | var val = value.ToString().ToUpper(); 254 | 255 | return val == "C0" || val == "C" || val == "C1" || val == "C2" || val == "C3" || val == "C4" || val == "C5" || val == "C6" || 256 | val == "N0" || val == "N" || val == "N1" || val == "N2" || val == "N3" || val == "N4" || val == "N5" || val == "N6" || 257 | val == "P0" || val == "P" || val == "P1" || val == "P2" || val == "P3" || val == "P4" || val == "P5" || val == "P6"; 258 | } 259 | 260 | public string StringFormat 261 | { 262 | get => (string)GetValue(StringFormatProperty); 263 | set => SetValue(StringFormatProperty, value); 264 | } 265 | 266 | private static void StringFormatPropertyChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e) 267 | { 268 | // Update the Text binding with the new StringFormat 269 | var textBinding = new Binding 270 | { 271 | Path = new PropertyPath("Number"), 272 | RelativeSource = new RelativeSource(RelativeSourceMode.Self), 273 | UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged, 274 | StringFormat = (string)e.NewValue, 275 | ConverterCulture = (CultureInfo)obj.GetValue(CultureProperty) 276 | }; 277 | 278 | BindingOperations.SetBinding(obj, TextProperty, textBinding); 279 | } 280 | 281 | public int UpDownRepeat 282 | { 283 | get => (int)GetValue(UpDownRepeatProperty); 284 | set => SetValue(UpDownRepeatProperty, value); 285 | } 286 | 287 | /// 288 | /// Set the Up/down value when key repeated 289 | /// 290 | public static readonly DependencyProperty UpDownRepeatProperty = 291 | DependencyProperty.Register(nameof(UpDownRepeat), typeof(int), typeof(CurrencyTextBox), new PropertyMetadata(10)); 292 | 293 | #endregion Dependency Properties 294 | 295 | 296 | public void Insert(Key K) 297 | { 298 | AddUndoInList(Number); 299 | InsertKey(K); 300 | } 301 | 302 | 303 | #region Events 304 | private void TextBox_TextChanged(object sender, TextChangedEventArgs e) 305 | { 306 | var tb = sender as TextBox; 307 | tb.CaretIndex = tb.Text.LastIndexOfAny(new[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' }) + 1; 308 | 309 | //if (Number < 0 && tb.Text.EndsWith(")")) 310 | // tb.CaretIndex = tb.Text.Length - 1; 311 | //else if (tb.Text.EndsWith("%")) 312 | // tb.CaretIndex = tb.Text.Length - 2; 313 | //else 314 | // tb.CaretIndex = tb.Text.Length; 315 | } 316 | 317 | private void TextBox_PreviewMouseDown(object sender, MouseButtonEventArgs e) 318 | { 319 | // Prevent changing the caret index 320 | e.Handled = true; 321 | (sender as TextBox).Focus(); 322 | } 323 | 324 | private void TextBox_PreviewMouseUp(object sender, MouseButtonEventArgs e) 325 | { 326 | // Prevent changing the caret index 327 | e.Handled = true; 328 | (sender as TextBox).Focus(); 329 | } 330 | 331 | /// 332 | /// Action when is key pressed 333 | /// 334 | private void TextBox_PreviewKeyDown(object sender, KeyEventArgs e) 335 | { 336 | if (IsReadOnly) 337 | { 338 | e.Handled = true; 339 | return; 340 | } 341 | 342 | if (KeyValidator.IsNumericKey(e.Key)) 343 | { 344 | e.Handled = true; 345 | 346 | AddUndoInList(Number); 347 | InsertKey(e.Key); 348 | } 349 | else if (KeyValidator.IsBackspaceKey(e.Key)) 350 | { 351 | e.Handled = true; 352 | 353 | AddUndoInList(Number); 354 | RemoveRightMostDigit(); 355 | } 356 | else if (KeyValidator.IsUpKey(e.Key)) 357 | { 358 | e.Handled = true; 359 | 360 | AddUndoInList(Number); 361 | AddOneDigit(); 362 | 363 | //if the key is repeated add more digit 364 | if (e.IsRepeat) 365 | { 366 | AddUndoInList(Number); 367 | AddOneDigit(UpDownRepeat); 368 | } 369 | } 370 | else if (KeyValidator.IsDownKey(e.Key)) 371 | { 372 | e.Handled = true; 373 | 374 | AddUndoInList(Number); 375 | SubstractOneDigit(); 376 | 377 | //if the key is repeated substract more digit 378 | if (e.IsRepeat) 379 | { 380 | AddUndoInList(Number); 381 | SubstractOneDigit(UpDownRepeat); 382 | } 383 | } 384 | else if (KeyValidator.IsCtrlZKey(e.Key)) 385 | { 386 | e.Handled = true; 387 | 388 | Undo(); 389 | } 390 | else if (KeyValidator.IsCtrlYKey(e.Key)) 391 | { 392 | e.Handled = true; 393 | 394 | Redo(); 395 | } 396 | else if (KeyValidator.IsEnterKey(e.Key)) 397 | { 398 | if (!IsCalculPanelMode) 399 | { 400 | AddUndoInList(Number); 401 | ShowAddPopup(); 402 | } 403 | else 404 | { 405 | ((Popup)((Grid)Parent).Parent).IsOpen = false; 406 | 407 | if (PopupClosed != null) 408 | { 409 | e.Handled = true; 410 | PopupClosed(this, new EventArgs()); 411 | } 412 | } 413 | } 414 | else if (KeyValidator.IsDeleteKey(e.Key)) 415 | { 416 | e.Handled = true; 417 | 418 | AddUndoInList(Number); 419 | Clear(); 420 | } 421 | else if (KeyValidator.IsSubstractKey(e.Key)) 422 | { 423 | e.Handled = true; 424 | 425 | AddUndoInList(Number); 426 | InvertValue(); 427 | } 428 | else if (KeyValidator.IsIgnoredKey(e.Key)) 429 | { 430 | e.Handled = false; 431 | } 432 | else if (KeyValidator.IsCtrlCKey(e.Key)) 433 | { 434 | e.Handled = true; 435 | 436 | CopyToClipBoard(); 437 | } 438 | else if (KeyValidator.IsCtrlVKey(e.Key)) 439 | { 440 | e.Handled = true; 441 | 442 | AddUndoInList(Number); 443 | PasteFromClipBoard(); 444 | } 445 | else 446 | { 447 | e.Handled = true; 448 | } 449 | } 450 | 451 | // cancel copy and paste 452 | private void CopyPasteEventHandler(object sender, DataObjectEventArgs e) => e.CancelCommand(); 453 | 454 | #endregion 455 | 456 | #region Private Methods 457 | private void Ctb_NumberChanged(object sender, EventArgs e) 458 | { 459 | var ctb = sender as CurrencyTextBox; 460 | 461 | Number = _numberBeforePopup + ctb.Number; 462 | 463 | _popupLabel.Content = ctb.Number >= 0 ? "+" : "-"; 464 | } 465 | 466 | /// 467 | /// Insert number from key 468 | /// 469 | private void InsertKey(Key key) 470 | { 471 | //Max length fix 472 | if (MaxLength != 0 && Number.ToString(Culture).Length > MaxLength) 473 | return; 474 | 475 | try 476 | { 477 | // Push the new number from the right 478 | if (KeyValidator.IsNumericKey(key)) 479 | Number = Number < 0 480 | ? Number * 10M - GetDigitFromKey(key) / GetDivider() 481 | : Number * 10M + GetDigitFromKey(key) / GetDivider(); 482 | } 483 | catch (OverflowException) 484 | { 485 | Number = Number < 0 ? decimal.MinValue : decimal.MaxValue; 486 | } 487 | 488 | } 489 | 490 | /// 491 | /// Get the digit from key 492 | /// 493 | private static decimal GetDigitFromKey(Key key) 494 | { 495 | switch (key) 496 | { 497 | case Key.D0: 498 | case Key.NumPad0: return 0M; 499 | case Key.D1: 500 | case Key.NumPad1: return 1M; 501 | case Key.D2: 502 | case Key.NumPad2: return 2M; 503 | case Key.D3: 504 | case Key.NumPad3: return 3M; 505 | case Key.D4: 506 | case Key.NumPad4: return 4M; 507 | case Key.D5: 508 | case Key.NumPad5: return 5M; 509 | case Key.D6: 510 | case Key.NumPad6: return 6M; 511 | case Key.D7: 512 | case Key.NumPad7: return 7M; 513 | case Key.D8: 514 | case Key.NumPad8: return 8M; 515 | case Key.D9: 516 | case Key.NumPad9: return 9M; 517 | default: throw new ArgumentOutOfRangeException($"Invalid key: {key}"); 518 | } 519 | } 520 | 521 | /// 522 | /// Get a decimal for adjust digit when a key was inserted 523 | /// 524 | private decimal GetDivider() 525 | { 526 | switch (GetBindingExpression(TextProperty).ParentBinding.StringFormat) 527 | { 528 | case "N0": 529 | case "C0": return 1M; 530 | case "N": 531 | case "C": return 100M; 532 | case "N1": 533 | case "C1": return 10M; 534 | case "N2": 535 | case "C2": return 100M; 536 | case "N3": 537 | case "C3": return 1000M; 538 | case "N4": 539 | case "C4": return 10000M; 540 | case "N5": 541 | case "C5": return 100000M; 542 | case "N6": 543 | case "C6": return 1000000M; 544 | case "P0": return 100M; 545 | case "P": return 10000M; 546 | case "P1": return 1000M; 547 | case "P2": return 10000M; 548 | case "P3": return 100000M; 549 | case "P4": return 1000000M; 550 | case "P5": return 10000000M; 551 | case "P6": return 100000000M; 552 | default: return 1M; 553 | } 554 | } 555 | 556 | /// 557 | /// Get the number of digit . 558 | /// 559 | private int GetDigitCount() 560 | { 561 | if (string.IsNullOrEmpty(StringFormat)) return 1; 562 | if (string.Equals("N", StringFormat, StringComparison.OrdinalIgnoreCase)) return 2; 563 | if (string.Equals("C", StringFormat, StringComparison.OrdinalIgnoreCase)) return 2; 564 | 565 | var s = StringFormat.Substring(StringFormat.Length - 1, 1); 566 | int resp; 567 | if (int.TryParse(s, NumberStyles.Integer, Culture, out resp)) return resp; 568 | 569 | return 1; 570 | } 571 | 572 | /// 573 | /// Delete the right digit of number property 574 | /// 575 | private void RemoveRightMostDigit() 576 | { 577 | try 578 | { 579 | bool isNegative = Number < 0; 580 | var digitCount = GetDigitCount(); 581 | string decimalSeparator = !string.IsNullOrEmpty(StringFormat) && StringFormat.StartsWith("C", StringComparison.OrdinalIgnoreCase) 582 | ? Culture.NumberFormat.CurrencyDecimalSeparator : Culture.NumberFormat.NumberDecimalSeparator; 583 | 584 | string numberString = Math.Abs(Number).ToString("#.###########", Culture); 585 | numberString = numberString.Substring(0, numberString.Length - 1); 586 | numberString = numberString.Replace(decimalSeparator, string.Empty); 587 | numberString = numberString.PadLeft(digitCount + 1, '0'); 588 | 589 | numberString = (isNegative ? Culture.NumberFormat.NegativeSign : string.Empty) + 590 | numberString.Substring(0, numberString.Length - digitCount) + 591 | Culture.NumberFormat.NumberDecimalSeparator + 592 | numberString.Substring(numberString.Length - digitCount); 593 | 594 | Number = Convert.ToDecimal(numberString, Culture); 595 | } 596 | catch 597 | { 598 | Clear(); 599 | } 600 | } 601 | #endregion Privates methodes 602 | 603 | #region Undo/Redo 604 | 605 | /// 606 | /// Add undo to the list 607 | /// 608 | private void AddUndoInList(decimal number, bool clearRedo = true) 609 | { 610 | //Clear first item when undolimit is reach 611 | if (_undoList.Count == UndoLimit) 612 | _undoList.RemoveRange(0, 1); 613 | 614 | //Add item to undo list 615 | _undoList.Add(number); 616 | 617 | //Clear redo when needed 618 | if (clearRedo) 619 | _redoList.Clear(); 620 | } 621 | 622 | /// 623 | /// Undo the to the previous value 624 | /// 625 | public new void Undo() 626 | { 627 | if (!CanUndo()) return; 628 | 629 | Number = _undoList[_undoList.Count - 1]; 630 | 631 | _redoList.Add(_undoList[_undoList.Count - 1]); 632 | _undoList.RemoveAt(_undoList.Count - 1); 633 | } 634 | 635 | /// 636 | /// Redo to the value previously undone. The list is clear when key is handled 637 | /// 638 | public new void Redo() 639 | { 640 | if (_redoList.Count <= 0) return; 641 | 642 | AddUndoInList(Number, false); 643 | Number = _redoList[_redoList.Count - 1]; 644 | _redoList.RemoveAt(_redoList.Count - 1); 645 | } 646 | 647 | /// 648 | /// Get or set for indicate if control CanUndo 649 | /// 650 | public new bool IsUndoEnabled { get; set; } = true; 651 | 652 | /// 653 | /// Clear the undo list 654 | /// 655 | public void ClearUndoList() => _undoList.Clear(); 656 | 657 | /// 658 | /// Check if the control can undone to a previous value 659 | /// 660 | /// 661 | public new bool CanUndo() => IsUndoEnabled && _undoList.Count > 0; 662 | 663 | #endregion Undo/Redo 664 | 665 | #region Public Methods 666 | 667 | /// 668 | /// Reset the number to zero. 669 | /// 670 | public new void Clear() => Number = 0M; 671 | 672 | /// 673 | /// Set number to positive 674 | /// 675 | public void SetPositive() { if (Number < 0) Number *= -1; } 676 | 677 | /// 678 | /// Set number to negative 679 | /// 680 | public void SetNegative() { if (Number > 0) Number *= -1; } 681 | 682 | /// 683 | /// Alternate value to Negative-Positive and Positive-Negative 684 | /// 685 | public void InvertValue() => Number *= -1; 686 | 687 | /// 688 | /// Add one digit to the property number 689 | /// 690 | /// Repeat add 691 | public void AddOneDigit(int repeat = 1) 692 | { 693 | for (var i = 0; i < repeat; i++) 694 | switch (GetBindingExpression(TextProperty).ParentBinding.StringFormat) 695 | { 696 | case "P0": 697 | Number = decimal.Add(Number, 0.01M); 698 | break; 699 | case "N0": 700 | case "C0": 701 | Number = decimal.Add(Number, 1M); 702 | break; 703 | case "P": 704 | Number = decimal.Add(Number, 0.0001M); 705 | break; 706 | case "N": 707 | case "C": 708 | Number = decimal.Add(Number, 0.01M); 709 | break; 710 | case "P1": 711 | Number = decimal.Add(Number, 0.001M); 712 | break; 713 | case "N1": 714 | case "C1": 715 | Number = decimal.Add(Number, 0.1M); 716 | break; 717 | case "P2": 718 | Number = decimal.Add(Number, 0.0001M); 719 | break; 720 | case "N2": 721 | case "C2": 722 | Number = decimal.Add(Number, 0.01M); 723 | break; 724 | case "P3": 725 | Number = decimal.Add(Number, 0.00001M); 726 | break; 727 | case "N3": 728 | case "C3": 729 | Number = decimal.Add(Number, 0.001M); 730 | break; 731 | case "P4": 732 | Number = decimal.Add(Number, 0.000001M); 733 | break; 734 | case "N4": 735 | case "C4": 736 | Number = decimal.Add(Number, 0.0001M); 737 | break; 738 | case "P5": 739 | Number = decimal.Add(Number, 0.0000001M); 740 | break; 741 | case "N5": 742 | case "C5": 743 | Number = decimal.Add(Number, 0.00001M); 744 | break; 745 | case "P6": 746 | Number = decimal.Add(Number, 0.00000001M); 747 | break; 748 | case "N6": 749 | case "C6": 750 | Number = decimal.Add(Number, 0.000001M); 751 | break; 752 | } 753 | } 754 | 755 | /// 756 | /// Substract one digit to the property number 757 | /// 758 | /// Repeat substract 759 | public void SubstractOneDigit(int repeat = 1) 760 | { 761 | for (var i = 0; i < repeat; i++) 762 | switch (GetBindingExpression(TextProperty).ParentBinding.StringFormat) 763 | { 764 | case "P0": 765 | Number = decimal.Subtract(Number, 0.01M); 766 | break; 767 | case "N0": 768 | case "C0": 769 | Number = decimal.Subtract(Number, 1M); 770 | break; 771 | case "P": 772 | Number = decimal.Subtract(Number, 0.0001M); 773 | break; 774 | case "N": 775 | case "C": 776 | Number = decimal.Subtract(Number, 0.01M); 777 | break; 778 | case "P1": 779 | Number = decimal.Subtract(Number, 0.001M); 780 | break; 781 | case "N1": 782 | case "C1": 783 | Number = decimal.Subtract(Number, 0.1M); 784 | break; 785 | case "P2": 786 | Number = decimal.Subtract(Number, 0.0001M); 787 | break; 788 | case "N2": 789 | case "C2": 790 | Number = decimal.Subtract(Number, 0.01M); 791 | break; 792 | case "P3": 793 | Number = decimal.Subtract(Number, 0.00001M); 794 | break; 795 | case "N3": 796 | case "C3": 797 | Number = decimal.Subtract(Number, 0.001M); 798 | break; 799 | case "P4": 800 | Number = decimal.Subtract(Number, 0.000001M); 801 | break; 802 | case "N4": 803 | case "C4": 804 | Number = decimal.Subtract(Number, 0.0001M); 805 | break; 806 | case "P5": 807 | Number = decimal.Subtract(Number, 0.0000001M); 808 | break; 809 | case "N5": 810 | case "C5": 811 | Number = decimal.Subtract(Number, 0.00001M); 812 | break; 813 | case "P6": 814 | Number = decimal.Subtract(Number, 0.00000001M); 815 | break; 816 | case "N6": 817 | case "C6": 818 | Number = decimal.Subtract(Number, 0.000001M); 819 | break; 820 | } 821 | } 822 | 823 | #endregion Other function 824 | 825 | #region Clipboard 826 | /// 827 | /// Paste if is a number on clipboard 828 | /// 829 | private void PasteFromClipBoard() 830 | { 831 | try 832 | { 833 | switch (GetBindingExpression(TextProperty).ParentBinding.StringFormat) 834 | { 835 | case "P0": 836 | case "P": 837 | case "P1": 838 | case "P2": 839 | case "P3": 840 | case "P4": 841 | case "P5": 842 | case "P6": 843 | Number = decimal.Parse(Clipboard.GetText()); 844 | break; 845 | default: 846 | Number = Math.Round(decimal.Parse(Clipboard.GetText()), GetDigitCount()); 847 | break; 848 | } 849 | } 850 | catch 851 | { 852 | // ignored 853 | } 854 | } 855 | 856 | /// 857 | /// Copy the property Number to Control 858 | /// 859 | private void CopyToClipBoard() 860 | { 861 | Clipboard.Clear(); 862 | Clipboard.SetText(Number.ToString(Culture)); 863 | } 864 | #endregion Clipboard 865 | 866 | #region Add/remove value Popup 867 | /// 868 | /// Show popup for add/remove value 869 | /// 870 | private void ShowAddPopup() 871 | { 872 | if (!CanShowAddPanel) return; 873 | 874 | //Initialize somes Child object 875 | var grid = new Grid { Background = Brushes.White }; 876 | 877 | var ctbPopup = new CurrencyTextBox 878 | { 879 | CanShowAddPanel = false, 880 | IsCalculPanelMode = true, 881 | StringFormat = StringFormat, 882 | Background = Brushes.WhiteSmoke 883 | }; 884 | 885 | _popup = new Popup 886 | { 887 | Width = ActualWidth, 888 | Height = 32, 889 | PopupAnimation = PopupAnimation.Fade, 890 | Placement = PlacementMode.Bottom, 891 | PlacementTarget = this, 892 | StaysOpen = false, 893 | Child = grid, 894 | IsOpen = true 895 | }; 896 | 897 | //Set object properties 898 | ctbPopup.NumberChanged += Ctb_NumberChanged; 899 | ctbPopup.PopupClosed += CtbPopup_PopupClosed; 900 | 901 | _numberBeforePopup = Number; 902 | _popupLabel = new Label { Content = "+" }; 903 | 904 | //ColumnDefinition 905 | var c1 = new ColumnDefinition { Width = new GridLength(20, GridUnitType.Auto) }; 906 | var c2 = new ColumnDefinition { Width = new GridLength(80, GridUnitType.Star) }; 907 | grid.ColumnDefinitions.Add(c1); 908 | grid.ColumnDefinitions.Add(c2); 909 | Grid.SetColumn(_popupLabel, 0); 910 | Grid.SetColumn(ctbPopup, 1); 911 | 912 | //Add object 913 | grid.Children.Add(_popupLabel); 914 | grid.Children.Add(ctbPopup); 915 | 916 | ctbPopup.Focus(); 917 | } 918 | 919 | private void CtbPopup_PopupClosed(object sender, EventArgs e) => Focus(); 920 | #endregion Add/remove value Popup 921 | } 922 | } -------------------------------------------------------------------------------- /src/CurrencyTextBoxControl/CurrencyTextBox.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netcoreapp3.0;net40;net45;net47 5 | true 6 | CurrencyTextbox 7 | CurrencyTextbox 8 | CurrencyTextbox user control 9 | true 10 | Derek Tremblay (derektremblay666@gmail.com) 11 | Derek Tremblay (derektremblay666@gmail.com) 12 | 13 | .NET FRAMEWORK 4.5/4.7 AND .NET CORE 3.0 14 | A WPF TextBox for entering a Currency/Numeric/Percent value, similar to how a cash register works. 15 | Numbers typed are pushed in from the right. 16 | 17 | 18 | CurrencyTextbox 19 | MIT 20 | 2.0.2.0 21 | 2.0.2.0 22 | 2.0.2 23 | true 24 | wpf-controls;currency;user-interface 25 | 26 | preview 27 | https://github.com/abbaye/WpfCurrencyTextbox 28 | false 29 | 30 | license.txt 31 | 32 | 33 | 34 | TRACE;DEBUG 35 | x64 36 | 37 | 38 | 39 | 40 | True 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /src/CurrencyTextBoxControl/KeyValidator.cs: -------------------------------------------------------------------------------- 1 | using System.Windows.Input; 2 | 3 | namespace CurrencyTextBoxControl 4 | { 5 | public static class KeyValidator 6 | { 7 | /// 8 | /// Check if is a numeric key as pressed 9 | /// 10 | public static bool IsNumericKey(Key key) => key == Key.D0 || key == Key.D1 || key == Key.D2 || key == Key.D3 || key == Key.D4 || key == Key.D5 || key == Key.D6 || key == Key.D7 || key == Key.D8 || key == Key.D9 || 11 | key == Key.NumPad0 || key == Key.NumPad1 || key == Key.NumPad2 || key == Key.NumPad3 || key == Key.NumPad4 || key == Key.NumPad5 || key == Key.NumPad6 || key == Key.NumPad7 || key == Key.NumPad8 || key == Key.NumPad9; 12 | 13 | public static bool IsBackspaceKey(Key key) => key == Key.Back; 14 | 15 | public static bool IsSubstractKey(Key key) => key == Key.Subtract || key == Key.OemMinus; 16 | 17 | public static bool IsDeleteKey(Key key) => key == Key.Delete; 18 | 19 | public static bool IsIgnoredKey(Key key) => key == Key.Tab; 20 | 21 | public static bool IsUpKey(Key key) => key == Key.Up; 22 | 23 | public static bool IsDownKey(Key key) => key == Key.Down; 24 | 25 | public static bool IsEnterKey(Key key) => key == Key.Enter; 26 | 27 | public static bool IsCtrlCKey(Key key) => key == Key.C && Keyboard.Modifiers == ModifierKeys.Control; 28 | 29 | public static bool IsCtrlZKey(Key key) => key == Key.Z && Keyboard.Modifiers == ModifierKeys.Control; 30 | 31 | public static bool IsCtrlYKey(Key key) => key == Key.Y && Keyboard.Modifiers == ModifierKeys.Control; 32 | 33 | public static bool IsCtrlVKey(Key key) => key == Key.V && Keyboard.Modifiers == ModifierKeys.Control; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/CurrencyTextBoxControl/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | using System.Windows; 3 | 4 | 5 | // Setting ComVisible to false makes the types in this assembly not visible 6 | // to COM components. If you need to access a type in this assembly from 7 | // COM, set the ComVisible attribute to true on that type. 8 | [assembly: ComVisible(false)] 9 | 10 | //In order to begin building localizable applications, set 11 | //CultureYouAreCodingWith in your .csproj file 12 | //inside a . For example, if you are using US english 13 | //in your source files, set the to en-US. Then uncomment 14 | //the NeutralResourceLanguage attribute below. Update the "en-US" in 15 | //the line below to match the UICulture setting in the project file. 16 | 17 | //[assembly: NeutralResourcesLanguage("en-US", UltimateResourceFallbackLocation.Satellite)] 18 | 19 | 20 | [assembly: ThemeInfo( 21 | ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located 22 | //(used if a resource is not found in the page, 23 | // or application resource dictionaries) 24 | ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located 25 | //(used if a resource is not found in the page, 26 | // app, or any theme specific resource dictionaries) 27 | )] 28 | 29 | -------------------------------------------------------------------------------- /src/CurrencyTextBoxControl/Properties/Resources.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // Ce code a été généré par un outil. 4 | // Version du runtime :4.0.30319.42000 5 | // 6 | // Les modifications apportées à ce fichier peuvent provoquer un comportement incorrect et seront perdues si 7 | // le code est régénéré. 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace CurrencyTextBoxControl.Properties { 12 | using System; 13 | 14 | 15 | /// 16 | /// Une classe de ressource fortement typée destinée, entre autres, à la consultation des chaînes localisées. 17 | /// 18 | // Cette classe a été générée automatiquement par la classe StronglyTypedResourceBuilder 19 | // à l'aide d'un outil, tel que ResGen ou Visual Studio. 20 | // Pour ajouter ou supprimer un membre, modifiez votre fichier .ResX, puis réexécutez ResGen 21 | // avec l'option /str ou régénérez votre projet VS. 22 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "15.0.0.0")] 23 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 24 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 25 | internal class Resources { 26 | 27 | private static global::System.Resources.ResourceManager resourceMan; 28 | 29 | private static global::System.Globalization.CultureInfo resourceCulture; 30 | 31 | [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] 32 | internal Resources() { 33 | } 34 | 35 | /// 36 | /// Retourne l'instance ResourceManager mise en cache utilisée par cette classe. 37 | /// 38 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 39 | internal static global::System.Resources.ResourceManager ResourceManager { 40 | get { 41 | if (object.ReferenceEquals(resourceMan, null)) { 42 | global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("CurrencyTextBoxControl.Properties.Resources", typeof(Resources).Assembly); 43 | resourceMan = temp; 44 | } 45 | return resourceMan; 46 | } 47 | } 48 | 49 | /// 50 | /// Remplace la propriété CurrentUICulture du thread actuel pour toutes 51 | /// les recherches de ressources à l'aide de cette classe de ressource fortement typée. 52 | /// 53 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 54 | internal static global::System.Globalization.CultureInfo Culture { 55 | get { 56 | return resourceCulture; 57 | } 58 | set { 59 | resourceCulture = value; 60 | } 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/CurrencyTextBoxControl/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 | -------------------------------------------------------------------------------- /src/CurrencyTextBoxControl/Properties/Settings.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // Ce code a été généré par un outil. 4 | // Version du runtime :4.0.30319.42000 5 | // 6 | // Les modifications apportées à ce fichier peuvent provoquer un comportement incorrect et seront perdues si 7 | // le code est régénéré. 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace CurrencyTextBoxControl.Properties { 12 | 13 | 14 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 15 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "15.3.0.0")] 16 | internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase { 17 | 18 | private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); 19 | 20 | public static Settings Default { 21 | get { 22 | return defaultInstance; 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/CurrencyTextBoxControl/Properties/Settings.settings: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/CurrencyTextBoxControl/Themes/Generic.xaml: -------------------------------------------------------------------------------- 1 |  4 | 5 | 6 | 15 | -------------------------------------------------------------------------------- /src/CurrencyTextBoxControl/license.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Adam Anderson, Derek Tremblay 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. -------------------------------------------------------------------------------- /src/CurrencyTextBoxExample/App.xaml: -------------------------------------------------------------------------------- 1 |  5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/CurrencyTextBoxExample/App.xaml.cs: -------------------------------------------------------------------------------- 1 | namespace CurrencyTextBoxExample 2 | { 3 | /// 4 | /// Interaction logic for App.xaml 5 | /// 6 | public partial class App 7 | { 8 | 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/CurrencyTextBoxExample/CurrencyTextBox.Sample.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | WinExe 5 | netcoreapp3.0;net40;net45;net47 6 | true 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/CurrencyTextBoxExample/MainWindow.xaml: -------------------------------------------------------------------------------- 1 |  11 | 12 | 13 | 18 | 19 | 29 | 30 | 37 | 38 | 39 | 40 | 46 | 56 | 57 | 63 | 74 | 75 | 76 | 77 |