当前位置: 代码迷 >> C# >> WPF,一个自定义的TextBox,哪位高手能解释下这个现象吗
  详细解决方案

WPF,一个自定义的TextBox,哪位高手能解释下这个现象吗

热度:31   发布时间:2016-05-05 04:47:28.0
WPF,一个自定义的TextBox,谁能解释下这个现象吗?
本帖最后由 df4rtv4t 于 2015-02-07 09:53:14 编辑
一个自定义的TextBox:

<Window x:Class="WPF1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"  
xmlns:ed="http://schemas.microsoft.com/expression/2010/drawing" 
mc:Ignorable="d"
        Title="MainWindow" Height="500" Width="875">
    <Window.Resources>
        <SolidColorBrush x:Key="TextBox.Static.Border" Color="#FFABAdB3"/>
        <SolidColorBrush x:Key="TextBox.MouseOver.Border" Color="#FF7EB4EA"/>
        <SolidColorBrush x:Key="TextBox.Focus.Border" Color="#FF569DE5"/>
        <Style x:Key="TextBoxStyle1" TargetType="{x:Type TextBox}">
            <Setter Property="KeyboardNavigation.TabNavigation" Value="None"/>
            <Setter Property="HorizontalContentAlignment" Value="Left"/>
            <Setter Property="FocusVisualStyle" Value="{x:Null}"/>
            <Setter Property="AllowDrop" Value="true"/>
            <Setter Property="ScrollViewer.PanningMode" Value="VerticalFirst"/>
            <Setter Property="Stylus.IsFlicksEnabled" Value="False"/>
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="{x:Type TextBox}">
                        <Border x:Name="border" BorderBrush="#FF5A660B" BorderThickness="1" SnapsToDevicePixels="True">
                            <ScrollViewer x:Name="PART_ContentHost" Focusable="false" HorizontalScrollBarVisibility="Hidden" VerticalScrollBarVisibility="Hidden"/>
                        </Border>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
    </Window.Resources>
    <Grid>
        <TextBox Name="dd" Height="30" Width="100" Style="{DynamicResource TextBoxStyle1}"/>        
    </Grid>
</Window>



上面是一个自定义的TextBox控件,一切安好,使用上没有什么问题。但是,如果把控件模板中ScrollViewer 的x:Name="PART_ContentHost" 去掉,结果就不能在TextBox中输入文本了,这是什么原因呢?Name能影响什么呢?
------解决思路----------------------

没有了Name,程序找不到对像了。

------解决思路----------------------
nono,PART_ContentHost 是专属名词, 就像 DocumentViewer控件,它也有这个命名。 个人觉得  这也就是为什么会有TemplateBinding 的原因,你可以binding它的Width与Height 属性,但是不需要 任何x:name  ,但是ScrollViewer就需要。

如果会使用blend工具的话,可以编辑一个控件的副本,会发现有很多以 PART开头的名词。
------解决思路----------------------
我们知道WPF控件是不用有固定形状的,我们可以通过Style来任意改变它的具体表现。
但是,控件本身具有特定的逻辑和作用,比如一个按钮,应该是可以点击的;一个颜色选择控件,应该是可以用来选颜色的。
这里就存在了一个矛盾:如果可以任意改变控件的具体表现,那么如何保证它特有的逻辑和作用呢?

答案就是WPF控件的‘部件’概念。简单的说,就是你可以任意改变我,但是要提供我期待的部件,否则我的逻辑和作用就不能得到保证。如果阅读TextBoxBase的MSDN参考(TextBox继承于TextBoxBase),你会看到如下的特性:
引用:https://msdn.microsoft.com/zh-cn/library/system.windows.controls.primitives.textboxbase
[TemplatePartAttribute(Name = "PART_ContentHost", Type = typeof(FrameworkElement))]
[LocalizabilityAttribute(LocalizationCategory.Text)]
public abstract class TextBoxBase : Control

该特性表明TextBoxBase控件期待你提供一个名字叫PART_ContentHost的部件,该部件必须是FrameworkElement。而TextBoxBase将会把具体的TextView和TextEditor放到PART_ContentHost里面。
------解决思路----------------------
这里我用代码解释WPF控件中‘部件’的使用。

1、新建一个WPF自定义控件项目,并取名为MyUpDown。
2、把CustomControl1(包括文件等)换名为MyUpDown。
3、把Generic.xaml和MyUpDown.cs内容换成:

