In a button's control template, how can I set the color of contained text?
Asked Answered
D

3

10

Using Silverlight 4 & WPF 4, I'm trying to create a button style that alters the text color of any contained text when the button is mouseover'd. Since I'm trying to make this compatible with both Silverlight & WPF, I'm using the visual state manager:

<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>

Since this is a template for a regular old button, I know there's no guarantee that there even is a textblock inside of it, and at first I wasn't sure this was even possible. Curiously, the text color does change if the button is declared like:

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

but it does not change if the button is declared like:

<Button>
    <TextBlock Text="Hello, World!" /> <!-- Same result with <TextBlock>Hello, World </TextBlock> -->
</Button>

Even though the visual tree (when inspected in snoop) is identical (Button -> ContentPresenter -> TextBlock), with the caveat that the textblock created in the 1st version has it's data context set to "Hello, World", whereas the textblock in the second version merely has its text property set. I'm presuming this has something to do with the order of control creation (the first version the button creates the TextBlock, in the second version the textblock might be created first? Really not sure on this).

In the course of researching this, I've seen some solutions that work in Silverlight (like replacing the ContentPresenter with a ContentControl), but that won't work in WPF (program actually crashes).

Since this is in the button's control template, and I'd like to use the VSM if possible, I think that also rules out explicitly changing the Button's own Foreground property (I don't know how I would access that from within the template?)

I'd really appreciate any help, advice anyone could give.

Drice answered 4/10, 2010 at 15:41 Comment(1)
I am surprised it worked at all considering that TextElement got nothing to do with TextBlock.Voltaire
D
6

So after some more thinking, the solution I've ultimately arrived at is to add an attached property to the ContentPresenter element within the button's control template. The attached property accepts a Color and when set examines the visual tree of the content presenter for any TextBlocks, and in turn sets their Foreground properties to the value passed in. This could obviously be expanded/made to handle additional elements but for now it works for what I need.

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);
            }
        }
    }

and I modified the template like so:

<ContentPresenter x:Name="contentPresenter" 
                  ContentTemplate="{TemplateBinding ContentTemplate}" 
                  local:ButtonAttachedProperties.ButtonTextForeground="{StaticResource ButtonTextNormalColor}" />
Drice answered 5/10, 2010 at 17:51 Comment(2)
This is a good solution, too. I'm curious what animation storyboards you'd be capable of using since you are binding to a StaticResource.Parakeet
I had to use an ObjectAnimationUsingKeyFrames, as opposed to a ColorAnimation or something similar.Drice
P
5

This is a tricky one. The foreground is a property of the button that is passed through to the controls the content presenter creates. Because it is a dependency property and not a property of the controls available in the template it is hard to animate it using pure xaml.

MSDN Has a couple samples on how to change foreground color. However, it doesn't sound like you want to do this work from code. (http://msdn.microsoft.com/en-us/library/system.windows.controls.button(VS.95).aspx)

The button's default template is holding you back. As you've noticed, button is a content-less control; meaning the designer can push some random visual object inside of it. Being content-less forces the foreground property to be a member of the template rather than the control because there isn't a guarranteed component to set the color to. Which is why there is a content presenter inside of it.

So now you have two options. 1. Easy but not flexible (pure xaml), 2. Create your own control that does everything a button does (requires code and lots of testing)

I'll implement #1 for you.

If you modify the template of the button and remove the content presenter you can place inside of it two textblocks. One with the normal color, the other with your mouse over color.

<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"/> 

Notice how the Text properties of the Textblocks are bound to the content from button. This becomes a problem if there is ever a need to bind to ANYTHING other than text. TextBlocks simply can't show anything but AlphaNumeric values.

Also notice that by default I have collapsed the visibility on the RedBlock.

Then, in my MouseOver VisualState's Storyboard I can animate the RedBlock to be visible and the BlueBlock to be invisible:

<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>

It feels like a hack, and I probably wouldn't implement this button in lots of places. I'd want to make my own Button with good DependencyProperties to bind to. One each for HighlightForeground and Text. I'd also want to hide or atleast throw an exception if someone tried to set the content to anything other than AlphaNumeric values. However, the XAML would be the same, I'd have different textblocks for different visual states.

I hope this helps.

Have a great day.

-Jeremiah

Parakeet answered 5/10, 2010 at 16:13 Comment(1)
Hi Jeremiah, this is a really nice solution and works well in a situation where the button would only contain text. In my case I'd like to have a solution more generic, so that any textblock children will get their foreground colors set. (Any interesting thing to notice is this is how Button works when you set its Foreground property explicitly -- any child TextBlocks receive the appropriate color, no matter how nested inside panels/controls they are in the button's content. I've detailed the solution I've ultimately arrived at using attached properties in this thread.Drice
I
0

I use this in my template and it works fine

<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>

You must insert this code in your button template creation but i use this when i put text in the button content and if you want to add other controls on it use the normal style

Information answered 20/11, 2015 at 20:58 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.