在按钮的控件模板中,我该如何设置包含文本的颜色?

10

我正在使用Silverlight 4和WPF 4,尝试创建一个按钮样式,在鼠标悬停在按钮上时改变任何包含文本的文字颜色。由于我想使其与Silverlight和WPF兼容,因此我使用了可视状态管理器:

<Style TargetType="{x:Type Button}">
<Setter Property="Template">
    <Setter.Value>
        <ControlTemplate TargetType="Button">
            <Border x:Name="outerBorder" CornerRadius="4" BorderThickness="1" BorderBrush="#FF757679">
                <VisualStateManager.VisualStateGroups>
                    <VisualStateGroup x:Name="CommonStates">
                        <VisualState x:Name="Normal" />
                        <VisualState x:Name="MouseOver">
                            <Storyboard>
                                <ColorAnimation Duration="0" To="#FFFEFEFE"
                                                Storyboard.TargetProperty="(TextElement.Foreground).(SolidColorBrush.Color)"
                                                Storyboard.TargetName="contentPresenter"/> 
                            </Storyboard>
                        </VisualState>
                    </VisualStateGroup>
                </VisualStateManager.VisualStateGroups>
                <Grid>
                    <Border x:Name="Background" CornerRadius="3" BorderThickness="1" BorderBrush="Transparent">
                        <Grid>
                            <ContentPresenter x:Name="contentPresenter" ContentTemplate="{TemplateBinding ContentTemplate}"/>
                        </Grid>
                    </Border>
                </Grid>
            </Border>
        </ControlTemplate>
    </Setter.Value>
</Setter>

因为这是一个普通按钮的模板,我知道里面可能没有文本块,一开始我甚至不确定这是否可能。但是有趣的是,如果像这样声明按钮,文本颜色确实会改变:

<Button Content="Hello, World!" />

但是如果按钮的声明方式如下,它不会改变:

<Button>
    <TextBlock Text="Hello, World!" /> <!-- Same result with <TextBlock>Hello, World </TextBlock> -->
</Button>
即使在 snoop 工具中检查的可视树是相同的 (Button -> ContentPresenter -> TextBlock),但第一版创建的 TextBlock 的数据上下文设置为“Hello, World”,而第二版中的 TextBlock 只是设置了其文本属性。我认为这可能与控件创建的顺序有关 (在第一个版本中,按钮创建 TextBlock,在第二个版本中,TextBlock 可能先创建?对此真的不确定)。
在研究过程中,我看到了一些在 Silverlight 中起作用的解决方案 (比如用 ContentControl 替换 ContentPresenter),但在 WPF 中不起作用 (程序会崩溃)。
由于这是在按钮的控件模板中,如果可能,我想使用 VSM,这也排除了显式更改按钮自身的前景属性 (我不知道怎样从模板内部访问它?)。
我非常感谢任何人能给予的帮助和建议。

考虑到TextElement与TextBlock没有任何关系,我很惊讶它居然能够工作。 - Denis
3个回答

6
因此,经过更多的思考,我最终想到的解决方案是在按钮控件模板中的ContentPresenter元素上添加一个附加属性。该附加属性接受一个颜色值,并在设置时检查内容呈现器的可视树中是否有任何文本块,然后将它们的Foreground属性设置为传递的值。显然,这可以扩展/处理其他元素,但目前它对我所需的内容起作用。
public static class ButtonAttachedProperties
    {
        /// <summary>
        /// ButtonTextForegroundProperty is a property used to adjust the color of text contained within the button.
        /// </summary>
        public static readonly DependencyProperty ButtonTextForegroundProperty = DependencyProperty.RegisterAttached(
            "ButtonTextForeground",
            typeof(Color),
            typeof(FrameworkElement),
            new FrameworkPropertyMetadata(Color.FromArgb(255, 0, 0, 0), FrameworkPropertyMetadataOptions.AffectsRender, OnButtonTextForegroundChanged));

        public static void OnButtonTextForegroundChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
        {
            if (e.NewValue is Color)
            {
                var brush = new SolidColorBrush(((Color) e.NewValue)) as Brush;
                if (brush != null)
                {
                    SetTextBlockForegroundColor(o as FrameworkElement, brush);
                }
            }
        }

        public static void SetButtonTextForeground(FrameworkElement fe, Color color)
        {
            var brush = new SolidColorBrush(color);
            SetTextBlockForegroundColor(fe, brush);
        }

        public static void SetTextBlockForegroundColor(FrameworkElement fe, Brush brush)
        {
            if (fe == null)
            {
                return;
            }

            if (fe is TextBlock)
            {
                ((TextBlock)fe).Foreground = brush;
            }

            var children = VisualTreeHelper.GetChildrenCount(fe);
            if (children > 0)
            {
                for (int i = 0; i < children; i++)
                {
                    var child = VisualTreeHelper.GetChild(fe, i) as FrameworkElement;
                    if (child != null)
                    {
                        SetTextBlockForegroundColor(child, brush);
                    }
                }
            }
            else if (fe is ContentPresenter)
            {
                SetTextBlockForegroundColor(((ContentPresenter)fe).Content as FrameworkElement, brush);
            }
        }
    }

