Can I access a named fill gradient in a WPF Datatemplate? - wpf

I have a DataTemplate with a number of layered text and graphic objects. One of them is a glow effect that comes from the RadialGradientBrush Fill property of a Rectangle. At first, I named the Rectangle and bound to the Fill property and changed it using a DataTrigger. This worked fine, but I have a number of RadialGradientBrush objects in the Resources section and as you can see below, it is a lot to repeat when all I want to do is change the GradientStops. So I removed the Fill binding and added and named a RadialGradientBrush and although I can bind to the brush from Resources, I can't access it in the DataTrigger. I get the 'Cannot find Trigger target' error.
<Rectangle x:Name="Glow" IsHitTestVisible="False" RadiusX="1.5" RadiusY="1.5" VerticalAlignment="Stretch" HorizontalAlignment="Stretch" StrokeThickness="0" Opacity="1.0">
<Rectangle.Fill>
<RadialGradientBrush x:Name="GlowGradient" Center="0.5,0.848" GradientOrigin="0.5,0.818" RadiusX="-1.424" RadiusY="-0.622" GradientStops="{StaticResource DefaultGradient}">
<RadialGradientBrush.RelativeTransform>
<TransformGroup>
<ScaleTransform CenterX="0.5" CenterY="0.848" ScaleX="1" ScaleY="1.8"/>
<SkewTransform AngleX="0" AngleY="0" CenterX="0.5" CenterY="0.848"/>
<RotateTransform Angle="-33.418" CenterX="0.5" CenterY="0.848"/>
<TranslateTransform Y="0.278"/>
</TransformGroup>
</RadialGradientBrush.RelativeTransform>
</RadialGradientBrush>
</Rectangle.Fill>
</Rectangle>
In the resources, I have several RadialGradientBrush objects like this one.
<RadialGradientBrush x:Key="EscalatedGlow" Center="0.5,0.848" GradientOrigin="0.5,0.818" RadiusX="-1.424" RadiusY="-0.622">
<RadialGradientBrush.RelativeTransform>
<TransformGroup>
<ScaleTransform CenterX="0.5" CenterY="0.848" ScaleX="1" ScaleY="1.8"/>
<SkewTransform AngleX="0" AngleY="0" CenterX="0.5" CenterY="0.848"/>
<RotateTransform Angle="-33.418" CenterX="0.5" CenterY="0.848"/>
<TranslateTransform Y="0.278"/>
</TransformGroup>
</RadialGradientBrush.RelativeTransform>
<GradientStop Color="Aqua" Offset="0.168"/>
<GradientStop Color="#5E1D96FF" Offset="0.474"/>
<GradientStop Color="#1101FFFF" Offset="1"/>
</RadialGradientBrush>
I want to replace them with less code for each colour change, so I created some GradientStopCollection objects in the Resources to replace them with.
<GradientStopCollection x:Key="EscalatedGradient">
<GradientStop Color="Aqua" Offset="0.168"/>
<GradientStop Color="#5E1D96FF" Offset="0.474"/>
<GradientStop Color="#1101FFFF" Offset="1"/>
</GradientStopCollection>
Although I can bind to the Resource gradients, the problem is that I can't access the GlowGradient brush to change its GradientStops property. I could previously access the Glow Rectangle using a DataTrigger with the following.
<DataTrigger Binding="{Binding Path=Status}" Value="Escalated">
<Setter TargetName="Glow" Property="Fill" Value="{StaticResource EscalatedGlow}"/>
</DataTrigger>
When I use the following, I get the 'Cannot find Trigger target' error.
<DataTrigger Binding="{Binding Path=Status}" Value="Escalated">
<Setter TargetName="GlowGradient" Property="GradientStops" Value="{StaticResource EscalatedGradient}"/>
</DataTrigger>
I'm thinking there just has to be a way to save me from replicating the whole RadialGraientBrush each time I want to change the colours. Is there any way to access the Rectangle Fill brush from the DataTrigger? Any tips anyone? Thanks in advance.

