Реализовать placeholder в UserControl

296
03 декабря 2017, 10:37

Доброго времени суток, всем. Я так понимаю, в WPF нет контрола, который будет похож на TextBox и иметь свойство placeholder'а. В WinForms я писал такой контрол, и зайдествовал, помню, WinAPI. Я так понял, что в WPF задействовать какое-либо низкое API для отображения PlaceHolder'а в TextBox нет необходимости, поэтому сделал следующее: 1. Создал UserControl:

<UserControl x:Class="PTRCPriceCalculator.InputBox"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:local="clr-namespace:PTRCPriceCalculator"
             mc:Ignorable="d"
             d:DesignHeight="40" d:DesignWidth="300">
    <!-- InputBox template -->
    <UserControl.Template>
        <ControlTemplate TargetType="{x:Type UserControl}">
            <Grid>
                <!-- Inner texbox -->
                <TextBox HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}" 
                         VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}" 
                         Background="{TemplateBinding Background}"
                         Foreground="{TemplateBinding Foreground}"
                         FontFamily="{TemplateBinding FontFamily}"
                         FontSize="{TemplateBinding FontSize}"
                         BorderThickness="0">
                    <!-- Style for the inner textbox -->
                    <TextBox.Style>
                        <Style TargetType="{x:Type TextBoxBase}">
                            <Setter Property="Template">
                                <Setter.Value>
                                    <ControlTemplate TargetType="{x:Type TextBoxBase}">
                                        <Grid>
                                            <!-- Standard textbox role -->
                                            <Border x:Name="border" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}" SnapsToDevicePixels="True">
                                                <ScrollViewer x:Name="PART_ContentHost" Focusable="False" HorizontalScrollBarVisibility="Hidden" VerticalScrollBarVisibility="Hidden"/>
                                            </Border>
                                            <!-- Place holder -->
                                            <TextBlock IsHitTestVisible="false"
                                                       VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
                                                       HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
                                                       FontFamily="{TemplateBinding FontFamily}"
                                                       FontSize="{TemplateBinding FontSize}"
                                                       Text="{Binding PlaceHolder, RelativeSource={RelativeSource FindAncestor, AncestorType=UserControl}}"
                                                       Padding="{TemplateBinding Padding}">
                                                <!-- Place holder style -->
                                                <TextBlock.Style>
                                                    <Style TargetType="{x:Type TextBlock}">
                                                        <Setter Property="Visibility" Value="Collapsed"/>
                                                        <Style.Triggers>
                                                            <DataTrigger Binding="{Binding Text, RelativeSource={RelativeSource TemplatedParent}}" Value="">
                                                                <Setter Property="Visibility" Value="Visible"/>
                                                            </DataTrigger>
                                                        </Style.Triggers>
                                                    </Style>
                                                </TextBlock.Style>
                                            </TextBlock>
                                        </Grid>
                                        <ControlTemplate.Triggers>
                                            <Trigger Property="IsEnabled" Value="False">
                                                <Setter Property="Opacity" TargetName="border" Value="0.56"/>
                                            </Trigger>
                                            <Trigger Property="IsMouseOver" Value="True">
                                                <Setter Property="BorderBrush" TargetName="border" Value="#FF7EB4EA"/>
                                            </Trigger>
                                            <Trigger Property="IsKeyboardFocused" Value="True">
                                                <Setter Property="BorderBrush" TargetName="border" Value="#FF569DE5"/>
                                            </Trigger>
                                        </ControlTemplate.Triggers>
                                    </ControlTemplate>
                                </Setter.Value>
                            </Setter>
                        </Style>
                    </TextBox.Style>
                </TextBox>
            </Grid>
        </ControlTemplate>
    </UserControl.Template>
</UserControl>

В codebehind определил два свойства зависимости:

public static readonly DependencyProperty PlaceHolderProperty = DependencyProperty.Register(
"PlaceHolder",
typeof( string ),
typeof( InputBox ) );
public string PlaceHolder
{
    get { return (string)GetValue( PlaceHolderProperty ); }
    set { SetValue( PlaceHolderProperty, value ); }
}
public static readonly DependencyProperty TextProperty = DependencyProperty.Register(
"Text",
typeof( string ),
typeof( InputBox ) );
public string Text
{
    get { return (string)GetValue( TextProperty ); }
    set { SetValue( TextProperty, value ); }
}

Все здорово, PlaceHolder появляется. Но я не могу понять, как мне делегировать свойство Text к свойству Text моего TextBox'а, который находится внутри Template моего UserControl'а? И еще пару незначительных вопросов: 1. Насколько эффективнее/неэффективнее использовать такой способ создания TextBox'а с placeholder'ом по сравнению с WinAPI? 2. Для отображения PlaceHolder'а, как Вы видите, я определил TextBlock, который отображается/не отображается в зависимости от значения Text в TextBox. Суть в том, что, отображение PlaceHolder'а в TextBlock и реального текста в TextBox не совпадают на несколько пикселей, хотя все проверено мной. Даже Border у TextBox'а убрал, все равно не совпадает. Можно ли это исправить, или лучше забить на это?:)

Answer 1

Мне кажется, вы делаете избыточно сложно.

Вот, как мне кажется, более простой вариант. Передаём TextBlock'у-плейсхолдеру его «хозяина» как Tag, и пишем вот такой стиль:

