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

来源:https://www.cnblogs.com/Peter-Luo/archive/2020/05/03/12748243.html
-Advertisement-
Play Games

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


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 通俗理解spring源碼(五)—— 解析及註冊BeanDefinitions 上節講到瞭如何獲取document,當把文件轉換為document後,接下來的提取及註冊bean就是我們的重頭戲。 protected int doLoadBeanDefinitions(InputSource input ...
  • Docker的一些概念 docker image docker鏡像 即容器模板,操作系統+軟體運⾏環境+⽤戶程式 類似於安裝操作系統的windows鏡像、centos鏡像,只是一個模板 Docker container docker容器 容器是從鏡像運行的實例,比如用tomcat鏡像運行tomcat ...
  • 一、問題 問題1 場景:如果你未來的丈母娘要求你,第1天給她1分錢,第2天給2分錢,第3天給4分錢,以此類推,每天給前一天的2倍,給1個月(按30天)算就行。問:第30天給多少錢,總共給多少錢? 問題2 場景:如果有兩份工作。 第1份:第1天給你1分錢,第2天給你2分錢,第3天給你4分錢,以此類推, ...
  • 本節常見面試題 如何判斷對象是否死亡(兩種方法)。 簡單的介紹一下強引用、軟引用、弱引用、虛引用(虛引用與軟引用和弱引用的區別、使用軟引用能帶來的好處)。 如何判斷一個常量是廢棄常量 如何判斷一個類是無用的類 堆中幾乎放著所有的對象實例,對堆垃圾回收前的第一步就是要判斷那些對象已經死亡(即不能再被任 ...
  • 1、深拷貝 --> 克隆一份,修改拷貝後的內容不對原對象內容產生影響 拷貝後修改序列中元素內容,註意:被修改的元素不能為一個序列中的某個值 a = [["北京多測師","成都多測師"],"上海多測師","深圳多測師","廣州多測師"] b = a.copy() b[1] = "天津多測師" prin ...
  • 本篇博客接著講解機器視覺的有關技術和知識。包括寬度測量,缺陷檢測,醫學處理。 一:寬度測量 在傳統的自動化生產中,對於尺寸的測量,典型的方法就是千分尺、游標卡尺、塞尺等。而這些測量手段測量精度低、速度慢,無法滿足大規模的自動化生產需求。基於機器視覺的尺寸測量屬於非接觸式的測量,具有檢測精度高、速度快 ...
  • 25. K 個一組翻轉鏈表 題目來源: "https://leetcode cn.com/problems/reverse nodes in k group" 題目 給你一個鏈表,每 k 個節點一組進行翻轉,請你返回翻轉後的鏈表。 k 是一個正整數,它的值小於或等於鏈表的長度。 如果節點總數不是 k ...
  • 1、命令行的開始 首先介紹幾個常用到的命令 dotnet --info :查看電腦上環境。 dotnet new --list:查看腳手架模板列表 dotnet new console -n newHelloworld:創建一個名為newHelloworld的控制台程式 dotnet build:切 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...