In the end, I went with the following code:
<Rectangle Name="Glow" IsHitTestVisible="False" RadiusX="2.5" RadiusY="2.5"
Fill="{StaticResource OrangeGlow}" />
<Storyboard x:Key="GlowColourStoryboard" TargetName="Glow" Duration="0:0:1.5"
AutoReverse="True" BeginTime="0:0:0" RepeatBehavior="Forever">
<ColorAnimation Storyboard.TargetProperty="Fill.GradientStops[0].Color"
To="{StaticResource RedGradient.Colour1}" />
<ColorAnimation Storyboard.TargetProperty="Fill.GradientStops[1].Color"
To="{StaticResource RedGradient.Colour2}" />
<ColorAnimation Storyboard.TargetProperty="Fill.GradientStops[2].Color"
To="{StaticResource RedGradient.Colour3}" />
</Storyboard>
I haven't shown the Brush resources because... well you can make your own. This Storyboard is used in the following way and works as required:
<DataTrigger.EnterActions>
<BeginStoryboard Storyboard="{StaticResource GlowColourStoryboard}" />
</DataTrigger.EnterActions>

Related

Rotating Ellipse Around Center

I'm trying to build a generic loading control. I have the dummy control all set up with the gradient and masking, but I'm finding when I actually run it in a window it appears to rotate slightly off kilter. If you drop the below code into a user control, then drop that control into a window you should see the behavior I'm describing. I defined RenderTransformOrigin, so I'm a little confused as to why it's still not centering the rotation to the middle of the ellipse.
<UserControl x:Class="SpinningGradient.LoadingControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:SpinningGradient"
mc:Ignorable="d"
d:DesignHeight="300"
d:DesignWidth="300">
<Grid>
<Ellipse Stretch="Uniform"
RenderTransformOrigin=".5,.5">
<Ellipse.RenderTransform>
<RotateTransform x:Name="noFreeze" />
</Ellipse.RenderTransform>
<Ellipse.Triggers>
<EventTrigger RoutedEvent="Loaded">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="(Ellipse.RenderTransform).(RotateTransform.Angle)"
By="10"
To="360"
Duration="0:0:1"
RepeatBehavior="Forever" />
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Ellipse.Triggers>
<Ellipse.Fill>
<RadialGradientBrush RadiusX="0.5"
RadiusY="0.5">
<RadialGradientBrush.GradientOrigin>
<Point X=".9"
Y=".9" />
</RadialGradientBrush.GradientOrigin>
<RadialGradientBrush.Center>
<Point X="0.5"
Y="0.5" />
</RadialGradientBrush.Center>
<RadialGradientBrush.GradientStops>
<GradientStop Color="Blue"
Offset="1" />
<GradientStop Color="Red"
Offset="-.5" />
</RadialGradientBrush.GradientStops>
</RadialGradientBrush>
</Ellipse.Fill>
<Ellipse.OpacityMask>
<DrawingBrush>
<DrawingBrush.Drawing>
<GeometryDrawing>
<GeometryDrawing.Brush>
<SolidColorBrush Color="Black" />
</GeometryDrawing.Brush>
<GeometryDrawing.Geometry>
<GeometryGroup FillRule="EvenOdd">
<RectangleGeometry Rect="0,0,100,100" />
<EllipseGeometry RadiusX="30"
RadiusY="30"
Center="50,50" />
</GeometryGroup>
</GeometryDrawing.Geometry>
<GeometryDrawing.Pen>
<Pen Thickness="0"
Brush="Black" />
</GeometryDrawing.Pen>
</GeometryDrawing>
</DrawingBrush.Drawing>
</DrawingBrush>
</Ellipse.OpacityMask>
</Ellipse>
</Grid>
</UserControl>
The UserControl below creates a very similar or the same visual result with a lot less XAML. The ratio between StrokeThickness and RadiusX/RadiusY determines the relative stroke width.
<UserControl ...>
<Viewbox>
<Path StrokeThickness="1" Stretch="Uniform" RenderTransformOrigin="0.5,0.5">
<Path.Triggers>
<EventTrigger RoutedEvent="Loaded">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation
Storyboard.TargetProperty="RenderTransform.Angle"
To="360" Duration="0:0:1" RepeatBehavior="Forever"/>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Path.Triggers>
<Path.RenderTransform>
<RotateTransform/>
</Path.RenderTransform>
<Path.Data>
<EllipseGeometry RadiusX="2" RadiusY="2"/>
</Path.Data>
<Path.Stroke>
<RadialGradientBrush GradientOrigin="0.9,0.9">
<RadialGradientBrush.GradientStops>
<GradientStop Color="Blue" Offset="1" />
<GradientStop Color="Red" Offset="-0.5" />
</RadialGradientBrush.GradientStops>
</RadialGradientBrush>
</Path.Stroke>
</Path>
</Viewbox>
</UserControl>
The reason for that is that even though your Ellipse looks like a circle, it is still stretching to the size of its container (or, more precisely, to the size of the LoadingControl). So, unless the said LoadingControl is contained within a perfect square, the Ellipse's actual center point is going to be offset from the center point of the visible circle, either horizontally or vertically. And around that actual center point (rather than the apparent center point) the Ellipse is rotated.
#Clemens already gave you a handful of options to remedy this situation in the comments section, but let me quickly go over them:
Set Width and Height of your Ellipse to the same value - this is limiting since you have to hard-code the size of your control
Bind the Width to the ActualHeight of the Ellipse (or the other way around) - this gives you some extent of dynamic behavior, but things go bad if the target dimension constraint is smaller than the source dimension constraint (e.g. if you bind Width to ActualHeight, things go bad when your control is contained within a rectangle with its width smaller than its height - the Ellipse is clipped)
Put the Ellipse in a square container - or more precisely, within a container that would always arrange it in a square
I strongly recommend the last approach, since it gives you full flexibility in sizing your control. Now I don't think there's a component shipped with WPF that could accomplish this task, but it is fairly simple to devise one of your own, and I guarantee from my experience, that it'll become a useful tool in your toolbox. Here's an example implementation, which is based on a Decorator:
public class SquareDecorator : Decorator
{
protected override Size ArrangeOverride(Size arrangeSize)
{
if (Child != null)
{
var paddingX = 0d;
var paddingY = 0d;
if (arrangeSize.Width > arrangeSize.Height)
paddingX = (arrangeSize.Width - arrangeSize.Height) / 2;
else
paddingY = (arrangeSize.Height - arrangeSize.Width) / 2;
var rect = new Rect
{
Location = new Point(paddingX, paddingY),
Width = arrangeSize.Width - 2 * paddingX,
Height = arrangeSize.Height - 2 * paddingY,
};
Child.Arrange(rect);
}
return arrangeSize;
}
protected override Size MeasureOverride(Size constraint)
{
var desiredSize = new Size();
if (Child != null)
{
Child.Measure(constraint);
var max = Math.Min(constraint.Width, constraint.Height);
var desired = Math.Max(Child.DesiredSize.Width, Child.DesiredSize.Height);
desiredSize.Width = desiredSize.Height = Math.Min(desired, max);
}
return desiredSize;
}
}
Then you only need to slightly modify your control:
<UserControl (...)>
<local:SquareDecorator>
<Ellipse (...) />
</local:SquareDecorator>
</UserControl>

