I have the following storyboard:
<Window.Resources>
<Storyboard x:Key="ButtonsAnim">
<DoubleAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="topRightButton" Storyboard.TargetProperty="(UIElement.RenderTransform).(TransformGroup.Children)[3].(TranslateTransform.Y)">
<SplineDoubleKeyFrame KeyTime="00:00:00" Value="0"/>
<SplineDoubleKeyFrame KeyTime="00:00:00.2000000" Value="-100"/>
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="topRightButton" Storyboard.TargetProperty="(UIElement.RenderTransform).(TransformGroup.Children)[3].(TranslateTransform.X)">
<SplineDoubleKeyFrame KeyTime="00:00:00" Value="0"/>
<SplineDoubleKeyFrame KeyTime="00:00:00.2000000" Value="100"/>
...
It basically moves some buttons around in a canvas.
This is the code that starts the animation:
void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
Storyboard sb = (Storyboard)Resources["ButtonsAnim"];
storyBoard = sb;
storyBoard.Begin(this, true);
}
What I am trying to do is to reset the animation when I click a button which hides the window. When the window reappears the animation should start from the beginning.
I tried using storyBoard.Begin(this, true) when the application reappears but for this first milliseconds the buttons are at their last position.
I then tried storyBoard.seek(TimeSpan.Zero) before hiding the window but it fails:
System.Windows.Media.Animation
Warning: 6 : Unable to perform action
because the specified Storyboard was
never applied to this object for
interactive control.; Action='Seek';
Storyboard='System.Windows.Media.Animation.Storyboard';
Storyboard.HashCode='24901833';
Storyboard.Type='System.Windows.Media.Animation.Storyboard';
TargetElement='System.Windows.Media.Animation.Storyboard';
TargetElement.HashCode='24901833';
TargetElement.Type='System.Windows.Media.Animation.Storyboard'
I also tried storyBoard.remove(this) before hiding the window, same effect: the buttons are at their last position.
Any ideas?
Thank you.
To use StoryBoard.Remove() this way - you should keep reference to your storyboard object.
Like this:
Storyboard myStoryBoard;
void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
myStoryBoard = (Storyboard)Resources["myStoryBoard"];
myStoryBoard.Begin();
}
void sbRemoveEvent()
{
myStoryBoard.Remove();
}
I think Storyboard.Stop() should work here. But if you not find anything elegant, you can try reset buttons' transform after you've hided the window. E.g.:
((TranslateTransform)((TransformGroup)topRightButton.RenderTransform)[3]).X = 0;
((TranslateTransform)((TransformGroup)topRightButton.RenderTransform)[3]).Y = 0;
Hope I didn't make any mistake while casting.
NB: You may also find useful this example from MSDN: How to: Control a Storyboard After It Starts
Related
Trying to port to WPF some behavior from an old Silverlight application that allowed users to configure their view by moving/minimizing/maximizing various UserControls. Several StoryBoards were declared in UserControl.Resources and later accessed from code via their x:Name.
<Storyboard x:Name="MaximizeStoryboard" Completed="MaximizeStoryboard_Completed" Storyboard.TargetName="WholeControl">
<DoubleAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetProperty="(FrameworkElement.Width)">
<SplineDoubleKeyFrame x:Name="MaximizeWidth" KeyTime="00:00:00.3000000" Value="0"/>
</DoubleAnimationUsingKeyFrames>
...
In WPF, after changing x:Name to x:Key, I can access the Storyboard via:
Storyboard maximizeStoryboard = (Storyboard)Resources["MaximizeStoryboard"];
Previously used
MaximizeWidth.Value = Container.Contents.ActualWidth;
to set the restore width (and other values) from code, before beginning the animation. That doesn't work in WPF. What is the proper way to get access so those values can be set from code?
One of the possible way can be go through the children of Storyboard, find DoubleAnimationUsingKeyFrames child and then get the first KeyFrame.
Storyboard maximizeStoryboard = (Storyboard)Resources["MaximizeStoryboard"];
var doubleAnimationUsingKeyFrames = maximizeStoryboard.Children.OfType<DoubleAnimationUsingKeyFrames>().FirstOrDefault();
if (doubleAnimationUsingKeyFrames != null)
{
if (doubleAnimationUsingKeyFrames.KeyFrames.Count > 0)
{
var keyFrame = doubleAnimationUsingKeyFrames.KeyFrames[0] as SplineDoubleKeyFrame;
if(keyFrame !=null)
{
keyFrame.Value = Container.Contents.ActualWidth;
}
}
}
I subscribed to the wpf window's Loaded event: Loaded += loaded; and try to change the opacity of some controls in code behind.
I notice that in the method loaded the controls are not painted by wpf yet. So the code has no effect, the rendering of the controls occurs only after the method is exited.
1) Is there another event e.g. Rendered that I can subscribe to?
EDIT: I just discovered that there is an OnContentRendered event and the following code works:
Although an animation is probably preferrable.
protected override void OnContentRendered(EventArgs e)
{
base.OnContentRendered(e);
for (int i = 0; i < 100; i++)
{
Parentpanel.Opacity += 0.01;
Splashscreen.Opacity -= 0.01;
Dispatcher.Invoke(new Action(() => { }), DispatcherPriority.ContextIdle, null);
Thread.Sleep(50);
}
}
Otherwise I probably have to use an animation that changes the opacity of usercontrol1 from 0.1 to 1.0 and of usercontrol2 from 1.0 to 0.0.
2) Do you know an example for such an animation?
In your Loaded handler you can post the UI altering operation ( e.g. void ChangeOpacity() ) on the dispatcher:
Dispatcher.BeginInvoke(DispatcherPriority.Loaded, new Action(ChangeOpacity));
It will execute after the rendering is done.
Edit
I see you simply need an animation to start when the window opens. It's easily done in XAML, here's a working example generated in Blend:
<Window x:Class="WpfApplication3.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="100" Width="200">
<Window.Resources>
<Storyboard x:Key="myStoryboard">
<DoubleAnimationUsingKeyFrames
Storyboard.TargetProperty="(UIElement.Opacity)"
Storyboard.TargetName="myControl">
<EasingDoubleKeyFrame KeyTime="0:0:2" Value="0"/>
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</Window.Resources>
<Window.Triggers>
<EventTrigger RoutedEvent="FrameworkElement.Loaded">
<BeginStoryboard Storyboard="{StaticResource myStoryboard}"/>
</EventTrigger>
</Window.Triggers>
<StackPanel>
<TextBox x:Name="myControl" Text="I'm disappearing..." />
</StackPanel>
</Window>
I just recently was having issues while trying to render some standardized animation to WPF usercontrols when the visibility changed. In my app, I have a couple singleton static classes. In one, I added a static method "VisibleFader" and you pass in the framework element control and it automatically attaches the event handler to the double-animation against the opacity property. It works great and does not require ANY changes to any other Styles, Control Templates, or any other theme implementations.
public static DoubleAnimation da;
public static void VisibleFader(FrameworkElement fe)
{
if (da == null)
{
da = new DoubleAnimation();
da.From = 0;
da.To = 1;
da.Duration = new Duration(TimeSpan.FromSeconds(.7));
}
fe.IsVisibleChanged += myFader;
}
private static void myFader(object sender, DependencyPropertyChangedEventArgs e)
{
((FrameworkElement)sender).BeginAnimation(FrameworkElement.OpacityProperty, da);
}
Then, in my class (such as your Loaded Event), I just call this static method with that "userControl" object.
MySingletonClass.VisibleFader( this.whateverUserControl );
Done... So, when the visibility changes, it fades IN from nothing to 1. And if something was having its visibilty to hidden, it's gone anyhow.
I have a a custom Panel that raises a RoutedEvent which is defined globally in a static class :
public class CompressItemsToFitStackPanel : StackPanel
{
protected override Size ArrangeOverride(Size arrangeSize)
{
// some logic
// Raise Attached Event
CustomEventManager.RaiseArrangeEvent(this);
return base.ArrangeOverride(arrangeSize);
}
}
My attached Event:
public static class CustomEventManager
{
public static readonly RoutedEvent ArrangeEvent = EventManager.RegisterRoutedEvent("Arrange",
RoutingStrategy.Bubble,
typeof(RoutedEventHandler),
typeof(CustomEventManager));
internal static void RaiseArrangeEvent(UIElement target)
{
var args = new RoutedEventArgs(ArrangeEvent);
target.RaiseEvent(args);
}
}
this Panel is the items panel for an Items control ,
the ItemsTemplate as an EventTrigger which i wan't to fire when the attached event is raised:
<DataTemplate DataType="{x:Type local:Checker}" x:Key="CheckerTempalte">
<Ellipse x:Name="ellipse" Style="{StaticResource checkerStyle}">
<Ellipse.Triggers>
<EventTrigger RoutedEvent="local:CustomEventManager.Arrange">
<BeginStoryboard>
<Storyboard>
<DoubleAnimationUsingKeyFrames
Storyboard.TargetProperty="(UIElement.RenderTransform).(TransformGroup.Children)[3].(TranslateTransform.Y)"
Storyboard.TargetName="ellipse"
>
<EasingDoubleKeyFrame KeyTime="0:0:0" Value="0" />
<EasingDoubleKeyFrame KeyTime="0:0:1" Value="{Binding Val}" />
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Ellipse.Triggers>
</Ellipse>
The Event trigger is not triggered ,
Maybe i'm not using AttachedEvents correctly or it is not Declared correctly
i need the event from the panel to propagate and trigger the EventTriggers in the Child elements ,
any ideas what i'm doing wrong ?
EDIT :
After dkozl's insight i came to the conclusion that i need an AddXXXHandler and RemoveXXXHandler in order for the XAML to add/ remove the handler for the EventTrigger
public static void AddArrangeHandler(DependencyObject d, RoutedEventHandler handler)
{
UIElement uie = d as UIElement;
if (uie != null)
{
uie.AddHandler(CustomEventManager.ArrangeEvent, handler);
}
}
public static void RemoveArrangeHandler(DependencyObject d, RoutedEventHandler handler)
{
UIElement uie = d as UIElement;
if (uie != null)
{
uie.RemoveHandler(CustomEventManager.ArrangeEvent, handler);
}
}
but still nothing happens , and i never reach these methods .
EDIT 2 :
thanks to dkozl's comments below ,
the Event is raised for each child element , since he the ellipses are down the
Visual Tree .
protected override Size ArrangeOverride(Size arrangeSize)
{
foreach (UIElement child in children)
{
CustomEventManager.RaiseArrangeEvent(child);
}
}
but still nothing happens , iv'e also tested the 'StoryBoard' by giving the 'EventTrigger'
the 'MouseEnter' event and moving my mouse over the element , it works fine.
still , even raising the event on each Ellipse still does not work ...
any ideas ?
Another point of interest is that the child elements are actually the 'Checker' type
and not the Ellipse which the DataTemplate represents , i don't get how 'Checker' is considered a UIElement .?
What you've created is standard RoutedEvent, not an attached one. Add/Remove handler is different for attached events. You'll need two separate methods (AddArrangeChildrenHandler and RemoveArrangeChildrenHandler). It has been explained on MSDN site
UPDATE:
I've copied updated Ellipse definition and CustomEventManager class into my application, added test button which calls CustomEventManager.RaiseArrangeEvent(ellipse); when clicked and it works for me.
I also had to add Ellipse.RenderTransorm of TransformGroup, fourth transformation being TranslateTransform to make it work like in the example
UPDATE2: Event is raised on the panel where the Ellipse is placed which means that bubbling event would never reach it as it will start from the panel and go up the visual tree to Window never reaching children of the panel
You should maybe consider a code only approach to this problem. If you wish to define the animations in XAML you can possibly StaticResource them into properties on the CompressToFitStackPanel. But if your DataTemplate is referring to a specific transform by index you are definitely losing the power and abstraction of DataTemplate'ing.
Reevaluate your problem space and what the design is supposed to solve. Use ILSpy or Reflector to analyze how other Panels solved problems with properties or Attached Dependency Properties (Grid.Column, Grid.Row).
Place more of the burden on the new layout panel and less on those who use it.
Good luck.
My application has a few background worker, each doing different work. When I click the 'Start' button, all backgroundworker will start simultaneously.
in my xaml, I had defined my animation of a rotate image:
<window.Resources>
<Storyboard x:Key="imageRotate">
<DoubleAnimation Storyboard.TargetName="transRotate"
Storyboard.TargetProperty="(Image.RenderTransform).(RotateTransform.Angle)"
From="0" To="360"
Duration="0:0:0.5"
AutoReverse="False"
RepeatBehavior="Forever"/>
</Storyboard>
</window.Resources>
I want the animation to begin when all the backgroundworker started, and stop ONLY after ALL background worker stopped.
I have a property call AreWorkersBusy:
private bool _areWorkerBusy;
public bool AreWorkerBusy
{
get
{
return _areWorkerBusy;
}
set
{
bool isBusy = false;
foreach(BackgroundWorker worker in BackgroundWorkerList)
{
if(worker.IsBusy)
isBusy = true;
}
_areWorkerBusy = isBusy;
}
}
but it's not dependencyProperty, so I can't bind to my animation's DataTrigger.
Any workaround???
Help!
One of the possible ways,
You can implement INotifyPropertyChanged to notify 'AreWorkerBusy' changes, create a dependency property in the control and bind 'AreWorkerBusy' with it.
Create two routed events one to start animation and another to stop animation.
In the property changed handler for your DP, raise the specific routed event.
In your control write event triggers, and based on the event start of stop the animation.
I had written a similar experience, http://keepitsimpleengineer.blogspot.com/2010/09/wpf-circular-progress-control-part-2.html
Is it possible to have nested visual states. What I mean is if a ParentControl has a ChildControl and both have their own visual states, is it possible by setting the state of the ParentControl to change the state of the ChildControl accordingly.
You need to call the GoToState method to change the visual state of the child control.
Since you need to call a method you can't use Storyboards in visual state manager of a parent control since these can only animate properties.
Hence you need to write some code in the child control. To monitor the state of the parent and respond appropriately.
There are a number of different ways to do this but the crucial nugget of info is to use the VisualStateManager.GetVisualStateGroups method to find the VisualStateGroup on the parent that you are interested in, then attach to that group's CurrentStateChanging event. Hence code in the Child control can be notified when a state its interested in is being transitioned to by the parent and can call GoToState appropriately against itself.
I'm just going to declare a new dependency property:
public static readonly DependencyProperty StateProperty =
DependencyProperty.Register("State",
typeof( string ),
typeof( TextBlockControl ),
new PropertyMetadata("Top",
new PropertyChangedCallback(StateChanged)));
[Category("DigItOut"), Description("State")]
public string State
{
get
{
return this.GetValue(StateProperty).ToString();
}
set
{
this.SetValue(StateProperty, value);
}
}
private static void StateChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args)
{
if (!String.IsNullOrEmpty(args.NewValue.ToString()))
VisualStateManager.GoToState(sender as TextBlockControl, args.NewValue.ToString(), true);
}
And then just set it from it's parents state:
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="States">
<VisualState x:Name="Reverse">
<Storyboard>
<ObjectAnimationUsingKeyFrames BeginTime="00:00:00" Duration="00:00:00.0010000" Storyboard.TargetName="textBlockControl" Storyboard.TargetProperty="(TextBlockControl.State)">
<DiscreteObjectKeyFrame KeyTime="00:00:00">
<DiscreteObjectKeyFrame.Value>
<System:String>Bottom</System:String>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="Straight"/>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
But if I still want control over the use of transitions, then I'll have to find another solution. Probably second property.