上一章分析了WPF元素的內(nèi)部工作元素——允許每個(gè)元素插入到WPF布局系統(tǒng)的MeasureOverride()和ArrangeOverride()方法中。本章將進(jìn)一步深入分析和研究元素如何渲染自身。 大多數(shù)WPF元素通過(guò)組合方式創(chuàng)建可視化外觀。換句話說(shuō),典型的元素通過(guò)其他更基礎(chǔ)的元素進(jìn)行構(gòu)建。例如,使用標(biāo)記定義用戶控件的組合元素,處理標(biāo)記的方式與自定義窗口中的XAML相同。使用控件模板為自定義控件定義可視化樹(shù)。并且當(dāng)創(chuàng)建自定義面板時(shí),根本不必定義任何可視化細(xì)節(jié)。組合元素由克難攻堅(jiān)使用者提供,并添加到Children集合?!?/p> 當(dāng)然,知道現(xiàn)在才能使用組合。最終,一些類需要負(fù)責(zé)繪制內(nèi)容。在WPF中,這些類位于元素樹(shù)的底層。在典型窗口中,是通過(guò)單獨(dú)的文本、形狀以及位圖執(zhí)行渲染的,而不是通過(guò)高級(jí)元素。 一、OnRender()方法 為了執(zhí)行自定義渲染,元素必須重寫OnRender()方法,該方法繼承自UIElement基類。OnRender()方法未必不需要替換組合——一些控件使用OnRender()方法繪制可視化細(xì)節(jié)并使用組合在其上疊加其他元素。Border和Panel類是兩個(gè)例子,Border類在OnRender()方法中繪制邊框,Panel類在OnRender()方法中繪制背景。Border和Panel類都支持子內(nèi)容,并且這些子內(nèi)容在自定義的繪圖細(xì)節(jié)之上進(jìn)行渲染。 OnRender()方法接受一個(gè)DrawingContext對(duì)象,該對(duì)象為繪制內(nèi)容提供了了一套很有用的方法。在OnRender()方法中執(zhí)行繪圖的主要區(qū)別是不能顯示地創(chuàng)建和關(guān)閉DrawingContext對(duì)象。這是因?yàn)閹讉€(gè)不同的OnRender()方法可能使用相同的DrawingContext對(duì)象。例如,派生的元素可以執(zhí)行一些自定義繪圖操作并調(diào)用基類中的OnRender()方法來(lái)繪制其他內(nèi)容。這種方法是可行的,因?yàn)楫?dāng)開(kāi)始這一過(guò)程時(shí),WPF會(huì)自動(dòng)創(chuàng)建DrawingContext對(duì)象,并且當(dāng)不再需要時(shí)關(guān)閉對(duì)象。 關(guān)于WPF渲染,最令人驚奇的細(xì)節(jié)是實(shí)際上只需要使用很少的類。大多數(shù)類是通過(guò)其他更簡(jiǎn)單的類構(gòu)建的,并且對(duì)于典型的控件,為了找到實(shí)際重寫OnRender()方法的類,需要進(jìn)入到控件元素樹(shù)中非常深的層次。下面是一些重寫OnRender()方法的類:
通常,OnRender()方法的實(shí)現(xiàn)看起來(lái)很簡(jiǎn)單。例如,下面是繼承自Shape類的所有渲染代碼: protected override void OnRender(DrawingContext drawingContext) { this.EnsureRenderedGeometry(); if(this._renderedGeometry!=Geometry.Empty) { drawingContext.DrawingGeometry(this.Fill,this.GetPen(),this._renderedGeometry); } } 請(qǐng)記住,重寫OnRender()方法不是渲染內(nèi)容并且將其添加到用戶界面的唯一方法。也可以創(chuàng)建DrawingVisual對(duì)象,并是喲AddVisualChild()方法為UIElement對(duì)象添加該可視化對(duì)象。然后可以調(diào)用DrawingVisual.RenderOpen()方法為DrawingVisual對(duì)象檢索DrawingContext對(duì)象,并使用返回的DrawingContext對(duì)象渲染DrawingVisual對(duì)象的內(nèi)容。 在WPF中,一些元素使用這種策略在其他元素內(nèi)容之上顯示一些圖形細(xì)節(jié)。例如,在拖放指示器、錯(cuò)誤指示器以及焦點(diǎn)框中可以看到這種情況。在所有這些情況中,DrawingVisual類允許元素在其他內(nèi)容之上繪制內(nèi)容,而不是在其他內(nèi)容之下繪制內(nèi)容。但對(duì)于大部分情況,是在專門的OnRender()方法中進(jìn)行渲染。 二、評(píng)估自定義繪圖 當(dāng)創(chuàng)建自定義元素時(shí),可能會(huì)選擇重寫OnRender()方法來(lái)繪制自定義內(nèi)容。可在包含內(nèi)容的元素(最常見(jiàn)的情況是繼承自Decorator的類)中重寫OnRender()方法,從而可以在內(nèi)容周圍添加圖形裝飾。也可以在沒(méi)有任何嵌套內(nèi)容的元素中重寫OnRender()方法,從而可以繪制元素的整個(gè)可視化外觀。例如,可以創(chuàng)建繪制一些小的圖形細(xì)節(jié)的自定義元素,然后可以通過(guò)組合,在其他類中使用自定義元素。WPF中的這方面示例是TickBar元素,該元素為Slider控件繪制刻度標(biāo)記。TickBar元素通過(guò)Slider控件的默認(rèn)控件模板(該模板還包括一個(gè)Border和一個(gè)Track元素,Track元素又包含了兩個(gè)RepeatButton控件和一個(gè)Thumb元素)嵌入到Slider控件的可視化樹(shù)中。 一個(gè)明顯的問(wèn)題是需要確定何時(shí)使用較低級(jí)的OnRender()方法,以及何時(shí)使用其他類(l例如,繼承自Shape類的元素)的組合來(lái)繪制所需的內(nèi)容。為了做出決定,需要評(píng)估所需圖形的復(fù)雜程度以及希望提供的交互能力。 例如,分析一下ButtonChrome類。在ButtonChrome類的WPF實(shí)現(xiàn)中,自定義的渲染代碼考慮了各種屬性,包括RenderDefaulted、RenderMouseOver以及RenderPressed。Button類的默認(rèn)控件模板在適當(dāng)?shù)臅r(shí)機(jī)使用觸發(fā)器設(shè)置這些屬性。例如,當(dāng)將鼠標(biāo)移動(dòng)到按鈕上時(shí),Button類使用觸發(fā)器將ButtonChrome.RenderMouseOver屬性設(shè)置為true。 無(wú)論何時(shí)改變RenderDefaulted、RenderMouseOver或RenderPressed屬性,ButtonChrome類都會(huì)調(diào)用基本的InvalidateVisual()方法來(lái)指示當(dāng)前外觀不在有效。WPF然后調(diào)用ButtonChrome.OnRender()方法來(lái)獲取新的圖形表示。 如果ButtonChrome類使用組合,這種行為就更難實(shí)現(xiàn)。使用合適的元素為ButtonChrome類創(chuàng)建標(biāo)準(zhǔn)外觀很容易,但是當(dāng)按鈕的狀態(tài)發(fā)生變化是,需要做更多的工作來(lái)修改外觀。需要?jiǎng)討B(tài)改變構(gòu)成ButtonChrome類的嵌套元素,如果外觀變化很大的話,就必須隱藏一個(gè)元素并在合適的位置顯示另一個(gè)元素。 大多數(shù)自定義元素不需要自定義渲染。但是當(dāng)屬性發(fā)生變化或執(zhí)行特定操作是,需要渲染復(fù)雜的變化很大的可視化外觀,此時(shí)使用自定義的渲染方法可能更加簡(jiǎn)單并且更便捷。 三、自定義繪圖元素 通過(guò)前面對(duì)OnRender()方法的介紹,理解其工作原理。下面使用OnRender()方法創(chuàng)建自定義控件。 下面創(chuàng)建了一個(gè)名為CustomDrawnElement的元素,演示了一種簡(jiǎn)單的效果。該元素使用RadialGradientBrush畫刷繪制陰影背景,技巧是動(dòng)態(tài)設(shè)置強(qiáng)調(diào)顯示的漸變起點(diǎn),使用其跟隨鼠標(biāo)。從而當(dāng)用戶在控件上移動(dòng)鼠標(biāo)時(shí),白色的發(fā)光中心點(diǎn)跟隨鼠標(biāo)移動(dòng)。 CustomDrawnElement元素不需要包含任何子內(nèi)容,所以它直接繼承自FrameworkElement類。該元素只提供了一個(gè)可以設(shè)置的屬性——漸變的背景色。 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標(biāo)志明確進(jìn)行了標(biāo)識(shí)。因此,無(wú)論何時(shí)改變了背景色,WPF都自動(dòng)調(diào)用OnRender()方法。然而,當(dāng)鼠標(biāo)移動(dòng)到新的位置時(shí),也需要確保調(diào)用OnRender()方法。這是通過(guò)在合適的時(shí)間調(diào)用InvalidateVisual()方法實(shí)現(xiàn)的。 . . . protected override void OnMouseMove(MouseEventArgs e) { base.OnMouseMove(e); this.InvalidateVisual(); } protected override void OnMouseLeave(MouseEventArgs e) { base.OnMouseLeave(e); this.InvalidateVisual(); } . . . 剩下的唯一細(xì)節(jié)是渲染代碼。渲染代碼使用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()的私有輔助方法根據(jù)鼠標(biāo)的當(dāng)前位置構(gòu)造正確的RadialGradientBrush畫刷。為了計(jì)算中心點(diǎn),需要將鼠標(biāo)在元素上懸停的當(dāng)前位置轉(zhuǎn)換成從0到1的相對(duì)位置,這正是RadialGradientBrush畫刷期望的結(jié)果。 . . . 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; } } . . . 四、創(chuàng)建自定義裝飾元素 作為一條通用規(guī)則,切勿在控件中使用自定義繪圖。如果在控件中使用自定義繪圖,就違反了WPF無(wú)外觀空間的承諾。問(wèn)題是一旦硬編碼一些繪圖邏輯,就會(huì)使控件可視化外觀的一部分不能通過(guò)控件模板進(jìn)行定制。更好的方法是設(shè)計(jì)單獨(dú)的繪制自定義內(nèi)容的元素(如上面示例中的CustomDrawnElement類),然后在控件的默認(rèn)模板內(nèi)部使用自定義元素。 有必要快速分析一下如何修改上面示例,使其能夠成為控件模板的一部分。在控件模板中,自定義繪圖元素通常扮演兩個(gè)角色:
第二種方法需要自定義裝飾元素,可以通過(guò)兩個(gè)輕微的改動(dòng)將CustomDrawnElement類轉(zhuǎn)換成自定義繪圖元素。首先,使該類繼承自Decorator類: public class CustomDrawnDecorator:Decorator 然后重寫OnMeasure()方法,指定需要的尺寸,所有裝飾元素都會(huì)考慮它們的子元素,增加裝飾所需要的額外空間,然后返回組合之后的尺寸。CustomDrawnDecorator類不需要任何額外的空間來(lái)繪制邊框,相反,使用下面的代碼簡(jiǎn)單地使其自定和其內(nèi)容具有相同的尺寸: protected override Size MeasureOverride(Size constraint) { UIElement child = this.Child; if (child != null) { child.Measure(constraint); return child.DesiredSize; } else { return new Size(); } } 一旦創(chuàng)建自定義裝飾元素,就可以在自定義控件模板中使用它們。例如,下面的按鈕模板在按鈕內(nèi)容的后面放置了跟隨鼠標(biāo)蹤跡的漸變背景。使用模板綁定確保使用對(duì)齊屬性和內(nèi)邊距屬性。 <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> 現(xiàn)在可以使用這個(gè)模板重新樣式化按鈕,使其具有新的外觀。當(dāng)然,為了使自定義裝飾元素更加實(shí)用,當(dāng)單擊鼠標(biāo)按鈕時(shí)可能更希望改變它的外觀。使用修改裝飾類屬性的觸發(fā)器可以完成該工作。 本章示例源碼:CustomDrawnElement.zip |
|