V dnešním článku si ukážeme jak napsat vlastní “WatermarkedTextBox”. Jedná se o klasický TextBox, který je rozšířen o vodoznak (doplňující informace co se zde má např. vyplnit).

Začínáme

Ve VS vytvoříme nový projekt typu “WPF Custom Control Library” (Standardně se vytvoří i nová třída CustomControl.cs, kterou můžeme smazat) a pojmenujeme ho jako “CustomControls”. Pro začátek stačí když si vytvoříme hlavní třídu “WatermarkedTextBox”

[TemplatePart(Name = "PART_TextBox", Type = typeof(TextBox))]
[TemplatePart(Name = "PART_TextBlock", Type = typeof(TextBlock))]
public class WatermarkedTextBox : TextBox
{
    static WatermarkedTextBox()
    {
        DefaultStyleKeyProperty.OverrideMetadata(typeof(WatermarkedTextBox),
            new FrameworkPropertyMetadata(typeof(WatermarkedTextBox)));
    }
}

WatermarkedTextBox se bude skládat z klasického TextBoxu (pojmenovaný jako PART_TextBox) a TextBlocku (PART_TextBlock) sloužící pro zobrazení vodoznaku. Pomocí metody OverrideMetada, kterou voláme v konstruktoru řekneme, že chceme využít styl definovaný v souboru Generic.xaml

<Style TargetType="local:WatermarkedTextBox">
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="local:WatermarkedTextBox">
                <Grid>
                    <TextBox x:Name="PART_TextBox"/>
                    <TextBlock x:Name="PART_TextBlock"/>
                </Grid>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

Aby jsme si mohli tento hrubý základ otestovat, vytvoříme novou WPF aplikaci, přidáme referenci na knihovnu CustomControls a vložíme WatermarkedTextBox do hlavního okna MainWindow

<Window xmlns:my="clr-namespace:CustomControls;assembly=CustomControls" 
        x:Class="CustomControls.Demo.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
        <my:WatermarkedTextBox  Height="50" Margin="10"/>
    </Grid>
</Window>

Po spuštění aplikace se nám zobrazí náš control, ale žádný vodoznak není vidět a navíc nejde do něho ani psát. Pojďme tedy přidat základní funkcionalitu.

Základní funkcionalita

Pro vytvoření základní funkcionality budeme potřebovat následující vlastnosti:

  • Watermark – text, který se zobrazí jako vodoznak
  • WatermarkForeground – barva vodoznaku
  • WatermarkStyle – styl vodoznaku
public static readonly DependencyProperty WatermarkProperty = DependencyProperty.Register(
    "Watermark",
    typeof(string),
    typeof(WatermarkedTextBox),
    new PropertyMetadata("Zadejte text..."));
public static readonly DependencyProperty WatermarkStyleProperty = DependencyProperty.Register(
    "WatermarkStyle",
    typeof(Style),
    typeof(WatermarkedTextBox));
public static readonly DependencyProperty WatermarkForegroundProperty = DependencyProperty.Register(
    "WatermarkForeground",
    typeof(Brush),
    typeof(WatermarkedTextBox),
    new PropertyMetadata(new SolidColorBrush(Color.FromArgb(255, 134, 134, 134))));
[Description("Vrati nebo nastavi vodoznak")]
public string Watermark
{
    get { return (string)GetValue(WatermarkProperty); }
    set { SetValue(WatermarkProperty, value); }
}
[Description("Vrati nebo nastavi styl vodoznaku")]
public Style WatermarkStyle
{
    get { return (Style)GetValue(WatermarkStyleProperty); }
    set { SetValue(WatermarkStyleProperty, value); }
}
[Description("Vrati nebo nastavi barvu vodoznaku")]
public Brush WatermarkForeground
{
    get { return (Brush)GetValue(WatermarkForegroundProperty); }
    set { SetValue(WatermarkForegroundProperty, value); }
}

Všechny potřebné vlastnosti jsou definovány jako jednoduché Dependency property a není potřeba provádět v kódu na pozadí nějaké další věci. O vše ostatní se postaráme přímo v XAMLu.

Nejdříve rozšíříme TextBlock, který se bude starat o zobrazení vodoznaku:

 <TextBlock x:Name="PART_TextBlock"
            Style="{TemplateBinding WatermarkStyle}"
            Foreground="{TemplateBinding WatermarkForeground}"
            Text="{TemplateBinding Watermark}"/>

Nyní umí náš TextBlock zobrazit defaultní vodoznak (můžete si to vyzkoušet spuštěním aplikace). Problém je stále v tom, že do TextBoxu nelze psát. Tento problém vyřešíme nastavením vlastnosti IsHitTestVisible na false u elementu TextBlock.

Další problém nám spočívá vtom, že když začneme psát do TextBoxu, vodoznak nezmizí, ale je stále vidět což nepůsobí dobře. U tohoto řešení využijeme triggery, kterými definujeme chování vodoznaku:

  • Pokud je hodnota Textu - null zobraz vodoznak
  • Pokud je hodnota Textu prázdný řetězec – zobraz vodoznak
  • Pokud je hodnota Textu nějaký řetězec – schovej vodoznak
<ControlTemplate.Triggers>
    <DataTrigger Binding="{Binding Path=Text, ElementName=PART_TextBox}" Value="{x:Null}">
        <Setter TargetName="PART_TextBlock" Property="Visibility" Value="Visible"/>
    </DataTrigger>
    <DataTrigger Binding="{Binding Path=Text.Length, ElementName=PART_TextBox}" Value="0">
        <Setter TargetName="PART_TextBlock" Property="Visibility" Value="Visible"/>
    </DataTrigger>
