On UIElement.Opacity and the Removal of Storyboards

by Christoph Menge in Software

Today, I encountered two WPF related problems:

1 – UIElement.Opacity and the way it is being ‘inherited’

Every UIElement has an Opacity property, which will we be used when rendering the control. When rendering an object, 0.0 means the object is fully transparent, thereby invisible to the user, while 1.0 means it is fully opaque. Since controls can be composed so simply, it is desireable that child controls will use the opacity of their parents, so you don’t have to set it explicitly for each child.

So there is some kind of opacity stack, which will multiply opacities with each other when traversing down the tree upon rendering. Therefore, a 100% opaque button on a 10% opaque canvas will yield 10% opacity for the button (for an even longer description, see this MSDN entry).

However, it also seemed natural to me that it is well possible to specify an opacity of 2.0, so the child of a half-transparent object is rendered fully opaque (the same math applies). This is, however, not the case. While there are no warnings or exceptions (even the value is preserved), it will be clipped during the rendering process silently, therefore not allowing any UIElement to be more opaque than their parent.

For the MenuKiller Control, that is a catastrophe, since it changes the opacity of children all the time in order to convey the hierarchical concept better.

So, what I came up with is a helper-method that does the whole thing manually… Any thoughts on this one?

I really don’t understand why this is not allowed, since it would make some things a lot easier, at a very low risk of objects that are too opaque.

2 – The need to remove storyboards

When testing the MenuKiller Control, I realized (not for the first time, but the first time it bugged me), that the XAML code I had did not work entirely correct.

The Button has a few triggers, including these:

<!-- exact storyboard omitted for clarity -->
<Trigger Property="IsMouseOver" SourceName="contentPresenter" Value="True">
  <Trigger.EnterActions>
    <BeginStoryboard x:Name="MouseOver_BeginStoryboard"
                     Storyboard="{StaticResource MouseOver}"/>
  </Trigger.EnterActions>
  <Trigger.ExitActions>
    <BeginStoryboard x:Name="MouseOut_BeginStoryboard"
                     Storyboard="{StaticResource MouseOut}"/>
  </Trigger.ExitActions>
</Trigger>
<Trigger Property="IsPressed"  Value="True">
  <Trigger.EnterActions>
    <BeginStoryboard x:Name="PressedOn_BeginStoryboard"
                     Storyboard="{StaticResource PressedOn}"/>
  </Trigger.EnterActions>
  <Trigger.ExitActions>
    <BeginStoryboard x:Name="PressedOff_BeginStoryboard"
                     Storyboard="{StaticResource PressedOff}"/>
  </Trigger.ExitActions>
</Trigger>

Expected Behaviour

The button has a trigger for the IsMouseOver property, so this should increase the opacity to 100% if hovering:
Screenshot of the desired hover effect.

Observed Behaviour

This works, but only as long the button is never clicked. If the button is clicked once, it will simply stay at its default 0.7 opacity. The only thing that still works is the animation for IsPressed.

Screenshot of non-hovering Button due to IsPressed Storyboard's ExitAction still pertaining.
(The red dot, by the way, is only a debugging tool, not a mistake)

I wondered how this could happen, trying to figure whether the button might behave like a ToggleButton for some reason. But it didn’t.

Instead, I realized that the trigger system, being based upon EnterActions and ExitActions, would have to preserve the controls’ state somehow. Several storyboards could easily cross each others paths, isn’t it? Interestingly, there is a property called HandoffBehaviour which I did not know until then. Unfortunately, the default value is already SnapshotAndReplace, while the only other option Compose was clearly not what I wanted.

As it turns out, the ExitAction, once applied, will stay on the control as long as the original enter condition is valid. Therefore, one simply has to remove the storyboard explicitly if another animation is supposed to be visible to the user. This yields the following XAML Code:

<Trigger Property="IsMouseOver" SourceName="contentPresenter" Value="True">
  <Trigger.EnterActions>
    <RemoveStoryboard BeginStoryboardName="PressedOff_BeginStoryboard" />
    <BeginStoryboard x:Name="MouseOver_BeginStoryboard"
                     Storyboard="{StaticResource MouseOver}"/>
  </Trigger.EnterActions>
  <Trigger.ExitActions>
    <RemoveStoryboard BeginStoryboardName="PressedOff_BeginStoryboard" />
    <BeginStoryboard x:Name="MouseOut_BeginStoryboard"
                     Storyboard="{StaticResource MouseOut}"/>
  </Trigger.ExitActions>
</Trigger>
<Trigger Property="IsPressed"  Value="True">
  <Trigger.EnterActions>
    <BeginStoryboard x:Name="PressedOn_BeginStoryboard"
                     Storyboard="{StaticResource PressedOn}"/>
  </Trigger.EnterActions>
  <Trigger.ExitActions>
    <BeginStoryboard x:Name="PressedOff_BeginStoryboard"
                     Storyboard="{StaticResource PressedOff}"/>
  </Trigger.ExitActions>
</Trigger>

What it accomplished, in the end, is the newest incarnation of the MenuKiller, here in Debug draw-mode:

Screenshot of the MenuKiller Control in a debug version.

Post to Twitter Post to Delicious Post to Digg Post to Facebook

Related posts:

  1. Applying ItemContainerStyle Recursively

Tags: ,

← Previous

Next →

Comments are closed.