<Style TargetType="TextBlock" x:Key="PlaceholderStyle"
       xmlns:sys="clr-namespace:System;assembly=mscorlib">
    <Setter Property="IsHitTestVisible" Value="False"/>
    <Setter Property="Visibility" Value="Hidden"/>
    <Setter Property="FontStyle" Value="Italic"/>
    <Setter Property="Foreground" Value="Gray"/>
    <Setter Property="Margin" Value="4"/>
    <Style.Triggers>
        <DataTrigger Binding="{Binding Tag.Text, RelativeSource={RelativeSource Self}}"
                     Value="{x:Static sys:String.Empty}">
            <Setter Property="Visibility" Value="Visible"/>
        </DataTrigger>
    </Style.Triggers>
</Style>

Применяем:

<Grid>
    <TextBox x:Name="TB"/>
    <TextBlock Text="Placeholder" Tag="{Binding ElementName=TB}"
               Style="{StaticResource PlaceholderStyle}"/>
</Grid>

Получается:

Если вы хотите, чтобы при установке курсора внутрь фокус плейсхолдер пропадал, вам нужен чуть-чуть более сложный код:

<MultiDataTrigger>
    <MultiDataTrigger.Conditions>
        <Condition Binding="{Binding Tag.Text, RelativeSource={RelativeSource Self}}"
             Value="{x:Static sys:String.Empty}"/>
        <Condition Binding="{Binding Tag.IsKeyboardFocused,
                                 RelativeSource={RelativeSource Self}}">
            <Condition.Value>
                <sys:Boolean>False</sys:Boolean>
            </Condition.Value>
        </Condition>
    </MultiDataTrigger.Conditions>
    <Setter Property="Visibility" Value="Visible"/>
</MultiDataTrigger>

Если вы хотите сделать из этого UserControl, это тоже несложно. Единственная тонкость — при привязке TextBox.Text к свойству UserControl'а нужно устанавливать UpdateSourceTrigger=PropertyChanged.

Итак, заводим стандартный UserControl, называем его InputBox, и кладём в него dependency property Text и Placeholder:

public partial class InputBox : UserControl
{
    public InputBox()
    {
        InitializeComponent();
    }
    #region dp string Text
    public string Text
    {
        get { return (string)GetValue(TextProperty); }
        set { SetValue(TextProperty, value); }
    }
    public static readonly DependencyProperty TextProperty =
        DependencyProperty.Register(
            "Text", typeof(string), typeof(InputBox), new FrameworkPropertyMetadata("",
                  FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
    #endregion
    #region dp string Placeholder
    public string Placeholder
    {
        get { return (string)GetValue(PlaceholderProperty); }
        set { SetValue(PlaceholderProperty, value); }
    }
    public static readonly DependencyProperty PlaceholderProperty =
        DependencyProperty.Register(
            "Placeholder", typeof(string), typeof(InputBox), new PropertyMetadata(""));
    #endregion
}

Теперь XAML. Можно пойти традиционным путём:

<UserControl x:Class="Test.InputBox"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:local="clr-namespace:Test">
    <Grid DataContext="{Binding RelativeSource={RelativeSource FindAncestor,
                                    AncestorType=UserControl}}">
        <TextBox
            Text="{Binding Text, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
            Foreground="{Binding Foreground}" Background="{Binding Background}"/>
        <TextBlock Text="{Binding Placeholder}" Foreground="Gray"
            IsHitTestVisible="False" FontStyle="Italic" Margin="4">
            <TextBlock.Style>
                <Style TargetType="TextBlock">
                    <Setter Property="Visibility" Value="Hidden"/>
                    <Style.Triggers>
                        <DataTrigger Binding="{Binding Text}" Value="">
                            <Setter Property="Visibility" Value="Visible"/>
                        </DataTrigger>
                    </Style.Triggers>
                </Style>
            </TextBlock.Style>
        </TextBlock>
    </Grid>
</UserControl>

Можно переопределить ControlTemplate:

<UserControl x:Class="Test.InputBox"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:local="clr-namespace:Test">
    <UserControl.Template>
        <ControlTemplate TargetType="UserControl">
            <Grid DataContext="{Binding RelativeSource={RelativeSource FindAncestor,
                                            AncestorType=UserControl}}">
                <TextBox
                    Text="{Binding Text, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
                    Foreground="{TemplateBinding Foreground}"
                    Background="{TemplateBinding Background}"/>
                <TextBlock Text="{Binding Placeholder}" Name="PH" Visibility="Hidden"
                   IsHitTestVisible="False" FontStyle="Italic" Foreground="Gray" Margin="4"/>
            </Grid>
            <ControlTemplate.Triggers>
                <Trigger Property="local:InputBox.Text" Value="">
                    <Setter Property="Visibility" Value="Visible" TargetName="PH"/>
                </Trigger>
            </ControlTemplate.Triggers>
        </ControlTemplate>
    </UserControl.Template>
</UserControl>

Результат одинаковый.

READ ALSO
Установка selectItem после удаления элемента

Установка selectItem после удаления элемента

Имеется простой listBox, с него пользователь может удалить выбранные элементы, те

255
Изменения стиля внутренних контролов у UserControl&#39;а

Изменения стиля внутренних контролов у UserControl'а

Подскажите пожалуйста, как правильно делают такие вещиУ меня есть UserControl - SearchBox, который собран из TextBox и Button

258
C# функции, подпрограммы [требует правки]

C# функции, подпрограммы [требует правки]

Даны 4 целых числа:a,b,c,dДля каждой из всех комбинаций по 3 числа, ИСПОЛЬЗУЯ ФУНКЦИЮ, найти количество отрицательных среди них

264
Cannot open database requested by the login

Cannot open database requested by the login

Изучаю ASPNet Core MVC по книге Адама Фримена

794