how to start the animation if the storyboard definition and the corresponding grid inside canvas is in windows.resources tag

i want to start animation in the grid.the grid is inside the canvas tag.both the story board definition and the grid to animate is inside the windows.resources tag.
the code is given below
<VisualBrush x:Key="Passport" x:Shared="false" Stretch="Uniform">
<VisualBrush.Visual>
<Canvas RenderTransformOrigin="0.5,0.5" >
<Grid Margin="5.397,45.106,5.239,0" VerticalAlignment="Top" Height="7.801" x:Name="side_scan_strip" RenderTransformOrigin="0.5,0.5">
<Grid.RenderTransform >
<TransformGroup>
<ScaleTransform ScaleX="1" ScaleY="1"/>
<SkewTransform AngleX="0" AngleY="0"/>
<RotateTransform Angle="0"/>
<TranslateTransform X="0" Y="0"/>
</TransformGroup>
</Grid.RenderTransform>
</Grid>
</Canvas>
</VisualBrush.Visual>
</VisualBrush>
<Storyboard x:Key="Storyboard1" x:Name="Storyboard1">
<DoubleAnimationUsingKeyFrames x:Name="dak1" BeginTime="00:00:00" Storyboard.TargetName=" side_scan_strip" Storyboard.TargetProperty="(UIElement.RenderTransform).(TransformGroup.Children)[3].(TranslateTransform.Y)">
<SplineDoubleKeyFrame KeyTime="00:00:01" Value="109.75"/>
<SplineDoubleKeyFrame KeyTime="00:00:02" Value="0"/>
<SplineDoubleKeyFrame KeyTime="00:00:03" Value="109.75"/>
<SplineDoubleKeyFrame KeyTime="00:00:04" Value="0"/>
</DoubleAnimationUsingKeyFrames>
</Storyboard>
the exception i got is :
' side_scan_strip' name cannot be found in the name scope of 'WpfApplication1.MainWindow'.
It might be because of a little error :) In the Storyboard you have written: " side_scan_strip" while it should be "side_scan_strip". See the little space in the beginning of the first? I think that is what is triggering the exception.

