Пользовательский WPF TextBox с подключенным модулем TextBlock

Мое приложение имеет тонны TextBox контролирует, где пользователь может вводить числовые значения. Большинство из этих значений находятся в какой-то физической единице. Этот индикатор блока отображается справа от TextBox контроль.

Это выглядит как следующий эскиз: [________] км (где единица "км")

В настоящее время я сделал это с StackPanel экземпляры везде. Это всегда один и тот же шаблон. Это делает XAML менее читабельным, чем должно быть.

Я ищу TextBox контроль, который уже включает в себя TextBlock на его стороне, чтобы отобразить блок.

Моей первой попыткой был класс, полученный из TextBox, с файлом XAML, который заменяет Template свойство как это:

<TextBox
    x:Class="WpfApplication1.UnitTextBox"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    x:Name="_this"
    KeyboardNavigation.IsTabStop="False"
    Style="{StaticResource {x:Type TextBox}}">
    <TextBox.Template>
        <ControlTemplate>
            <Grid>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="*"/>
                    <ColumnDefinition Width="Auto"/>
                </Grid.ColumnDefinitions>
                <TextBox
                    Foreground="{TemplateBinding Foreground}"
                    IsEnabled="{TemplateBinding IsEnabled}"
                    IsReadOnly="{Binding IsReadOnly, ElementName=_this}"
                    Style="{TemplateBinding Style}"
                    Text="{Binding Text, ElementName=_this}"
                    Width="{TemplateBinding Width}"
                    ... lots more ...
                    VerticalAlignment="Center"/>
                <TextBlock
                    Grid.Column="1"
                    Text="{Binding Unit, ElementName=_this}"
                    Margin="4,0,0,0"
                    VerticalAlignment="Center"/>
            </Grid>
        </ControlTemplate>
    </TextBox.Template>
</TextBox>

Unit это свойство зависимости в моем UnitTextBox класс с выделенным кодом:

public partial class UnitTextBox : TextBox
{
    public static DependencyProperty UnitProperty = DependencyProperty.Register(
        name: "Unit",
        propertyType: typeof(string),
        ownerType: typeof(UnitTextBox));

    public string Unit
    {
        get { return (string) GetValue(UnitProperty); }
        set { SetValue(UnitProperty, value); }
    }

    public UnitTextBox()
    {
        InitializeComponent();
    }
}

К сожалению, у этого подхода есть ряд проблем. Мне нужно пройти через практически все свойства к внутренней TextBox как вы можете видеть (я сократил это здесь). Кроме того, я хотел бы Width свойство применять к внутренней TextBox как обычно, не наружу Grid, Думаю, мне нужно отдельное свойство для этого и связать внутреннее TextBox пример тому. И в настоящее время стиль, который я установил при использовании UnitTextBox класс игнорируется. Я даже не знаю, как это решить.

Есть ли возможность создать такой комбинированный элемент управления с WPF? Это должно действовать как TextBox со всеми его обработчиками событий, связываемыми свойствами и т. д., но уже включают в себя эту единичную строку в своем виде, присваиваемую дополнительным свойством.

Могу ли я вместо этого использовать кастом Style это добавляет TextBlock где-то вокруг (но я думаю, что мне нужно внешнее Grid для выравнивания вещей), и объявить блок с прикрепленным свойством?

1 ответ

Решение

Раздражающим расширением шаблонного элемента управления является то, что вам обычно нужно определить новый шаблон для каждой системной темы или для вашей TextBox будет выглядеть неуместно рядом с обычным TextBox, Однако, поскольку ваше "улучшение" довольно простое, мы можем полностью избежать этого, просто переопределив макет и код рендеринга, чтобы включить текст модуля:

public class UnitTextBox : TextBox
{
    private FormattedText _unitText;
    private Rect _unitTextBounds;

    public static DependencyProperty UnitTextProperty =
        DependencyProperty.Register(
            name: "UnitText",
            propertyType: typeof(string),
            ownerType: typeof(UnitTextBox),
            typeMetadata: new FrameworkPropertyMetadata(
                default(string),
                FrameworkPropertyMetadataOptions.AffectsMeasure |
                FrameworkPropertyMetadataOptions.AffectsArrange |
                FrameworkPropertyMetadataOptions.AffectsRender));

    public string UnitText
    {
        get { return (string)GetValue(UnitTextProperty); }
        set { SetValue(UnitTextProperty, value); }
    }

