上一章分析了WPF元素的內部工做元素——容許每一個元素插入到WPF佈局系統的MeasureOverride()和ArrangeOverride()方法中。本章將進一步深刻分析和研究元素如何渲染自身。ide
大多數WPF元素經過組合方式建立可視化外觀。換句話說,典型的元素經過其餘更基礎的元素進行構建。例如,使用標記定義用戶控件的組合元素,處理標記的方式與自定義窗口中的XAML相同。使用控件模板爲自定義控件定義可視化樹。而且當建立自定義面板時,根本沒必要定義任何可視化細節。組合元素由克難攻堅使用者提供,並添加到Children集合。 佈局
固然,知道如今才能使用組合。最終,一些類須要負責繪製內容。在WPF中,這些類位於元素樹的底層。在典型窗口中,是經過單獨的文本、形狀以及位圖執行渲染的,而不是經過高級元素。this
1、OnRender()方法編碼
爲了執行自定義渲染,元素必須重寫OnRender()方法,該方法繼承自UIElement基類。OnRender()方法未必不須要替換組合——一些控件使用OnRender()方法繪製可視化細節並使用組合在其上疊加其餘元素。Border和Panel類是兩個例子,Border類在OnRender()方法中繪製邊框,Panel類在OnRender()方法中繪製背景。Border和Panel類都支持子內容,而且這些子內容在自定義的繪圖細節之上進行渲染。spa
OnRender()方法接受一個DrawingContext對象,該對象爲繪製內容提供了了一套頗有用的方法。在OnRender()方法中執行繪圖的主要區別是不能顯示地建立和關閉DrawingContext對象。這是由於幾個不一樣的OnRender()方法可能使用相同的DrawingContext對象。例如,派生的元素能夠執行一些自定義繪圖操做並調用基類中的OnRender()方法來繪製其餘內容。這種方法是可行的,由於當開始這一過程時,WPF會自動建立DrawingContext對象,而且當再也不須要時關閉對象。設計
關於WPF渲染,最使人驚奇的細節是實際上只須要使用不多的類。大多數類是經過其餘更簡單的類構建的,而且對於典型的控件,爲了找到實際重寫OnRender()方法的類,須要進入到控件元素樹中很是深的層次。下面是一些重寫OnRender()方法的類:code
一般,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()方法中進行渲染。blog
2、評估自定義繪圖
當建立自定義元素時,可能會選擇重寫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類的嵌套元素,若是外觀變化很大的話,就必須隱藏一個元素並在合適的位置顯示另外一個元素。
大多數自定義元素不須要自定義渲染。可是當屬性發生變化或執行特定操做是,須要渲染複雜的變化很大的可視化外觀,此時使用自定義的渲染方法可能更加簡單而且更便捷。
3、自定義繪圖元素
經過前面對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; } } . . .
4、建立自定義裝飾元素
做爲一條通用規則,切勿在控件中使用自定義繪圖。若是在控件中使用自定義繪圖,就違反了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