【WPF學習】第六十八章 自定義繪圖元素

  上一章分析了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

  •   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()方法中進行渲染。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

相關文章
相關標籤/搜索