正如上一章介紹,WPF影片通過一組影片類(Animation類)表示,使用少數幾個熟悉設定相關資訊,如開始值、結束值以及持續時間,這顯然使得它們非常適合于XAML,不是很清晰的時:如何為特定的事件和屬性關聯影片,以及如何在正確的時間觸發影片,
在所有宣告式影片中都會用到如下兩個要素:
- 故事板,故事板是BeginAnimation()方法的XAML等價物,通過故事板將影片指定到合適的元素和屬性,
- 事件觸發器,事件觸發器回應屬性變化或事件(如按鈕的Click事件),并控制故事板,例如,為了開始影片,事件觸發器必須開始故事板,
一、故事板
故事板是增強的事件線,可用來分組多個影片,而且具有控制影片播放的能力——暫停、停止以及改變播放位置,然而,Storyboard類提供的最基本功能是,能夠使用TargetProperty和TargetName屬性指向某個特定屬性和特定元素,換句話說,故事板在影片和希望應用影片的屬性之間架起了一座橋梁,
下面的標記演示了如何定義用于管理DoubleAnimation的故事板:
<Storyboard TargetName="cmdGrow" TargetProperty="Width"> <DoubleAnimation From="160" To="300" Duration="0:0:5"></DoubleAnimation> </Storyboard>
TargetName和TargetProperty都是附加屬性,這意味著可以直接將他們應用于影片,如下所示:
<Storyboard > <DoubleAnimation Storyboard.TargetName="cmdGrow" Storyboard.TargetProperty="Width" From="160" To="300" Duration="0:0:5"> </DoubleAnimation> </Storyboard>
上面的語法更常用,因為通過這種語法可在同一個故事板中放置幾個影片,并且每個影片可用于不同的元素和屬性,
定義故事板是創建影片的第一步,為讓故事板實際運行起來,還需要有事件觸發器,
二、事件觸發器
在“【WPF學習】第三十七章 觸發器 ”時第一次提到事件觸發器,樣式提供了一種將事件觸發器關聯到元素的方法,然而,可在如下4個位置定義事件觸發器:
- 在樣式中(Styles.Triggers集合)
- 在資料目標中(DataTemplate.Triggers集合)
- 在控制元件模板中(ControlTemplate.Triggers集合)
- 直接在元素中定義事件觸發器(FrameworkElement.Triggers集合)
當創建事件觸發器時,需要制定開始出發其的路由事件和由觸發器執行的一個或多個動作,對于影片,最常用的動作是BeginStoryboard,該動作相當于呼叫BeginAnimation()方法,
下面的示例使用按鈕的Triggers集合為Click事件關聯某個影片,當單擊按鈕時,該影片增長按鈕:
<Button Margin="10" Name="cmdGrow" Height="40" Width="160" HorizontalAlignment="Center" VerticalAlignment="Center"> <Button.Triggers> <EventTrigger RoutedEvent="Button.Click"> <BeginStoryboard> <Storyboard> <DoubleAnimation Storyboard.TargetProperty="Width" To="300" Duration="0:0:5"> </DoubleAnimation> </Storyboard> </BeginStoryboard> </EventTrigger> </Button.Triggers> <Button.Content> Click and Make Me Grow </Button.Content> </Button>
Storyboard.TargetProperty屬性指定了希望改變的屬性(在這個示例中是Width屬性),如果沒有提供類的名稱,故事板使用其父元素,在此使用的是希望擴展的按鈕,如果希望設定附加屬性(如Canvas.Left或Canvas.Top),需要在括號中封裝整個屬性,如下所示:
<DoubleAnimation Storyboard.TargetName="(Canvas.Top)" .../>
在這個示例中需不需要使用Storyboard.TargetName屬性,當忽略該屬性時,故事板使用父元素,在此是按鈕,
在這個示例中使用的宣告式方法和前面演示的只使用代碼的方法存在如下區別:To值被硬編碼為300個單位,而不是相對于包含按鈕的視窗的尺寸設定,如果希望使用視窗寬度,需要使用資料系結運算式,如下所示:
<DoubleAnimation Storyboard.TargetProperty="Width" To="{Binding ElementName=cmdGrow, Path=Width}" Duration="0:0:5"> </DoubleAnimation>
這仍不能準確地得到所希望的結果,在此,按鈕從當前尺寸增大到視窗的完整寬度,只使用代碼的方法使用一種簡單的計算,將按鈕擴大到比整個視窗寬度小30個單位的值,但XAML不支持行內計算,一種解決方法是構建能夠自動完成作業的IValueConverter介面,如下所示的示例:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; using System.Windows.Data; namespace Animation { public class ArithmeticConverter : IValueConverter { private const string ArithmeticParseExpression = "([+\\-*/]{1,1})\\s{0,}(\\-?[\\d\\.]+)"; private Regex arithmeticRegex = new Regex(ArithmeticParseExpression); public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { if (value is double && parameter != null) { string param = parameter.ToString(); if (param.Length > 0) { Match match = arithmeticRegex.Match(param); if (match != null && match.Groups.Count == 3) { string operation = match.Groups[1].Value.Trim(); string numericValue = https://www.cnblogs.com/Peter-Luo/p/match.Groups[2].Value; double number = 0; if (double.TryParse(numericValue, out number)) // this should always succeed or our regex is broken { double valueAsDouble = (double)value; double returnValue = https://www.cnblogs.com/Peter-Luo/p/0; switch (operation) { case "+": returnValue = valueAsDouble + number; break; case "-": returnValue = valueAsDouble - number; break; case "*": returnValue = valueAsDouble * number; break; case "/": returnValue = valueAsDouble / number; break; } return returnValue; } } } } return null; } public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { throw new NotImplementedException(); } } }ArithmeticConverter
<Window x:Class="Animation.XamlAnimation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:Animation" Title="XamlAnimation" Height="300" Width="300"> <Window.Resources> <local:ArithmeticConverter x:Key="converter"></local:ArithmeticConverter> </Window.Resources> <Button Padding="10" Name="cmdGrow" Height="40" Width="160" HorizontalAlignment="Center" VerticalAlignment="Center"> <Button.Triggers> <EventTrigger RoutedEvent="Button.Click"> <EventTrigger.Actions> <BeginStoryboard> <Storyboard> <DoubleAnimation Storyboard.TargetProperty="Width" To="{Binding ElementName=window,Path=Width,Converter={StaticResource converter},ConverterParameter=-30}" Duration="0:0:5"></DoubleAnimation> <DoubleAnimation Storyboard.TargetProperty="Height" To="{Binding ElementName=window,Path=Height,Converter={StaticResource converter},ConverterParameter=-50}" Duration="0:0:5"></DoubleAnimation> </Storyboard> </BeginStoryboard> </EventTrigger.Actions> </EventTrigger> </Button.Triggers> <Button.Content> Click and Make Me Grow </Button.Content> </Button> </Window>XamlAnimation
使用樣式關聯觸發器
FrameworkElement.Triggers集合有點奇怪,它僅支持事件觸發器,其他觸發器集合(Style.Triggers、DataTemplate.Triggers與ControlTemplate.Triggers)的功能更強大,他們支持三種基本型別的WPF觸發器:屬性觸發器、資料觸發器以及事件觸發器,
使用事件觸發器是關聯影片的最常用方式,但并不是唯一的選擇,如果使用位于樣式、資料模板或控制元件模板中的Triggers集合,還可創建當屬性值發生變化時進行回應的屬性觸發器,例如,下面的樣式復制了前面顯示的示例,當IsPressed屬性為true時,該樣式觸發一個故事板:
<Window.Resources> <Style x:Key="GrowButtonStyle"> <Style.Triggers> <Trigger Property="Button.IsPressed" Value="True"> <Trigger.EnterActions> <BeginStoryboard> <Storyboard> <DoubleAnimation Storyboard.TargetProperty="Width" To="250" Duration="0:0:5"></DoubleAnimation> </Storyboard> </BeginStoryboard> </Trigger.EnterActions> </Trigger> </Style.Triggers> </Style> </Window.Resources>
可使用兩種方式為屬性觸發器關聯動作,可使用Trigger.EnterActions設定當屬性改變到指定的數值時希望執行的動作(在上面的示例中,當IsPressed屬性值變為true時),也可以使用Trigger.ExitActions設定當屬性改變回原來的數值時執行的動作(當IsPressed屬性的值變回false時),這是一種封裝一堆互補影片的簡便方法,
下面的按鈕使用上面顯示的樣式:
<Button Padding="10" Name="cmdGrow" Height="40" Width="160" Style="{StaticResource GrowButtonStyle}" HorizontalAlignment="Center" VerticalAlignment="Center"> Click and Make Me Grow </Button>
請記住,不見得在樣式中使用屬性觸發器,也可使用事件觸發器,就像在前面介紹的那樣,最后,不見得以與使用樣式的按鈕相分離的方式定義樣式(也可使用行內樣式設定Button.Style屬性),但是這種兩部分相分離的方法更常用,并且提供了為多個元素應用相同的靈活性,
三、重疊影片
故事板提供了改變處理重疊影片方式的能力——換句話說,決定第二個影片何時被應用到已經具有一個正在運行的影片的屬性上,可使用BeginStoryboard.HandoffBehavior屬性改變處理重疊影片的方式,
通常,當兩個影片相互重疊時,第二個影片會立即覆寫第一個影片,這種行為就是所謂的“快照并替換”(由HandoffBehavior列舉中的SnapshotAndReplace值表示),當第二個影片開始時,第二個影片獲取屬性當前值(基于第一個影片)的快照,停止影片,并用新影片替換第一個影片,
另一個HandoffBehavior選項是Compose,這種方式將第二個影片融合到第一個影片的時間線中,例如,分析ListBox示例的修改版本,當縮小按鈕時使用HandoffBehavior.Compose:
<EventTrigger RoutedEvent="ListBoxItem.MouseLeave"> <EventTrigger.Actions> <BeginStoryboard HandoffBehavior="Compose"> <Storyboard> <DoubleAnimation Storyboard.TargetProperty="FontSize" BeginTime="0:0:0.5" Duration="0:0:0.2"></DoubleAnimation> </Storyboard> </BeginStoryboard> </EventTrigger.Actions> </EventTrigger>
現在,如果將滑鼠移到ListBoxItem物件上,然后在移開,將看到不同的行為,當滑鼠移開項時,項會繼續擴張,這種行為非常明顯,知道第二個影片到達其0.5秒得開始時間延遲,然后,第二個影片會縮小按鈕,如果不使用Compose行為,在第二個影片開始之前的0.5秒得時間間隔內,按鈕會處于等待狀態,并固定為當前尺寸,
使用組合的HandoffBehavior行為需要更大開銷,這是因為當第二個影片開始時,用于運行原來影片的時鐘不能被釋放,相反,這個時鐘會繼續保持存活,知道ListBoxItem物件被垃圾回識訓為相同的屬性應用新的影片為止,
四、同步的影片
Storyboard類間接地繼承自TimelineGroup類,所以Storyboard類能包含多個影片,最令人高興的是,這些影片可以作為一組進行管理——這意味著他們在同一時間開始,
為查看這個一個示例,分析下面的故事板,它開始兩個影片,一個影片用于按鈕的Width屬性,而另一個影片用于按鈕的Height屬性,因為影片被分組到故事板中,它們共同增加按鈕的尺寸,所以可得到比在代碼中通過簡單地多次呼叫BeginAnimation()方法得到的效果更趨向同步的效果,
<EventTrigger RoutedEvent="Button.Click"> <EventTrigger.Actions> <BeginStoryboard> <Storyboard> <DoubleAnimation Storyboard.TargetProperty="Width" To="300" Duration="0:0:5"></DoubleAnimation> <DoubleAnimation Storyboard.TargetProperty="Height" To="300" Duration="0:0:5"></DoubleAnimation> </Storyboard> </BeginStoryboard> </EventTrigger.Actions> </EventTrigger>
在這個示例中,兩個影片具有相同的持續時間,但這并不是必須的,對于在不同時間結束的影片,唯一需要考慮的是它們的FillBehavior行為,如果一個影片的FillBehavior屬性被設定為HoldEnd,它會保持值直到故事板中所有的影片都結束,如果故事板的FillBehavior屬性是HoldEnd,最后那個影片的值將被永久保存(直到使用新的影片替換這個影片或手動洗掉了這個影片),
上一章列出的Timeline類的屬性開始變得特別有用,例如,可通過SpeedRatio屬性使故事板中的某個影片比其他影片更快,也可以使用BeginTime屬性相對于一個影片來編譯另一個影片的開始時間,使該影片在特定的時間點開始,
五、控制播放
到目前位置,已在事件觸發器中使用了一個動作——加載影片的BeginStoryboard動作,然而,一旦創建故事板,就可以用在其他動作控制故事板,這些作業類都繼承自ControllableStoryboardAction類,下表列出了這些類,
表 控制故事板的動作類

幫助檔案中沒有記載會妨礙使用這些動作的內容,為成功地執行這些動作,必須在同一個Triggers集合中定義所有觸發器,如果將BeginStoryboard動作的觸發器和PauseStoryboard動作的觸發器放置到不同集合中,PauseStoryboard動作就無法作業,為查看需要使用的設計,分析示例是有幫助的,
例如,分析下圖中顯示的視窗,該視窗使用一個網格在完全相同的位置精確地重疊了兩個Image元素,最初,只有最頂部的影像可見,但當影片運行是,該影像從1到0逐漸地增加透明度,最終使夜間的場景完全蓋過白天場景,效果就像是影像從白天變換到黑夜,就像連續的隨時間流逝的照片,

下面的標記定義了包含兩個影像的Grid控制元件:
<Grid> <Image Source="night.jpg"></Image> <Image Source="day.jpg" Name="imgDay"></Image> </Grid>
下面是從一幅影像淡入到另一幅影像的影片:
<DoubleAnimation Storyboard.TargetName="imgDay" Storyboard.TargetProperty="Opacity" From="1" To="0" Duration="0:0:10"></DoubleAnimation>
為增加這個示例的趣味性,還在底部提供了幾個用于控制影片播放的按鈕,使用這些按鈕,可執行典型的媒體播放器動作,如暫停、恢復播放以及停止(可添加其他按鈕來改變速度系數以及挑選特定的時間),
下面的標記定義了這些按鈕:
<StackPanel Grid.Row="1" Orientation="Horizontal" HorizontalAlignment="Center"> <Button Name="cmdStart">Start</Button> <Button Name="cmdPause">Pause</Button> <Button Name="cmdResume">Resume</Button> <Button Name="cmdStop">Stop</Button> <Button Name="cmdMiddle">Move To Middle</Button> </StackPanel>
通常,可選擇在每個按鈕的Triggers集合中放置事件觸發器,然而,在前面已解釋過,對于影片這種方法不能作業,最簡單的解決方法是在一個地方定義所有事件觸發器,例如,在包含元素的Triggers集合中,使用EventTrigger.SourceName屬性關聯這些事件觸發器,只要SourceName屬性和為按鈕設定的Name屬性相匹配,觸發器就會應用到恰當的按鈕上,
這個示例中,可使用包含這些按鈕的StackPanel面板的Triggers集合,然而,使用頂級元素(在這個示例中是視窗)的Triggers集合通常最簡單,這樣,就可在用戶界面中將按鈕移到不同的位置,而不會禁用他們的功能,
<Window.Triggers> <EventTrigger SourceName="cmdStart" RoutedEvent="Button.Click"> <BeginStoryboard Name="fadeStoryboardBegin"> <Storyboard> <DoubleAnimation Storyboard.TargetName="imgDay" Storyboard.TargetProperty="Opacity" From="1" To="0" Duration="0:0:10"></DoubleAnimation> </Storyboard> </BeginStoryboard> </EventTrigger> <EventTrigger SourceName="cmdPause" RoutedEvent="Button.Click"> <PauseStoryboard BeginStoryboardName="fadeStoryboardBegin"> </PauseStoryboard> </EventTrigger> <EventTrigger SourceName="cmdResume" RoutedEvent="Button.Click"> <ResumeStoryboard BeginStoryboardName="fadeStoryboardBegin"></ResumeStoryboard> </EventTrigger> <EventTrigger SourceName="cmdStop" RoutedEvent="Button.Click"> <StopStoryboard BeginStoryboardName="fadeStoryboardBegin"></StopStoryboard> </EventTrigger> <EventTrigger SourceName="cmdMiddle" RoutedEvent="Button.Click"> <SeekStoryboard BeginStoryboardName="fadeStoryboardBegin" Offset="0:0:5"></SeekStoryboard> </EventTrigger> </Window.Triggers>
注意,必須為BeginStoryboard動作指定名稱(在這個示例中,名稱是fadeStoryboardBegin),其他觸發器通過為BeginStoryboardName屬性指定這個名稱,連接到相同的故事板,
當使用故事板動作時將遇到限制,他們提供的屬性(如SeekStoryboard.Offset和SetStoryboardSpeedRatio.SpeedRatio屬性)不是依賴性項屬性,這會限制使用資料系結運算式,例如,不能自動讀取Slider.Value屬性值并將其應用到SetStoryboardSpeedRatio.SpeedRatio動作,因為SpeedRatio屬性不接受資料系結運算式,可能認為通過使用Storyboard物件的SpeedRatio屬性來解決這個問題,但這是行不同的,當影片開始時,讀取SpeedRatio值并創建一個影片時鐘,此后,即使改變了SpeedRatio屬性的值,影片也仍會保持正常的速度,
如果希望動態調整速度或位置,唯一的解決方法是使用代碼,Storyboard類中的方法提供了與故事板觸發器相同的功能,包括Begin()、Pause()、Resume()、Seek()、Stop()、SkipToFill()、SetSpeedRatio()以及Remove()方法,
要訪問Storyboard物件,必須在標記中設定其Name屬性:
<Storyboard Name="fadeStoryboard">
現在只需要撰寫恰當的事件處理程式,并使用Storyboard物件的方法(請記住,簡單地改變故事板的屬性(比如SpeedRatio)是沒有任何效果的,它們僅配置當影片開始時將要使用的設定),
當拖動Slider控制元件上的滑塊時,下面的事件處理程式會進行回應,該事件處理程式獲取滑動條的值(范圍是0~3),并使用該數值應用新的速率:
private void sldSpeed_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e) { fadeStoryboard.SetSpeedRatio(this, sldSpeed.Value); }
注意,SetSpeedRatio()方法需要兩個引數,第一個引數是頂級影片容器(在這個示例中,是指當前視窗),所有故事板方法都需要這個參考,第二個引數是新的速率,
六、監視影片進度
上一節顯示的影片播放器仍缺少一個在大多數媒體播放器中都具有的功能——確定當前位置的能力,為使這個影片播放器更加精致,可添加一些文本來顯示時間的流逝,并添加進度條來指示影片只需的速度,下圖顯示了使用這兩個細節的影片播放器的修改版,

添加這些細節相當簡單,首先需要使用TextBlock元素顯示時間,而后需要使用ProgressBar控制元件顯示圖形進度條,可能認為,可使用資料系結運算式設定TextBlock值和ProgressBar內容,但這是行不同的,因為從故事板中檢索當前影片時鐘相關的唯一方式是使用方法,如GetCurrentTime()和GetCurrentProgress(),無法從屬性中獲取相同的資訊,
最簡單的解決方法是回應下表中列出的某個故事板事件,
表 故事板事件
| 名 稱 | 說 明 |
| Completed | 影片已經到達終點 |
| CurrentGlobalSpeedInvalidated | 速度發生了變化,或者影片被暫停、重新開始、停止或移到某個新的位置,當影片時鐘反轉時(在可反轉影片的終點),以及當影片加速和減速時,也會引發該事件 |
| CurrentStateInvalidated | 影片已經開始或結束 |
| CurrentTimeInvalidated | 影片時鐘已經向前移動了一個步長,正在更改影片,當影片開始、停止或結束時也會引發該事件 |
| RemoveRequested | 影片正在被移除,使用影片的屬性隨后會回傳為原來的值 |
這個示例需要使用CurrentTimeInvalidated事件,每次向前移動影片時鐘都會引發該事件(通常,每秒移動60此,但如果執行的代碼需要更長時間,可能會丟失時鐘刻度),
當引發CurrentTimeInvalidated事件時,發送者是Clock物件(Clock類位于System.Windows.Media.Animation名稱空間),可以通過Clock物件檢索當前時間,當前時間使用TimeSpan物件表示;并且可檢索當前進度,當前進度使用0~1之間的數值表示,
下面的代碼更新標簽和進度條:
private void storyboard_CurrentTimeInvalidated(object sender, EventArgs e) { // Sender is the clock that was created for this storyboard. Clock storyboardClock = (Clock)sender; if (storyboardClock.CurrentProgress == null) { lblTime.Text = "[[ stopped ]]"; progressBar.Value = 0; } else { lblTime.Text = storyboardClock.CurrentTime.ToString(); progressBar.Value = (double)storyboardClock.CurrentProgress; } }
轉載請註明出處,本文鏈接:https://www.uj5u.com/net/2971.html
標籤:WPF