Image transition animation on ribbon button

How can I create a WPF ribbon button switching between two images, with an animated "wipe" transition between images? I can't place both images within a a grid within the button and animate the opacity of each image in turn (as suggested here) because I can't set the content of the ribbon directly, only the LargeImageSource/SmallImageSource and Label properties.
Update
I tried BorisB.'s suggestion together with the animation from the link above, but there is now no image displayed in the ribbon button. Removing the animation, opacity mask and multiple images, and leaving the following code also doesn't show the image at all.
<RibbonToggleButton Label="Dashboard" Name="btnDashboard" IsChecked="True">
<RibbonToggleButton.LargeImageSource>
<DrawingImage>
<DrawingImage.Drawing>
<DrawingGroup>
<ImageDrawing ImageSource="/Icons/Dashboard.png" />
</DrawingGroup>
</DrawingImage.Drawing>
</DrawingImage>
</RibbonToggleButton.LargeImageSource>
</RibbonToggleButton>
You can use a DrawingImage as an ImageSource. Then you can assign a DrawingGroup as a DrawingImage.Drawing. That drawing group can contain two ImageDrawings wrapped in their own DrawingGroups, so you could apply the approach from your link:
<Grid>
<Ribbon>
<RibbonTab x:Name="HomeTab" Header="Home">
<RibbonGroup x:Name="Group1" Header="Group1">
<RibbonButton x:Name="Button1" Label="Button1">
<RibbonButton.LargeImageSource>
<DrawingImage>
<DrawingImage.Drawing>
<DrawingGroup>
<DrawingGroup>
<ImageDrawing Rect="0, 0, 32, 32" ImageSource=ImageOne.png"/>
</DrawingGroup>
<DrawingGroup>
<ImageDrawing Rect="0, 0, 32, 32" ImageSource="ImageTwo.png"/>
<DrawingGroup.OpacityMask>
<LinearGradientBrush StartPoint="0,0" EndPoint="1,0">
<GradientStop Offset="0" Color="Black" x:Name="BlackStop"/>
<GradientStop Offset="0" Color="Transparent" x:Name="TransparentStop"/>
</LinearGradientBrush>
</DrawingGroup.OpacityMask>
</DrawingGroup>
</DrawingGroup>
</DrawingImage.Drawing>
</DrawingImage>
</RibbonButton.LargeImageSource>
</RibbonButton>
</RibbonGroup>
</RibbonTab>
</Ribbon>
</Grid>
<Window.Triggers>
<EventTrigger RoutedEvent="Window.Loaded">
<EventTrigger.Actions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetName="TransparentStop"
Storyboard.TargetProperty="Offset" By="1" Duration="0:0:2" />
<DoubleAnimation Storyboard.TargetName="BlackStop"
Storyboard.TargetProperty="Offset" By="1" Duration="0:0:2"
BeginTime="0:0:0.05" />
</Storyboard>
</BeginStoryboard>
</EventTrigger.Actions>
</EventTrigger>
</Window.Triggers>
The wrapping DrawingGroups are used so you can use OpacityMask, which is essential for the effect.

Xaml transformation keep existing transformations non-affected transformations