    public static DependencyProperty UnitPaddingProperty =
        DependencyProperty.Register(
            name: "UnitPadding",
            propertyType: typeof(Thickness),
            ownerType: typeof(UnitTextBox),
            typeMetadata: new FrameworkPropertyMetadata(
                new Thickness(5d, 0d, 0d, 0d),
                FrameworkPropertyMetadataOptions.AffectsMeasure |
                FrameworkPropertyMetadataOptions.AffectsArrange |
                FrameworkPropertyMetadataOptions.AffectsRender));

    public Thickness UnitPadding
    {
        get { return (Thickness)GetValue(UnitPaddingProperty); }
        set { SetValue(UnitPaddingProperty, value); }
    }

    public static DependencyProperty TextBoxWidthProperty =
        DependencyProperty.Register(
            name: "TextBoxWidth",
            propertyType: typeof(double),
            ownerType: typeof(UnitTextBox),
            typeMetadata: new FrameworkPropertyMetadata(
                double.NaN,
                FrameworkPropertyMetadataOptions.AffectsMeasure));

    public double TextBoxWidth
    {
        get { return (double)GetValue(TextBoxWidthProperty); }
        set { SetValue(TextBoxWidthProperty, value); }
    }

    protected override void OnPropertyChanged(DependencyPropertyChangedEventArgs e)
    {
        base.OnPropertyChanged(e);

        if (e.Property == ForegroundProperty)
            EnsureUnitText(invalidate: true);
    }

    protected override Size MeasureOverride(Size constraint)
    {
        var textBoxWidth = this.TextBoxWidth;
        var unit = EnsureUnitText(invalidate: true);
        var padding = this.UnitPadding;

        if (unit != null)
        {
            var unitWidth = unit.Width + padding.Left + padding.Right;
            var unitHeight = unit.Height + padding.Top + padding.Bottom;

            constraint = new Size(
                constraint.Width - unitWidth,
                Math.Max(constraint.Height, unitHeight));
        }

        var hasFixedTextBoxWidth = !double.IsNaN(textBoxWidth) &&
                                   !double.IsInfinity(textBoxWidth);

        if (hasFixedTextBoxWidth)
            constraint = new Size(textBoxWidth, constraint.Height);

        var baseSize = base.MeasureOverride(constraint);
        var baseWidth = hasFixedTextBoxWidth ? textBoxWidth : baseSize.Width;

        if (unit != null)
        {
            var unitWidth = unit.Width + padding.Left + padding.Right;
            var unitHeight = unit.Height + padding.Top + padding.Bottom;

            return new Size(
                baseWidth + unitWidth,
                Math.Max(baseSize.Height, unitHeight));
        }

        return new Size(baseWidth, baseSize.Height);
    }

    protected override Size ArrangeOverride(Size arrangeBounds)
    {
        var textSize = arrangeBounds;
        var unit = EnsureUnitText(invalidate: false);
        var padding = this.UnitPadding;

        if (unit != null)
        {
            var unitWidth = unit.Width + padding.Left + padding.Right;
            var unitHeight = unit.Height + padding.Top + padding.Bottom;

            textSize.Width -= unitWidth;

            _unitTextBounds = new Rect(
                textSize.Width + padding.Left,
                (arrangeBounds.Height - unitHeight) / 2 + padding.Top,
                textSize.Width,
                textSize.Height);
        }

        var baseSize = base.ArrangeOverride(textSize);

        if (unit != null)
        {
            var unitWidth = unit.Width + padding.Left + padding.Right;
            var unitHeight = unit.Height + padding.Top + padding.Bottom;

            return new Size(
                baseSize.Width + unitWidth,
                Math.Max(baseSize.Height, unitHeight));
        }

        return baseSize;
    }

    protected override void OnRender(DrawingContext drawingContext)
    {
        base.OnRender(drawingContext);

        var unitText = EnsureUnitText(invalidate: false);
        if (unitText != null)
            drawingContext.DrawText(unitText, _unitTextBounds.Location);
    }

    private FormattedText EnsureUnitText(bool invalidate = false)
    {
        if (invalidate)
            _unitText = null;

        if (_unitText != null)
            return _unitText;

        var unit = this.UnitText;

        if (!string.IsNullOrEmpty(unit))
        {
            _unitText = new FormattedText(
                unit,
                CultureInfo.InvariantCulture,
                this.FlowDirection,
                new Typeface(
                    this.FontFamily,
                    this.FontStyle,
                    this.FontWeight,
                    this.FontStretch),
                this.FontSize,
                this.Foreground);
        }

        return _unitText;
    }
}

TextBoxWidth свойство позволяет установить фиксированную ширину только для TextBox, Поведение Width остается неизменным, как и должно быть: он определяет размер всего элемента управления, например, TextBox и текст блока.

Никакой пользовательский стиль или шаблон не требуется.

Другие вопросы по тегам