<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:MyUpDown">
    <Style TargetType="{x:Type local:MyUpDown}">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="local:MyUpDown">
                    <StackPanel Margin="3" Orientation="Horizontal" Background="{TemplateBinding Background}">
                        <TextBlock Width="30" VerticalAlignment="Center"
                                   Text="{Binding Path=Value, RelativeSource={RelativeSource FindAncestor,AncestorType={x:Type local:MyUpDown}}}" />
                        <RepeatButton Width="40" Content="Up"  Name="PART_UpButton" />
                        <RepeatButton Width="40" Content="Down" Name="PART_DownButton" />
                    </StackPanel>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</ResourceDictionary>



[TemplatePart(Name = "PART_UpButton", Type = typeof(ButtonBase))]
[TemplatePart(Name = "PART_DownButton", Type = typeof(ButtonBase))]
public partial class MyUpDown : Control
{
    static MyUpDown()
    {
        DefaultStyleKeyProperty.OverrideMetadata(typeof(MyUpDown), new FrameworkPropertyMetadata(typeof(MyUpDown)));
    }

    public override void OnApplyTemplate()
    {
        UpButton = GetTemplateChild("PART_UpButton") as ButtonBase;
        DownButton = GetTemplateChild("PART_DownButton") as ButtonBase;
    }

    ButtonBase upButton;
    ButtonBase UpButton
    {
        get { return this.upButton; }
        set
        {
            if (this.upButton != null) this.upButton.Click -= Button_Click;
            this.upButton = value;
            if (this.upButton != null) this.upButton.Click += Button_Click;
        }
    }

    ButtonBase downButton;
    ButtonBase DownButton
    {
        get { return this.downButton; }
        set
        {
            if (this.downButton != null) this.downButton.Click -= Button_Click;
            this.downButton = value;
            if (this.downButton != null) this.downButton.Click += Button_Click;
        }
    }

    void Button_Click(object sender, RoutedEventArgs e)
    {
        if (object.ReferenceEquals(sender, this.upButton)) this.Value++;
        if (object.ReferenceEquals(sender, this.downButton)) this.Value--;
    }

    public int Value
    {
        get { return (int)GetValue(ValueProperty); }
        set { SetValue(ValueProperty, value); }
    }

    public static readonly DependencyProperty ValueProperty = DependencyProperty.Register("Value", typeof(int), typeof(MyUpDown));
}

4、确保编译成功(控件本身不能运行)。
5、新建一个WPF窗口项目,并添加对MyUpDown项目的引用。
6、从工具栏中,拖一个MyUpDown控件到窗口中,编译运行就可以看到一个类似NumericUpDown的控件。点击Up和Down按钮,可以看到数字的相应变化。

下面,我们来自定义(假设是别人提供的)MyUpDown控件:
<Grid>
    <MyUpDown:MyUpDown Value="123" >
        <MyUpDown:MyUpDown.Template>
            <ControlTemplate TargetType="MyUpDown:MyUpDown">
                <Grid>
                    <TextBox Text="{Binding Path=Value,RelativeSource={RelativeSource AncestorType=MyUpDown:MyUpDown}}" Width="150" Height="30" />
                    <Button Content="UpOnly" Width="50" Height="30" Margin="200 0 0 0" />
                </Grid>
            </ControlTemplate>
        </MyUpDown:MyUpDown.Template>
    </MyUpDown:MyUpDown>
</Grid>

在新的模板中,我们用TextBox来支持直接编辑,同时提供一个按键。编译运行,可以发现MyUpDown可以显示Value的数值,但是,两个按钮不见了,我们提供的UpOnly按钮不起作用。这就是我们没有按照控件期待提供特定名字按钮的结果。

现在,把Button的代码更改为:
<Button Content="UpOnly" Width="50" Height="30" Margin="200 0 0 0" Name="PART_UpButton" />
编译运行,可以发现只有一个按钮,但该UpOnly按钮可以向上调整数字了。

用代码上看,MyUpDown.cs第10行中,当开始应用模板OnApplyTemplate的时候,MyUpDown控件尝试着寻找两个特定名字类型为按钮的部件:
GetTemplateChild("PART_UpButton") as ButtonBase;
...
如果找到按钮部件,则为按钮添加单击事件的响应函数,并在响应函数中相应地对当前数字进行加一或减一,从而达到用按钮调整数字的特定作用(NumericalUpDown)。

如果找不到部件,WPF控件可以根据严重程度,选择抛出异常,或减少功能等措施。
  相关解决方案