I modified a toggle switch so I can use an icon ontop of it. Then I added a custom property which holds the image information and it's rotation. The definition of my image looks somewhat like this:
<Image x:Name="SwitchKnobActive" Source="{Binding Path=(common:FilterSwitchImageHolder.ActiveImage), RelativeSource={RelativeSource TemplatedParent}}" Width="50" Visibility="{Binding IsOn, RelativeSource={RelativeSource Mode=TemplatedParent}, Converter={StaticResource BoolToVisibilityConverter}, ConverterParameter=True}" RenderTransformOrigin="0.5, 0.5">
<Image.RenderTransform>
<TransformGroup>
<TranslateTransform x:Name="KnobActiveTranslateTransform"/>
<RotateTransform Angle="{Binding Path=(common:FilterSwitchImageHolder.Angle), RelativeSource={RelativeSource TemplatedParent}}" />
</TransformGroup>
</Image.RenderTransform>
</Image>
Now when I select the image I want to scale it by the factor of two, when I do this I loose the rotation. I tried setting the rotation again like above but it got ignored.. Is there a way how I can keep an existing transformation?
Edit:
The transformation on click XAML:
<VisualState x:Name="Pressed">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="RenderTransform" Storyboard.TargetName="SwitchKnobActive">
<DiscreteObjectKeyFrame KeyTime="0">
<DiscreteObjectKeyFrame.Value >
<ScaleTransform ScaleX="2" ScaleY="2"></ScaleTransform>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
FYI: All this code is within a control template of a toggle switch.
How are you applying scale transform?
Usual pattern for this would be to add dummy scale transform to the transform group and modify it later. This way all other transforms are not being replaced.
<Image.RenderTransform>
<TransformGroup>
<ScaleTransform x:Name="myScaleTransform" ScaleX="1.0" ScaleY="1.0">
<TranslateTransform x:Name="KnobActiveTranslateTransform"/>
<RotateTransform Angle="{Binding Path=(common:FilterSwitchImageHolder.Angle), RelativeSource={RelativeSource TemplatedParent}}" />
</TransformGroup>
</Image.RenderTransform>
You can later access it from xaml storyboard like this:
<DoubleAnimationUsingKeyFrames BeginTime="00:00:00"
Storyboard.TargetName="SwitchKnobActive"
Storyboard.TargetProperty="(UIElement.RenderTransform).(TransformGroup.Children)[0].(ScaleTransform.ScaleX)">
<SplineDoubleKeyFrame KeyTime="00:00:00" Value="1"/>

Troubles with GoToStateAction

I have had several problems with the GoToStateAction in different scenarios, and I'm beginning to believe that either the feature is buggy, or that my understanding of it is off.
In this case, I have a datatemplate with an ellipse that is representing a connector. The connector has an IsConnected property... I am using VisualStates and the GoToStateAction with a DataTrigger to switch between the 2 states 'Connected' and 'NotConnected'. However, in this case the state is never set.
I know the model is set up correctly, as trying other binding scenarios with IsConnected works fine. What am I doing wrong?
<DataTemplate x:Key="ConnectorTemplate">
<Grid x:Name="grid">
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="ConnectionStates">
<VisualState x:Name="Connected">
<Storyboard>
<ColorAnimation Duration="0" To="#FFEAFFDD" Storyboard.TargetProperty="(Shape.Fill).(GradientBrush.GradientStops)[1].(GradientStop.Color)" Storyboard.TargetName="ellipse" d:IsOptimized="True"/>
<ColorAnimation Duration="0" To="#FF56992B" Storyboard.TargetProperty="(Shape.Fill).(GradientBrush.GradientStops)[0].(GradientStop.Color)" Storyboard.TargetName="ellipse" d:IsOptimized="True"/>
</Storyboard>
</VisualState>
<VisualState x:Name="NotConnected"/>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<VisualStateManager.CustomVisualStateManager>
<ei:ExtendedVisualStateManager/>
</VisualStateManager.CustomVisualStateManager>
<Ellipse x:Name="ellipse"
Height="8"
Width="8">
<i:Interaction.Triggers>
<ei:DataTrigger Binding="{Binding IsConnected}" Value="true">
<ei:GoToStateAction StateName="Connected"/>
</ei:DataTrigger>
<ei:DataTrigger Binding="{Binding IsConnected}" Value="false">
<ei:GoToStateAction StateName="NotConnected"/>
</ei:DataTrigger>
</i:Interaction.Triggers>
<Ellipse.Fill>
<RadialGradientBrush Center="0.275,0.262"
GradientOrigin="0.275,0.262"
RadiusX="0.566"
RadiusY="0.566">
<GradientStop Color="#FF333333"
Offset="1" />
<GradientStop Color="#FFC4C4C4" />
</RadialGradientBrush>
</Ellipse.Fill>
</Ellipse>
</Grid>
</DataTemplate>
I think you should set TargetName in GoToStateAction, because by default,if my memory serves me right, Target is associated with GoToStateAction object, in your case - ellipse
GoToStateAction is not triggered when the item is loaded, it only comes in play when the related property is changed (PropertyChanged event is fired).

Resources