在WPF中製作位移類動畫大致有3種方式,Margin、RenderTransform和LayoutTransform。雖然3者的效果略有不同,但是不少情況下3種方式可以通用。這時RenderTransform就以其優秀的平均效能成了3者的首選。因為RenderTransform不涉及Layout的調整,不會觸發介面的重新布局(關於RenderTransform與LayoutTransform之間的不同可以參考這篇文章,由於某個眾所周知的原因,要看這篇文章需要翻牆,所以直接點是打不開的)。但是當你瞭解到RenderTransform所存在的Bug時,可能就需要考慮一番了。
我們都知道很多控制項都有FocusVisualStyle,一般就是一個虛線框。RenderTransform的問題就在於,控制項的FocusVisualStyle中的元素,不會隨著控制項本身一起被Transform。
Bug的重現過程如所示。
圖1. 程式運行圖
一個簡單得不能再簡單的程式。在空表單上放一個Button,建立一個動畫,當MouseEnter這個Button的時候,用RenderTransform把這個Button向右向下移動100個像素。代碼如下。
Demo Code
1 <Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
2 xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
3 x:Class="AnimationConflict.MainWindow"
4 x:Name="Window" Title="MainWindow"
5 Width="200" Height="200">
6 <Window.Resources>
7 <Storyboard x:Key="OnMouseEnter">
8 <DoubleAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="button"
9 Storyboard.TargetProperty="(UIElement.RenderTransform).(TranslateTransform.X)">
10 <SplineDoubleKeyFrame KeyTime="00:00:01" Value="100"/>
11 </DoubleAnimationUsingKeyFrames>
12 <DoubleAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="button"
13 Storyboard.TargetProperty="(UIElement.RenderTransform).(TranslateTransform.Y)">
14 <SplineDoubleKeyFrame KeyTime="00:00:01" Value="100"/>
15 </DoubleAnimationUsingKeyFrames>
16 </Storyboard>
17 </Window.Resources>
18 <Window.Triggers>
19 <EventTrigger RoutedEvent="Mouse.MouseEnter" SourceName="button">
20 <BeginStoryboard Storyboard="{StaticResource OnMouseEnter}"/>
21 </EventTrigger>
22 </Window.Triggers>
23 <Grid x:Name="LayoutRoot">
24 <Button x:Name="button" RenderTransformOrigin="0.5,0.5"
25 HorizontalAlignment="Left" VerticalAlignment="Top"
26 Width="75" Content="Button">
27 <Button.RenderTransform>
28 <TranslateTransform/>
29 </Button.RenderTransform>
30 </Button>
31 </Grid>
32 </Window>
程式剛運行起來的時候Button是沒有獲得焦點的,我們按幾下Tab鍵,讓這個Button獲得焦點。2所示。
圖2. Button得到了焦點
然後不要再動任何鍵了,把滑鼠移到Button上面以觸發動畫。發現什麼了?看圖3。
圖3. Focus被Button落下了
當滑鼠再次MouseEnterButton時,虛框會被自動放回正確的位置。整個樣本程式可以從這裡下載。運行環境是.NET Framework 3.5 SP1。
初步判斷,這個Bug與WPF對FocusVisualStyle的設計方式有關。這個虛框與Adorner Layer類似,並不屬於這個Button,所以渲染時沒有被當被Button的一部分。而RenderTransform是一種純渲染時行為,所以虛框就被無視了。從邏輯上來講似乎這個Bug很合理,不然會讓Render的設計很複雜,本來Render一個控制項只要關心控制項自身就可以了,現在還要關心相關的其它控制項。但是我相信以微軟的水平,應該是有能力解決這個問題的。就像文字渲染模糊的問題,WPF剛出來就被人詬病,微軟還振振有詞地解釋說這就是WPF的像素無關和理想渲染模型設計原則的體現。這不到了.NET 4.0最終還是提供了選項關閉WPF的理想渲染模型。
其實在WPF中類似這樣的問題有很多,比如Validation Error Template、Popup Window都有類似的問題。沒有為PopupWindow寫一個專題是因為產生Bug的條件可以避免出現。但是對於這類,動畫與Focus的組合,是很常見的情況。
而且這個問題從WPF的使用者而言是如此底層。到目前為止,我也只想到兩個不是辦法的辦法。
1. 不顯示FocusVisualStyle。
2. 不用RenderTransform。
其實這隻是迴避問題式的方案。不知大家有什麼好的解決方案?
更新解決方案:
根據Curry的回複。目前已經的最佳解決方案是。在Storyboard的CurrentTimeInvalidated事件處理函數中加入下面的代碼。
AdornerLayer.GetAdornerLayer(button).Update();
這樣Button的行為就正確了。但是大量使用可能導致一些效能問題。當然如果只有幾個控制項應該是沒有問題的。