</ControlTemplate.Triggers>

Na závěr ještě upravíme chování tabelátoru nastavením vlastnosti IsTabStop celému WatermarkedTextBoxu na False a vnitřímu TextBoxu na true.

na závěr ještě kompletní souhrn:

WatermarkedTextBox:

namespace CustomControls
{
    using System.Windows.Media;
    using System.Windows;
    using System.Windows.Controls;
    using System.ComponentModel;
    [TemplatePart(Name = "PART_TextBox", Type = typeof(TextBox))]
    [TemplatePart(Name = "PART_TextBlock", Type = typeof(TextBlock))]
    public class WatermarkedTextBox : TextBox
    {
        public static readonly DependencyProperty WatermarkProperty = DependencyProperty.Register(
                "Watermark",
                typeof(string),
                typeof(WatermarkedTextBox),
                new PropertyMetadata("Zadejte text..."));
        public static readonly DependencyProperty WatermarkStyleProperty = DependencyProperty.Register(
            "WatermarkStyle",
            typeof(Style),
            typeof(WatermarkedTextBox));
        public static readonly DependencyProperty WatermarkForegroundProperty = DependencyProperty.Register(
            "WatermarkForeground",
            typeof(Brush),
            typeof(WatermarkedTextBox),
            new PropertyMetadata(new SolidColorBrush(Color.FromArgb(255, 134, 134, 134))));
        static WatermarkedTextBox()
        {
            DefaultStyleKeyProperty.OverrideMetadata(typeof(WatermarkedTextBox),
                new FrameworkPropertyMetadata(typeof(WatermarkedTextBox)));
        }
        [Description("Vrati nebo nastavi vodoznak")]
        public string Watermark
        {
            get { return (string)GetValue(WatermarkProperty); }
            set { SetValue(WatermarkProperty, value); }
        }
        [Description("Vrati nebo nastavi styl vodoznaku")]
        public Style WatermarkStyle
        {
            get { return (Style)GetValue(WatermarkStyleProperty); }
            set { SetValue(WatermarkStyleProperty, value); }
        }
        [Description("Vrati nebo nastavi barvu vodoznaku")]
        public Brush WatermarkForeground
        {
            get { return (Brush)GetValue(WatermarkForegroundProperty); }
            set { SetValue(WatermarkForegroundProperty, value); }
        }
    }
}

Generic.xaml:

<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:CustomControls">
  
    <Style TargetType="local:WatermarkedTextBox">
        <Setter Property="IsTabStop" Value="False"/>
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="local:WatermarkedTextBox">
                    <Grid>
                        <TextBox x:Name="PART_TextBox" IsTabStop="True"/>
                        <TextBlock x:Name="PART_TextBlock"
                                   Style="{TemplateBinding WatermarkStyle}"
                                   Foreground="{TemplateBinding WatermarkForeground}"
                                   Text="{TemplateBinding Watermark}"
                                   IsHitTestVisible="False"
                                   Visibility="Hidden"/>
                    </Grid>
                    <ControlTemplate.Triggers>
                        <DataTrigger Binding="{Binding Path=Text, ElementName=PART_TextBox}" Value="{x:Null}">
                            <Setter TargetName="PART_TextBlock" Property="Visibility" Value="Visible"/>
                        </DataTrigger>
                        <DataTrigger Binding="{Binding Path=Text.Length, ElementName=PART_TextBox}" Value="0">
                            <Setter TargetName="PART_TextBlock" Property="Visibility" Value="Visible"/>
                        </DataTrigger>
                    </ControlTemplate.Triggers>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
    
</ResourceDictionary>

MainWindow.xaml:

<Window xmlns:my="clr-namespace:CustomControls;assembly=CustomControls" 
        x:Class="CustomControls.Demo.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
        <Grid.Resources>
            <Style x:Key="watermarkStyle" TargetType="{x:Type my:WatermarkedTextBox}">
                <Setter Property="WatermarkForeground" Value="#FF868686" />
                <Setter Property="WatermarkStyle">
                    <Setter.Value>
                        <Style TargetType="TextBlock">
                            <Setter Property="HorizontalAlignment" Value="Left" />
                            <Setter Property="Margin" Value="10,0,0,0" />
                            <Setter Property="VerticalAlignment" Value="Center" />
                            <Setter Property="FontStyle" Value="Italic" />
                            <Setter Property="TextWrapping" Value="NoWrap" />
                        </Style>
                    </Setter.Value>
                </Setter>
            </Style>
        </Grid.Resources>
        <my:WatermarkedTextBox x:Name="wtxt1"  Height="50" Margin="10"/>
        <my:WatermarkedTextBox x:Name="wtxt2"  Height="50" Margin="8,186,12,75"
                               Watermark="Vyplnte pozadovane udaje"
                               Style="{StaticResource watermarkStyle}"/>
    </Grid>
</Window>

Implementace vlastního WatermarkedTextBoxu byla vcelku jednoduchá a výsledný projekt si můžete stáhnout na adrese https://skydrive.live.com/redir.aspx?cid=35f7223b2eb524fd&resid=35F7223B2EB524FD!314&parid=35F7223B2EB524FD!313

Lukáš