using System; using System.Collections.Generic; using System.Drawing; using System.Linq; using System.Runtime.CompilerServices; using System.Windows; using System.Windows.Controls; using System.Windows.Documents; using System.Windows.Input; using System.Windows.Media; namespace MultiTerm.Wpf.CustomControl; /// /// Follow steps 1a or 1b and then 2 to use this custom control in a XAML file. /// /// Step 1a) Using this custom control in a XAML file that exists in the current project. /// Add this XmlNamespace attribute to the root element of the markup file where it is /// to be used: /// /// xmlns:MyNamespace="clr-namespace:MultiTerm.Wpf.CustomControl" /// /// /// Step 1b) Using this custom control in a XAML file that exists in a different project. /// Add this XmlNamespace attribute to the root element of the markup file where it is /// to be used: /// /// xmlns:MyNamespace="clr-namespace:MultiTerm.Wpf.CustomControl.MultiFormatTextBox;assembly=MultiTerm.Wpf.CustomControl.MultiFormatTextBox" /// /// You will also need to add a project reference from the project where the XAML file lives /// to this project and Rebuild to avoid compilation errors: /// /// Right click on the target project in the Solution Explorer and /// "Add Reference"->"Projects"->[Browse to and select this project] /// /// /// Step 2) /// Go ahead and use your control in the XAML file. /// /// /// /// internal class Format { private readonly string? backgroundColorResourceName; public string Name { get; set; } public Brush BackgroundBrush { get; set; } public Predicate IsKeyValid { get; set; } public Format(string name, Brush backgroundBrush, Predicate keyValidator) { this.Name = name; this.BackgroundBrush = backgroundBrush; this.IsKeyValid = keyValidator; } public Format(string name, string backgroundColorResourceName, Predicate keyValidator) { this.Name = name; this.backgroundColorResourceName = backgroundColorResourceName; this.BackgroundBrush = Brushes.White; // set background brush to white this.IsKeyValid = keyValidator; } public static List GetListOfNames(IEnumerable formats) { return formats.Select(item => item.Name).ToList(); } public static void UpdateBackgroundBrushesFromResources(FrameworkElement fwElement, IEnumerable formats) { if (fwElement == null) throw new ArgumentNullException(nameof(fwElement)); foreach (var format in formats) { // if resource name not set => skip if (format.backgroundColorResourceName == null) continue; // get background brush color from resources try { format.BackgroundBrush = (SolidColorBrush)fwElement.FindResource(format.backgroundColorResourceName); } catch (Exception) { continue; } // ignore } } } public class MultiFormatTextBox : Control { private static readonly Brush defaultBackgroundBrush = Brushes.White; private static readonly List formats = new() { // character input, accepts all keys except space //new Format("CHAR", Brushes.LightSkyBlue, delegate(Key k) { return (k != Key.Space); }), new Format("CHAR", "MultiFormatTextBox.CHAR.Background", delegate(Key k) { return (k != Key.Space); }), // hex input, ignores all keys that are not inbetween 0 and F //new Format("HEX", Brushes.LightGreen, delegate(Key k) { return (k >= Key.D0 && k <= Key.F); }), new Format("HEX", "MultiFormatTextBox.HEX.Background", delegate(Key k) { return (k >= Key.D0 && k <= Key.F); }), // binary input, ignores all keys except 0 and 1 //new Format("BIN", Brushes.LightPink, delegate(Key k) { return (k == Key.D0 || k == Key.D1); }) new Format("BIN", "MultiFormatTextBox.BIN.Background", delegate(Key k) { return (k == Key.D0 || k == Key.D1); }) }; private ComboBox comboBox; private RichTextBox richTextBox; private Format currentlySelectedFormat; private int offsetContentStartToFormatStart; static MultiFormatTextBox() { DefaultStyleKeyProperty.OverrideMetadata(typeof(MultiFormatTextBox), new FrameworkPropertyMetadata(typeof(MultiFormatTextBox))); } public override void OnApplyTemplate() { base.OnApplyTemplate(); // initialize format background brushes Format.UpdateBackgroundBrushesFromResources(this, formats); // set initially selected format this.currentlySelectedFormat = formats!.First(); // get comboBox from template var comboBox = GetTemplateChild("comboBox") as ComboBox; if (comboBox != null) { this.comboBox = comboBox; this.comboBox.ItemsSource = Format.GetListOfNames(formats); this.comboBox.SelectedItem = currentlySelectedFormat.Name; this.comboBox.SelectionChanged += ComboBox_SelectionChanged; } // get richTextBox from template var richTextBox = GetTemplateChild("richTextBox") as RichTextBox; if (richTextBox != null) { this.richTextBox = richTextBox; this.richTextBox.KeyDown += RichTextBox_KeyDown; this.richTextBox.TextChanged += RichTextBox_TextChanged; this.offsetContentStartToFormatStart = 0; } } private void ComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e) { // match all formats with the name selected in the combobox var matchingFormats = formats.Where(format => format.Name == (string)this.comboBox!.SelectedItem); // check if exactly one format was matched if(matchingFormats.Count() != 1) { throw new Exception($"{nameof(ComboBox_SelectionChanged)} could not match a correct amount of formats"); } // set currently selected format, reset offset counter this.currentlySelectedFormat = matchingFormats.First(); // insert space to separate formats this.InsertSeparation(); // set new start position this.StoreCaretPosition(); // focus textbox this.richTextBox.Focus(); } private void InsertSeparation() { // disable event handler this.richTextBox.TextChanged -= RichTextBox_TextChanged; // store caret position before this.StoreCaretPosition(); // insert this.richTextBox.AppendText(" "); // change background RtbChangeTextBackground(this.richTextBox, start: this.richTextBox.Document.ContentStart.GetPositionAtOffset(-this.offsetContentStartToFormatStart), end: this.richTextBox.Document.ContentEnd, color: defaultBackgroundBrush); // store new caret position after this.StoreCaretPosition(); // reenable this.richTextBox.TextChanged += RichTextBox_TextChanged; } private void StoreCaretPosition() { this.offsetContentStartToFormatStart = this.richTextBox.CaretPosition.GetOffsetToPosition(this.richTextBox.Document.ContentStart); } private void RichTextBox_KeyDown(object sender, KeyEventArgs e) { // guard combobox null if (this.comboBox == null) throw new Exception($"{nameof(this.comboBox)} cannot be null"); // if key is invalid for this format => ignore it (handled = true) if(this.currentlySelectedFormat.IsKeyValid(e.Key) == false) { e.Handled = true; } } private void RichTextBox_TextChanged(object sender, TextChangedEventArgs e) { if (e.Changes.Count > 1 || e.Changes.Count == 0) { return; } // if something was added => change background color if (e.Changes.First().AddedLength > 0) { RtbChangeTextBackground(this.richTextBox, start: this.richTextBox.Document.ContentStart.GetPositionAtOffset(-this.offsetContentStartToFormatStart), end: this.richTextBox.Document.ContentEnd, color: this.currentlySelectedFormat.BackgroundBrush); } // if something was removed => update start of content to current location else if(e.Changes.First().RemovedLength > 0) { this.StoreCaretPosition(); } } private static void RtbChangeTextBackground(RichTextBox rtb, TextPointer start, TextPointer end, Brush color) { // Get text selection TextSelection textRange = rtb.Selection; textRange.Select(start, end); // Apply property to the selection: textRange.ApplyPropertyValue(TextElement.BackgroundProperty, color); // deselect rtb.Selection.Select(rtb.Document.ContentEnd, rtb.Document.ContentEnd); } }