我修改了模板,如下所示:

<ContentPresenter x:Name="contentPresenter" 
                  ContentTemplate="{TemplateBinding ContentTemplate}" 
                  local:ButtonAttachedProperties.ButtonTextForeground="{StaticResource ButtonTextNormalColor}" />

这也是一个不错的解决方案。我很好奇你能使用哪些动画故事板,因为你正在绑定到 StaticResource。 - Jeremiah
我不得不使用ObjectAnimationUsingKeyFrames,而不是ColorAnimation或类似的东西。 - Jordan0Day

5
这是一个棘手的问题。前景是按钮的属性,通过传递给内容提供程序创建的控件。由于它是依赖属性而不是模板中可用控件的属性,因此很难使用纯xaml对其进行动画处理。
MSDN有一些关于如何更改前景颜色的示例。但是,听起来你不想从代码中进行这项工作。(http://msdn.microsoft.com/en-us/library/system.windows.controls.button(VS.95).aspx)
按钮的默认模板限制了你。正如你注意到的那样,按钮是一个没有内容的控件;这意味着设计师可以在其中推入一些随机的视觉对象。没有组件来设置颜色,使前景属性成为模板的成员而不是控件的成员。这就是为什么里面有一个内容提供程序的原因。
现在你有两个选择。1.容易但不灵活(纯xaml),2.创建自己的控件,完成按钮所做的一切(需要代码和大量测试)。
我将为您实施#1。
如果修改按钮的模板并删除内容提供程序,则可以在其中放置两个文本块。一个具有正常颜色,另一个具有鼠标悬停颜色。
<TextBlock x:Name="RedBlock" Text="{TemplateBinding Content}"      HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" Margin="{TemplateBinding Padding}" VerticalAlignment="{TemplateBinding VerticalContentAlignment}" Foreground="Red" Visibility="Collapsed"/>       
<TextBlock x:Name="BlueBlock" Text="{TemplateBinding Content}" HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" Margin="{TemplateBinding Padding}" VerticalAlignment="{TemplateBinding VerticalContentAlignment}" Foreground="#FF0027FF"/> 

请注意,TextBlock的文本属性绑定到按钮内容。如果需要绑定除文本之外的任何内容,则会出现问题。TextBlock无法显示除字母数字值以外的任何内容。
此外,请注意默认情况下我将RedBlock的可见性折叠起来。
然后,在我的“MouseOver VisualState's Storyboard”中,我可以动画显示RedBlock并将BlueBlock隐藏:
<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Visibility)" Storyboard.TargetName="RedBlock">
    <DiscreteObjectKeyFrame KeyTime="0">
        <DiscreteObjectKeyFrame.Value>
            <Visibility>Visible</Visibility>
        </DiscreteObjectKeyFrame.Value>
    </DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Visibility)" Storyboard.TargetName="BlueBlock">
    <DiscreteObjectKeyFrame KeyTime="0">
        <DiscreteObjectKeyFrame.Value>
            <Visibility>Collapsed</Visibility>
        </DiscreteObjectKeyFrame.Value>
    </DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>

这种做法感觉像是一种技巧,我可能不会在很多地方使用这个按钮。我希望自己能够创建一个具有良好依赖属性绑定的按钮。为高亮前景色和文本各定义一个依赖属性。此外,如果有人试图将内容设置为除字母数字值之外的任何内容,我也想要隐藏或至少抛出异常。但是,XAML代码将保持不变,我将为不同的可视状态使用不同的文本块。

希望这有所帮助。

祝您拥有美好的一天。

- Jeremiah


嗨,Jeremiah,这是一个非常好的解决方案,在按钮只包含文本的情况下运行良好。在我的情况下,我想要一个更通用的解决方案,以便任何文本块子元素都可以设置其前景色。(有趣的事情是,当您显式设置其前景属性时,按钮的工作方式就是这样--任何子TextBlocks都会接收到适当的颜色,无论它们在按钮内容中嵌套在哪个面板/控件中。我已经在此线程中详细说明了我最终使用附加属性到达的解决方案。 - Jordan0Day

0

我在我的模板中使用它,它运行良好

<ControlTemplate TargetType="Button">
                <Border 
                    Margin="{TemplateBinding Margin}"
                    BorderBrush="{StaticResource BorderBrush}"
                    BorderThickness="{TemplateBinding BorderThickness}" 
                    CornerRadius="2"
                    >

                    <TextBlock 
                        Margin="{TemplateBinding Padding}"
                        Text="{TemplateBinding Content}" 
                        Foreground="White"
                         HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
                        VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/>
                </Border>

您必须将此代码插入到按钮模板创建中,但是当我在按钮内容中放置文本时,我会使用它,如果您想在其上添加其他控件,请使用正常样式。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接