Make WPF TextBox Margins Clickable/Selectable

Viewed 375

I have a WPF TextBox with a white background. I've given it some Padding so that the text has a "margin" around it (akin to the margins in, say, MS Word).

<TextBox x:Name="MyTextBox" Padding="25" />

Unlike in Word, however, this blank area around the text is not "active" (see color-coded illustration below). Clicking within it (the red section) doesn't move the textbox's caret, nor will it begin a selection. Note, however, that the entire green area is active - even the blank space below the text (meaning you can click within it to move the caret or start a selection)...

textbox with colored alive/dead zones

So, my question is this: Is there any way to add space around aTextBox that is blank but is also part of the editable area (so that it will react to clicks/selections, like the blank area of the green section does)?

Thanks!

p.s. The closest I've come in my search for an answer was this: How to set the margin on a internal TextBoxView in wpf... However, while increasing the Margin of the inner TextBoxView does increase the space around the text, that space still doesn't seem to be "active" in any way... And it seems like the TextBoxView doesn't have a Padding property, so I can't try that (though I imagine Padding is more likely to be the solution than a Margin)...

1 Answers

I'm really confused why it's not a popular question. The TextBox has a counterintuitive Padding behavior for sure.

Looked at Microsoft referencesourece for TextBox selection code but it turns out too complicated to inject a fix. So Behavior attached property seems to be a simplest way to achieve this.

xaml:

<TextBox Padding="24 0" local:TextBoxSelectionBehavior.IsEnabled="True"/>

c#:

public static class TextBoxSelectionBehavior
{
    public const double MAX_DISTANCE = 10;
    private static TextBox control;
    private static Point pos;
    private static int start;
    private static int cur;
    private static double right;
    private static bool drag;
    private const double MAXDRAG = 12;

    public static readonly DependencyProperty IsEnabledProperty = DependencyProperty.RegisterAttached("IsEnabled", typeof(bool), typeof(TextBoxSelectionBehavior), new UIPropertyMetadata(false, IsEnabledChanged));
    public static bool GetIsEnabled(FrameworkElement obj) => (bool)obj.GetValue(IsEnabledProperty);
    public static void SetIsEnabled(FrameworkElement obj, bool value) => obj.SetValue(IsEnabledProperty, value);
    private static void IsEnabledChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var v = (TextBox)d;
        if ((bool)e.NewValue)
        {
            v.PreviewMouseDown += V_PreviewMouseDown;
            v.PreviewMouseUp += V_PreviewMouseUp;
            v.PreviewMouseMove += V_PreviewMouseMove;
            v.LostFocus += V_LostFocus;
            v.IsKeyboardFocusedChanged += V_IsKeyboardFocusedChanged;
        }
        else
        {
            v.PreviewMouseDown -= V_PreviewMouseDown;
            v.PreviewMouseUp -= V_PreviewMouseUp;
            v.PreviewMouseMove -= V_PreviewMouseMove;
            v.LostFocus -= V_LostFocus;
            v.IsKeyboardFocusedChanged -= V_IsKeyboardFocusedChanged;
        }
    }

    public static readonly DependencyProperty ClickSelectsProperty = DependencyProperty.RegisterAttached("ClickSelects", typeof(bool), typeof(TextBoxSelectionBehavior), new UIPropertyMetadata(true));
    public static bool GetClickSelects(FrameworkElement obj) => (bool)obj.GetValue(ClickSelectsProperty);
    public static void SetClickSelects(FrameworkElement obj, bool value) => obj.SetValue(ClickSelectsProperty, value);

    private static void V_PreviewMouseDown(object sender, System.Windows.Input.MouseButtonEventArgs e)
    {
        if (e.ChangedButton != MouseButton.Left) return;
        if (control != null) { control.ReleaseMouseCapture(); control = null; }
        if (e.OriginalSource is Grid g && (System.Windows.Media.VisualTreeHelper.GetParent(g) as FrameworkElement)?.Name == "PART_ContentHost")
        {
            control = (TextBox)sender;
            pos = e.GetPosition(control);
            var r = control.FindChild<ScrollContentPresenter>().Content as FrameworkElement;
            right = r.TranslatePoint(new Point(r.ActualWidth, 0), control).X;
            cur = start = control.GetCharacterIndexFromPoint(pos, true) + (pos.X >= right ? 1 : 0);
            control.SelectionStart = start;
            control.SelectionLength = 0;
            control.Focus();
            control.CaptureMouse();
            drag = false;
            e.Handled = true;
        }
    }
    private static void V_PreviewMouseMove(object sender, System.Windows.Input.MouseEventArgs e)
    {
        if (control == null) return;
        var p = e.GetPosition(control);
        cur = control.GetCharacterIndexFromPoint(p, true);
        control.SelectionStart = Math.Min(start, cur);
        control.SelectionLength = Math.Abs(cur - start) + (p.X >= right ? pos.X >= right ? -1 : 1 : 0);
        if (!drag) drag = Math.Max(Math.Abs(pos.X - p.X), Math.Abs(pos.Y - p.Y)) > MAXDRAG;
        e.Handled = true;
    }
    private static void V_PreviewMouseUp(object sender, System.Windows.Input.MouseButtonEventArgs e)
    {
        if (control == null) return;
        control.ReleaseMouseCapture();
        if (!drag && GetClickSelects(control)) control.SelectAll();
        e.Handled = true;
        control = null;
    }
    private static void V_LostFocus(object sender, RoutedEventArgs e)
    {
        if (control != null && control != sender) return;
        if (control != null) control.ReleaseMouseCapture();
        control = null;
    }
    private static void V_IsKeyboardFocusedChanged(object sender, DependencyPropertyChangedEventArgs e)
    {
        if (control != null && control != sender) return;
        if ((bool)e.NewValue) return;
        if (control != null) control.ReleaseMouseCapture();
        control = null;
    }
}

And this attached property could be easily assigned globally by a general Style setter for a TextBoxes.

Related