上一章分析了WPF元素的內部工作元素——允許每個元素插入到WPF佈局系統的MeasureOverride()和ArrangeOverride()方法中。本章將進一步深入分析和研究元素如何渲染自身。 大多數WPF元素通過組合方式創建可視化外觀。換句話說,典型的元素通過其他更基礎的元素進行構建。例如,使 ...
上一章分析了WPF元素的內部工作元素——允許每個元素插入到WPF佈局系統的MeasureOverride()和ArrangeOverride()方法中。本章將進一步深入分析和研究元素如何渲染自身。
大多數WPF元素通過組合方式創建可視化外觀。換句話說,典型的元素通過其他更基礎的元素進行構建。例如,使用標記定義用戶控制項的組合元素,處理標記的方式與自定義視窗中的XAML相同。使用控制項模板為自定義控制項定義可視化樹。並且當創建自定義面板時,根本不必定義任何可視化細節。組合元素由克難攻堅使用者提供,並添加到Children集合。
當然,知道現在才能使用組合。最終,一些類需要負責繪製內容。在WPF中,這些類位於元素樹的底層。在典型視窗中,是通過單獨的文本、形狀以及點陣圖執行渲染的,而不是通過高級元素。
一、OnRender()方法
為了執行自定義渲染,元素必須重寫OnRender()方法,該方法繼承自UIElement基類。OnRender()方法未必不需要替換組合——一些控制項使用OnRender()方法繪製可視化細節並使用組合在其上疊加其他元素。Border和Panel類是兩個例子,Border類在OnRender()方法中繪製邊框,Panel類在OnRender()方法中繪製背景。Border和Panel類都支持子內容,並且這些子內容在自定義的繪圖細節之上進行渲染。
OnRender()方法接受一個DrawingContext對象,該對象為繪製內容提供了了一套很有用的方法。在OnRender()方法中執行繪圖的主要區別是不能顯示地創建和關閉DrawingContext對象。這是因為幾個不同的OnRender()方法可能使用相同的DrawingContext對象。例如,派生的元素可以執行一些自定義繪圖操作並調用基類中的OnRender()方法來繪製其他內容。這種方法是可行的,因為當開始這一過程時,WPF會自動創建DrawingContext對象,並且當不再需要時關閉對象。
關於WPF渲染,最令人驚奇的細節是實際上只需要使用很少的類。大多數類是通過其他更簡單的類構建的,並且對於典型的控制項,為了找到實際重寫OnRender()方法的類,需要進入到控制項元素樹中非常深的層次。下麵是一些重寫OnRender()方法的類:
- TextBlock類。無論在何處放置文本,都有TextBlock對象使用OnRender()方法繪製文本。
- Image類。Image類重寫OnRender()方法,使用DrawingContext.DrawImage()方法繪製圖形內容。
- MediaElement類。如果正在使用該類播放視頻文件,該類會重寫OnRender()方法以繪製視頻幀。
- 各種形狀類。Shape基類重寫了OnRender()方法,通過使用DrawingContext.DrawGeometry()方法,繪製在其內部存儲的Geometry對象。根據Shape類的特定派生類,Geometry對象可以表示橢圓、矩形或更複雜的由直線和曲線構成的路徑。許多元素使用形狀繪製小的可視化細節。
- 各種修飾類。這些類(如ButtonChrome和ListBoxChrome)繪製通用控制項的外側外觀,併在具體制定的內部放置內容。其他許多繼承自Decorator的類,如Border類,都重寫了OnRender()方法。
- 各種面板類。儘管面板的內容是由其子元素提供的,但是OnRender()方法繪製具有背景色(假設設置了Background屬性)的矩形。
通常,OnRender()方法的實現看起來很簡單。例如,下麵是繼承自Shape類的所有渲染代碼:
protected override void OnRender(DrawingContext drawingContext) { this.EnsureRenderedGeometry(); if(this._renderedGeometry!=Geometry.Empty) { drawingContext.DrawingGeometry(this.Fill,this.GetPen(),this._renderedGeometry); } }
請記住,重寫OnRender()方法不是渲染內容並且將其添加到用戶界面的唯一方法。也可以創建DrawingVisual對象,並是喲AddVisualChild()方法為UIElement對象添加該可視化對象。然後可以調用DrawingVisual.RenderOpen()方法為DrawingVisual對象檢索DrawingContext對象,並使用返回的DrawingContext對象渲染DrawingVisual對象的內容。
在WPF中,一些元素使用這種策略在其他元素內容之上顯示一些圖形細節。例如,在拖放指示器、錯誤指示器以及焦點框中可以看到這種情況。在所有這些情況中,DrawingVisual類允許元素在其他內容之上繪製內容,而不是在其他內容之下繪製內容。但對於大部分情況,是在專門的OnRender()方法中進行渲染。
二、評估自定義繪圖
當創建自定義元素時,可能會選擇重寫OnRender()方法來繪製自定義內容。可在包含內容的元素(最常見的情況是繼承自Decorator的類)中重寫OnRender()方法,從而可以在內容周圍添加圖形裝飾。也可以在沒有任何嵌套內容的元素中重寫OnRender()方法,從而可以繪製元素的整個可視化外觀。例如,可以創建繪製一些小的圖形細節的自定義元素,然後可以通過組合,在其他類中使用自定義元素。WPF中的這方面示例是TickBar元素,該元素為Slider控制項繪製刻度標記。TickBar元素通過Slider控制項的預設控制項模板(該模板還包括一個Border和一個Track元素,Track元素又包含了兩個RepeatButton控制項和一個Thumb元素)嵌入到Slider控制項的可視化樹中。
一個明顯的問題是需要確定何時使用較低級的OnRender()方法,以及何時使用其他類(l例如,繼承自Shape類的元素)的組合來繪製所需的內容。為了做出決定,需要評估所需圖形的複雜程度以及希望提供的交互能力。
例如,分析一下ButtonChrome類。在ButtonChrome類的WPF實現中,自定義的渲染代碼考慮了各種屬性,包括RenderDefaulted、RenderMouseOver以及RenderPressed。Button類的預設控制項模板在適當的時機使用觸發器設置這些屬性。例如,當將滑鼠移動到按鈕上時,Button類使用觸發器將ButtonChrome.RenderMouseOver屬性設置為true。
無論何時改變RenderDefaulted、RenderMouseOver或RenderPressed屬性,ButtonChrome類都會調用基本的InvalidateVisual()方法來指示當前外觀不在有效。WPF然後調用ButtonChrome.OnRender()方法來獲取新的圖形表示。
如果ButtonChrome類使用組合,這種行為就更難實現。使用合適的元素為ButtonChrome類創建標準外觀很容易,但是當按鈕的狀態發生變化是,需要做更多的工作來修改外觀。需要動態改變構成ButtonChrome類的嵌套元素,如果外觀變化很大的話,就必須隱藏一個元素併在合適的位置顯示另一個元素。
大多數自定義元素不需要自定義渲染。但是當屬性發生變化或執行特定操作是,需要渲染複雜的變化很大的可視化外觀,此時使用自定義的渲染方法可能更加簡單並且更便捷。
三、自定義繪圖元素
通過前面對OnRender()方法的介紹,理解其工作原理。下麵使用OnRender()方法創建自定義控制項。
下麵創建了一個名為CustomDrawnElement的元素,演示了一種簡單的效果。該元素使用RadialGradientBrush畫刷繪製陰影背景,技巧是動態設置強調顯示的漸變起點,使用其跟隨滑鼠。從而當用戶在控制項上移動滑鼠時,白色的發光中心點跟隨滑鼠移動。
CustomDrawnElement元素不需要包含任何子內容,所以它直接繼承自FrameworkElement類。該元素只提供了一個可以設置的屬性——漸變的背景色。
public class CustomDrawnElement:FrameworkElement { public static DependencyProperty BackgroundColorProperty; static CustomDrawnElement() { FrameworkPropertyMetadata metadata = new FrameworkPropertyMetadata(Colors.Yellow); metadata.AffectsRender = true; BackgroundColorProperty = DependencyProperty.Register("BackgroundColor", typeof(Color), typeof(CustomDrawnElement), metadata); } public Color BackgroundColor { get { return (Color)GetValue(BackgroundColorProperty); } set { SetValue(BackgroundColorProperty, value); } } ... }
BackgroundColor依賴性屬性使用FrameworkPropertyMetadata.AffectRender標誌明確進行了標識。因此,無論何時改變了背景色,WPF都自動調用OnRender()方法。然而,當滑鼠移動到新的位置時,也需要確保調用OnRender()方法。這是通過在合適的時間調用InvalidateVisual()方法實現的。
. . . protected override void OnMouseMove(MouseEventArgs e) { base.OnMouseMove(e); this.InvalidateVisual(); } protected override void OnMouseLeave(MouseEventArgs e) { base.OnMouseLeave(e); this.InvalidateVisual(); } . . .
剩下的唯一細節是渲染代碼。渲染代碼使用DrawingContext.DrawRectangle()方法繪製元素的背景。ActualWidth和ActualHeight屬性只是控制項最終的渲染尺寸。
. . . protected override void OnRender(DrawingContext dc) { base.OnRender(dc); Rect bounds = new Rect(0, 0, base.ActualWidth, base.ActualHeight); dc.DrawRectangle(GetForegroundBrush(), null, bounds); } . . .
最後,名為GetForegroundBrush()的私有輔助方法根據滑鼠的當前位置構造正確的RadialGradientBrush畫刷。為了計算中心點,需要將滑鼠在元素上懸停的當前位置轉換成從0到1的相對位置,這正是RadialGradientBrush畫刷期望的結果。
. . . private Brush GetForegroundBrush() { if (!IsMouseOver) { return new SolidColorBrush(BackgroundColor); } else { RadialGradientBrush brush = new RadialGradientBrush(Colors.White, BackgroundColor); Point absoluteGradientOrigin = Mouse.GetPosition(this); Point relativeGradientOrigin = new Point( absoluteGradientOrigin.X / base.ActualWidth, absoluteGradientOrigin.Y / base.ActualHeight); brush.GradientOrigin = relativeGradientOrigin; brush.Center = relativeGradientOrigin; brush.Freeze(); return brush; } } . . .
四、創建自定義裝飾元素
作為一條通用規則,切勿在控制項中使用自定義繪圖。如果在控制項中使用自定義繪圖,就違反了WPF無外觀空間的承諾。問題是一旦硬編碼一些繪圖邏輯,就會使控制項可視化外觀的一部分不能通過控制項模板進行定製。更好的方法是設計單獨的繪製自定義內容的元素(如上面示例中的CustomDrawnElement類),然後在控制項的預設模板內部使用自定義元素。
有必要快速分析一下如何修改上面示例,使其能夠成為控制項模板的一部分。在控制項模板中,自定義繪圖元素通常扮演兩個角色:
- 它們繪製一些小的圖形細節(例如滾動按鈕上的箭頭)。
- 它們在另一個元素的周圍提供更詳細的背景或邊框。
第二種方法需要自定義裝飾元素,可以通過兩個輕微的改動將CustomDrawnElement類轉換成自定義繪圖元素。首先,使該類繼承自Decorator類:
public class CustomDrawnDecorator:Decorator
然後重寫OnMeasure()方法,指定需要的尺寸,所有裝飾元素都會考慮它們的子元素,增加裝飾所需要的額外空間,然後返回組合之後的尺寸。CustomDrawnDecorator類不需要任何額外的空間來繪製邊框,相反,使用下麵的代碼簡單地使其自定和其內容具有相同的尺寸:
protected override Size MeasureOverride(Size constraint) { UIElement child = this.Child; if (child != null) { child.Measure(constraint); return child.DesiredSize; } else { return new Size(); } }
一旦創建自定義裝飾元素,就可以在自定義控制項模板中使用它們。例如,下麵的按鈕模板在按鈕內容的後面放置了跟隨滑鼠蹤跡的漸變背景。使用模板綁定確保使用對齊屬性和內邊距屬性。
<ControlTemplate x:Key="ButtonWithCustomChrome"> <lib:CustomDrawnDecorator BackgroundColor="LightGreen"> <ContentPresenter Margin="{TemplateBinding Padding}" HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" VerticalAlignment="{TemplateBinding VerticalContentAlignment}" ContentTemplate="{TemplateBinding ContentControl.ContentTemplate}" Content="{TemplateBinding ContentControl.Content}" RecognizesAccessKey="True" /> </lib:CustomDrawnDecorator> </ControlTemplate>
現在可以使用這個模板重新樣式化按鈕,使其具有新的外觀。當然,為了使自定義裝飾元素更加實用,當單擊滑鼠按鈕時可能更希望改變它的外觀。使用修改裝飾類屬性的觸發器可以完成該工作。
本章示例源碼:CustomDrawnElement.zip