WPF 圖形轉譯概觀

本主題提供 WPF 視覺圖層的概觀。 重點會放在 Visual 類別在 WPF 模型中提供轉譯支援的角色。

視覺物件的角色

Visual 類別是每個 FrameworkElement 物件從中衍生的基本抽象概念。 它也作為在 WPF 中撰寫新控制項的進入點,且在許多方面,可以視為 Win32 應用程式模型中的視窗控制代碼 (HWND)。

Visual 物件是 WPF 核心物件,其主要角色是提供轉譯支援。 使用者介面控制項 (例如 ButtonTextBox) 衍生自 Visual 類別,並使用該類別來保存其轉譯資料。 Visual 物件提供下列支援:

  • 輸出顯示︰轉譯視覺物件已保存、序列化的繪圖內容。

  • 轉換︰執行視覺物件的轉換。

  • 裁剪:提供視覺物件的裁剪區域支援。

  • 點擊測試︰判斷座標或幾何是否包含於視覺物件的範圍內。

  • 週框方塊計算︰判斷視覺物件的週框矩形。

不過,Visual 物件不包含非轉譯功能的支援,例如:

  • 事件處理

  • 版面配置

  • 樣式

  • 資料繫結

  • 全球化

Visual 會公開為公用抽象類別,子類別必須從中衍生。 下圖顯示 WPF 中公開之視覺物件的階層。

衍生自 Visual 物件之類別的圖表

DrawingVisual 類別

DrawingVisual 是用來轉譯圖形、影像或文字的輕量型繪圖類別。 此類別之所以被視為輕量型,是因為它不提供版面配置或事件處理,而這會改善其執行階段效能。 基於此原因,繪圖適合背景或美工圖案。 DrawingVisual 可用來建立自訂視覺物件。 如需詳細資訊,請參閱使用 DrawingVisual 物件

Viewport3DVisual 類別

Viewport3DVisual 可作為 2D VisualVisual3D 物件之間的橋樑。 Visual3D 類別是所有 3D 視覺元素的基底類別。 Viewport3DVisual 要求您定義 Camera 值和 Viewport 值。 相機可讓您檢視場景。 檢視區會建立投影到 2D 平面上的對應位置。 如需 WPF 中 3D 的詳細資訊,請參閱 3D 圖形概觀

ContainerVisual 類別

ContainerVisual 類別可作為 Visual 物件集合的容器。 DrawingVisual 類別衍生自 ContainerVisual 類別,因此可包含視覺物件集合。

視覺物件中的繪圖內容

Visual 物件會將其轉譯資料儲存為向量圖形指示清單。 指示清單中的每個項目代表一組低階圖形資料和相關聯的序列化格式的資源。 可包含繪圖內容的轉譯資料有四種不同的類型。

繪圖內容類型 描述
向量圖形 代表向量圖形資料,以及任何相關聯的 BrushPen 資訊。
映像 代表由 Rect 定義之區域內的影像。
圖像 代表轉譯 GlyphRun 的繪圖,這是指定字型資源的字符繪製順序。 這是文字的表現方式。
影片 代表轉譯視訊的繪圖。

DrawingContext 可讓您將 Visual 填入視覺內容。 當您使用 DrawingContext 物件的繪製命令時,您實際上是儲存一組稍後會由圖形系統使用的轉譯資料,並不是即時在畫面上繪製。

當您建立 WPF 控制項 (例如 Button) 時,控制項會以隱含方式產生轉譯資料來自行繪製。 例如,設定 ButtonContent 屬性會導致控制項儲存字符的轉譯表示法。

Visual 將其內容描述為包含在 DrawingGroup 內的一或多個 Drawing 物件。 DrawingGroup 也會描述套用至其內容的不透明度遮罩、轉換、點陣圖效果及其他作業。 轉譯內容時,會依下列順序套用DrawingGroup 作業:OpacityMaskOpacityBitmapEffectClipGeometryGuidelineSet,然後 Transform

下圖顯示轉譯序列期間套用 DrawingGroup 作業的順序。

DrawingGroup 作業的順序
DrawingGroup 作業的順序

如需詳細資訊,請參閱繪製物件概觀

視覺圖層的繪圖內容

您絕不會直接具現化 DrawingContext;不過,您可以從特定方法取得繪圖內容,例如 DrawingGroup.OpenDrawingVisual.RenderOpen。 下列範例會從 DrawingVisual 擷取 DrawingContext,並用來繪製矩形。

// Create a DrawingVisual that contains a rectangle.
private DrawingVisual CreateDrawingVisualRectangle()
{
    DrawingVisual drawingVisual = new DrawingVisual();

    // Retrieve the DrawingContext in order to create new drawing content.
    DrawingContext drawingContext = drawingVisual.RenderOpen();

    // Create a rectangle and draw it in the DrawingContext.
    Rect rect = new Rect(new System.Windows.Point(160, 100), new System.Windows.Size(320, 80));
    drawingContext.DrawRectangle(System.Windows.Media.Brushes.LightBlue, (System.Windows.Media.Pen)null, rect);

    // Persist the drawing content.
    drawingContext.Close();

    return drawingVisual;
}
' Create a DrawingVisual that contains a rectangle.
Private Function CreateDrawingVisualRectangle() As DrawingVisual
    Dim drawingVisual As New DrawingVisual()

    ' Retrieve the DrawingContext in order to create new drawing content.
    Dim drawingContext As DrawingContext = drawingVisual.RenderOpen()

    ' Create a rectangle and draw it in the DrawingContext.
    Dim rect As New Rect(New Point(160, 100), New Size(320, 80))
    drawingContext.DrawRectangle(Brushes.LightBlue, CType(Nothing, Pen), rect)

    ' Persist the drawing content.
    drawingContext.Close()

    Return drawingVisual
End Function

列舉視覺圖層的繪圖內容

除了其他用途之外,Drawing 物件也提供物件模型來列舉 Visual 的內容。

注意

當您列舉視覺物件的內容時,您是擷取 Drawing 物件,而非轉譯資料作為向量圖形指示清單的基礎表示法。

下列範例會使用 GetDrawing 方法來擷取 VisualDrawingGroup 值並加以列舉。

public void RetrieveDrawing(Visual v)
{
    DrawingGroup drawingGroup = VisualTreeHelper.GetDrawing(v);
    EnumDrawingGroup(drawingGroup);
}

// Enumerate the drawings in the DrawingGroup.
public void EnumDrawingGroup(DrawingGroup drawingGroup)
{
    DrawingCollection dc = drawingGroup.Children;

    // Enumerate the drawings in the DrawingCollection.
    foreach (Drawing drawing in dc)
    {
        // If the drawing is a DrawingGroup, call the function recursively.
        if (drawing is DrawingGroup group)
        {
            EnumDrawingGroup(group);
        }
        else if (drawing is GeometryDrawing)
        {
            // Perform action based on drawing type.
        }
        else if (drawing is ImageDrawing)
        {
            // Perform action based on drawing type.
        }
        else if (drawing is GlyphRunDrawing)
        {
            // Perform action based on drawing type.
        }
        else if (drawing is VideoDrawing)
        {
            // Perform action based on drawing type.
        }
    }
}

視覺物件如何用來建置控制項

WPF 中的許多物件是由其他視覺物件所組成,這表示其可以包含不同階層的子系物件。 WPF 中的許多使用者介面元素 (例如控制項) 是由多個視覺物件所組成,代表不同類型的轉譯元素。 例如,Button 控制項可以包含一些其他物件,包括 ClassicBorderDecoratorContentPresenterTextBlock

下列程式碼顯示標記中定義的 Button 控制項。

<Button Click="OnClick">OK</Button>

如果您要列舉組成預設 Button 控制項的視覺物件,您會發現下面說明的視覺物件階層:

視覺化樹狀階層架構的圖表

Button 控制項包含 ClassicBorderDecorator 元素,其中包含 ContentPresenter 元素。 ClassicBorderDecorator 元素負責繪製 Button 的框線和背景。 ContentPresenter 元素負責顯示 Button 的內容。 在此案例中,由於您正在顯示文字,因此 ContentPresenter 元素包含 TextBlock 元素。 Button 控制項使用 ContentPresenter 表示內容可以由其他元素 (例如 Image) 或幾何 (例如 EllipseGeometry) 來表示。

控制項範本

將控制項展開為控制項階層的關鍵是 ControlTemplate。 控制項範本會指定控制項的預設視覺階層。 當您明確地參考控制項時,您會以隱含方式參考其視覺階層。 您可以覆寫控制項範本的預設值,以針對控制項建立自訂視覺外觀。 例如,您可以修改 Button 控制項的背景色彩值,好讓它使用線性漸層色彩值,而不是純色值。 如需詳細資訊,請參閱按鈕樣式和範本

使用者介面元素 (例如 Button 控制項) 包含數個描述整個控制項轉譯定義的向量圖形指示清單。 下列程式碼顯示標記中定義的 Button 控制項。

<Button Click="OnClick">
  <Image Source="images\greenlight.jpg"></Image>
</Button>

如果您要列舉組成 Button 控制項的視覺物件和向量圖形指示清單,您會發現下面說明的物件階層:

視覺化樹狀結構和轉譯資料的圖表

Button 控制項包含 ClassicBorderDecorator 元素,其中包含 ContentPresenter 元素。 ClassicBorderDecorator 元素負責繪製組成按鈕框線和背景的所有離散圖形元素。 ContentPresenter 元素負責顯示 Button 的內容。 在此案例中,由於您正在顯示影像,因此 ContentPresenter 元素包含 Image 元素。

關於視覺物件的階層和向量圖形指示清單,有幾點需要注意:

  • 階層中的順序代表繪製資訊的轉譯順序。 子元素會由左至右、由上而下從根視覺元素周遊。 如果某元素具有視覺化子元素,則會在元素的同層級元素之前周遊這些子元素。

  • 階層中的非分葉節點元素 (例如 ContentPresenter) 可用來包含子元素 (不會包含指示清單)。

  • 如果視覺元素同時包含向量圖形指示清單與視覺子元素,父系視覺元素中的指示清單就會在任何視覺子元素物件中的繪圖之前轉譯。

  • 向量圖形指示清單中的項目會由左至右轉譯。

視覺化樹狀結構

視覺化樹狀結構包含應用程式使用者介面中所使用的所有視覺元素。 由於視覺元素包含持續性繪圖資訊,您可以將視覺化樹狀結構想像為場景圖形,其中包含將輸出撰寫至顯示裝置所需的所有轉譯資訊。 此樹狀結構是直接由應用程式所建立 (不論是以程式碼或標記) 的所有視覺元素累積而成。 視覺化樹狀結構也包含由元素 (例如控制項和資料物件) 的範本展開所建立的所有視覺元素。

下列程式碼顯示標記中定義的 StackPanel 元素。

<StackPanel>
  <Label>User name:</Label>
  <TextBox />
  <Button Click="OnClick">OK</Button>
</StackPanel>

如果您要列舉在標記範例中組成 StackPanel 元素的視覺物件,您會發現下面說明的視覺物件階層:

StackPanel 控制項的視覺化樹狀結構階層圖表。

轉譯順序

視覺化樹狀結構會決定 WPF 視覺物件和繪圖物件的轉譯順序。 周遊順序會從根視覺物件開始,這是視覺化樹狀結構的最上層節點。 然後,會由左至右周遊根視覺物件的子系。 如果視覺物件具有子系,則會在該視覺物件的同層級項目之前周遊其子系。 這表示子視覺物件的內容會在視覺物件本身的內容前面轉譯。

視覺化樹狀結構轉譯順序的圖表

根視覺物件

「根視覺物件」是視覺化樹狀結構階層中最上層的元素。 在大部分應用程式中,根視覺物件的基底類別是 WindowNavigationWindow。 不過,如果您已在 Win32 應用程式中裝載視覺物件,根視覺物件是您在 Win32 視窗中裝載的最上層視覺物件。 如需詳細資訊,請參閱教學課程︰在 Win32 應用程式中裝載視覺物件

邏輯樹狀結構的關聯性

WPF 中的邏輯樹狀結構代表執行階段應用程式的元素。 雖然您不會直接操作此樹狀結構,但應用程式的這個檢視適合用來了解屬性繼承和事件路由。 不同於視覺化樹狀結構,邏輯樹狀結構可以代表非視覺化資料物件,例如 ListItem。 在許多案例中,邏輯樹狀結構會非常密切地對應至應用程式的標記定義。 下列程式碼顯示標記中定義的 DockPanel 元素。

<DockPanel>
  <ListBox>
    <ListBoxItem>Dog</ListBoxItem>
    <ListBoxItem>Cat</ListBoxItem>
    <ListBoxItem>Fish</ListBoxItem>
  </ListBox>
  <Button Click="OnClick">OK</Button>
</DockPanel>

如果您要列舉在標記範例中組成 DockPanel 元素的邏輯物件,您會發現下面說明的邏輯物件階層:

樹狀結構圖表
邏輯樹狀結構的圖表

視覺化樹狀結構和邏輯樹狀結構會與目前的應用程式元素集合同步處理,以反映元素的任何新增、刪除或修改。 不過,樹狀結構會呈現不同的應用程式檢視。 不同於視覺化樹狀結構,邏輯樹狀結構不會展開控制項的 ContentPresenter 元素。 這表示針對相同的物件集合,邏輯樹狀結構和視覺化樹狀結構之間沒有直接的一對一對應。 事實上,叫用 LogicalTreeHelper 物件的 GetChildren 方法和 VisualTreeHelper 物件的 GetChild 方法時,使用與參數相同的元素會產生不同的結果。

如需邏輯樹狀結構的詳細資訊,請參閱 WPF 中的樹狀結構

使用 XamlPad 檢視視覺化樹狀結構

WPF 工具 XamlPad 可讓您選擇檢視並探索對應至目前定義之 XAML 內容的視覺化樹狀結構。 按一下功能表列上的 [顯示視覺化樹狀結構] 按鈕以顯示視覺化樹狀結構。 下圖說明在 XamlPad 的 [視覺化樹狀結構總管] 面板中,XAML 內容展開為視覺化樹狀結構節點:

XamlPad 中的視覺化樹狀結構總管面板

注意 LabelTextBoxButton 控制項各自如何在 XamlPad 的 [視覺化樹狀結構總管] 面板中顯示個別的視覺物件階層。 這是因為 WPF 控制項具有包含該控制項視覺化樹狀結構的 ControlTemplate。 當您明確地參考控制項時,您會以隱含方式參考其視覺階層。

分析視覺效能

WPF 提供一套效能分析工具,可讓您分析應用程式的執行階段行為,並判斷您可以套用的效能最佳化類型。 Visual Profiler 工具透過直接對應至應用程式的視覺化樹狀結構,提供豐富的效能資料的圖形化檢視。 在這個螢幕擷取畫面中,視覺分析工具的 [CPU 使用率] 區段可提供您物件使用 WPF 服務 (例如轉譯和版面配置) 的精確明細。

Visual Profiler 顯示輸出
Visual Profiler 顯示輸出

Visual 轉譯行為

WPF 導入幾個會影響視覺物件轉譯行為的功能:保留模式圖形、向量圖形和裝置獨立圖形。

保留模式圖形

了解視覺物件角色的其中一個關鍵就是要了解「直接模式」和「保留模式」圖形系統之間的差異。 以 GDI 或 GDI+ 為基礎的標準 Win32 應用程式使用直接模式圖形系統。 這表示應用程式負責重新繪製因為像是重新調整視窗大小或者物件正在變更其視覺外觀等動作而無效的用戶端區域部分。

Win32 轉譯序列的圖表

相反地,WPF 使用保留模式系統。 這表示具有視覺外觀的應用程式物件會定義一組序列化繪圖資料。 一旦定義繪圖資料,系統此後就會負責回應轉譯應用程式物件的所有重新繪製要求。 即使在執行階段,您也可以修改或建立應用程式物件,並仍需依賴系統以回應繪製要求。 保留模式圖形系統的能力在於繪製資訊一律由應用程式以序列化狀態持續保存,但是轉譯的責任屬於系統。 下圖顯示應用程式如何依賴 WPF 來回應繪製要求。

WPF 轉譯序列的圖表

智慧型重新繪製

使用保留模式圖形的一個最大的優點就是 WPF 可以有效率地最佳化應用程式中需要重新繪製的項目。 即使您有一個具有不同層級不透明度的複雜場景,通常也不需要撰寫特殊用途程式碼來最佳化重新繪製作業。 與 Win32 程式設計比較,在 Win32 程式設計中您可能會花費大量精力,透過將更新區域中重新繪製的次數降至最低,來最佳化您的應用程式上。 如需在 Win32 應用程式中最佳化重新繪製所牽涉之複雜性類型的範例,請參閱在更新區域中重新繪製

向量圖形

WPF 使用向量圖形作為其轉譯資料格式。 向量圖形 (包含可縮放向量圖形 (SVG)、Windows 中繼檔案 (.wmf) 和 TrueType 字型) 會儲存轉譯資料並將它以說明如何使用圖形基元重新建立影像的指示清單來傳輸。 例如,TrueType 字型是邊框字型,可描述一組線條、曲線和命令,而非像素陣列。 向量圖形的其中一個主要優點就是能夠縮放為任何大小和解析度。

與向量圖形不同,點陣圖圖形將轉譯資料儲存為影像的逐像素 (pixel-by-pixel) 表示法,針對特定解析度預先轉譯。 點陣和向量圖形格式之間的其中一個主要差異是原始來源影像的逼真度。 例如,當來源影像的的大小經過修改,點陣圖圖形系統會伸展影像,而向量圖形系統則是縮放影像,因此保留影像逼真度。

下圖顯示已將大小調整為 300% 的來源影像。 請注意,當來源影像是以點陣圖圖形影像伸展而非以向量圖形影像縮放時,會出現扭曲。

點陣圖形和向量圖形之間的差異

下列標記顯示已定義的兩個 Path 元素。 第二個元素會使用 ScaleTransform,將第一個元素的繪製指示大小調整為 300%。 請注意,Path 元素中的繪製指示會保持不變。

<Path
  Data="M10,100 C 60,0 100,200 150,100 z"
  Fill="{StaticResource linearGradientBackground}"
  Stroke="Black"
  StrokeThickness="2" />

<Path
  Data="M10,100 C 60,0 100,200 150,100 z"
  Fill="{StaticResource linearGradientBackground}"
  Stroke="Black"
  StrokeThickness="2" >
  <Path.RenderTransform>
    <ScaleTransform ScaleX="3.0" ScaleY="3.0" />
  </Path.RenderTransform>
</Path>

有關解析度和裝置獨立圖形

決定螢幕上文字和圖形大小的系統因素有兩個:解析度和 DPI。 解析度描述螢幕上顯示的像素數目。 解析度越高,像素越小,使得圖形和文字看起來比較小。 顯示在設定為 1024 x 768 的監視器上的圖形,當解析度變更為 1600 x 1200 時,看起來會更小。

另一個系統設定 DPI 則描述以像素為單位的畫面英吋大小。 大部分 Windows 系統的 DPI 為 96,這表示畫面英吋為 96 像素。 提高 DPI 設定會讓畫面英吋更大;降低 DPI 則讓畫面英吋更小。 這表示畫面英吋與實際英吋不相同,在大部分的系統上,通常都不相同。 當您提高 DPI,DPI 感知的圖形和文字會變得更大,因為您已經提高畫面英吋的大小。 提高 DPI 可讓文字更方便閱讀,特別是在高解析度時。

並非所有應用程式都是 DPI 感知:某些應用程式使用硬體像素作為主要度量單位;變更系統 DPI 不會影響這些應用程式。 許多其他的應用程式會使用 DPI 感知單位來描述字型大小,但是使用像素來描述所有其他項目。 DPI 太小或太大可能造成這些應用程式的版面配置問題,因為應用程式的文字是隨著系統的 DPI 設定縮放,但應用程式的 UI 則否。 針對使用 WPF 開發的應用程式,此問題已解決。

WPF 支援自動縮放,方法是使用裝置獨立像素作為其主要度量單位,而非硬體像素;圖形和文字會適當地縮放,應用程式開發人員不需要執行任何額外工作。 下圖顯示以不同 DPI 設定顯示 WPF 文字和圖形之方式的範例。

不同 DPI 設定時的圖形和文字
不同 DPI 設定時的圖形和文字

VisualTreeHelper 類別

VisualTreeHelper 類別是靜態協助程式類別,可提供進行視覺物件層級程式設計的低階功能,適用於非常特殊的情況,例如開發高效能的自訂控制項。 在大部分情況下,較高層級的 WPF 架構物件 (例如 CanvasTextBlock) 可提供更大的彈性和易用性。

點擊測試

當預設點擊測試支援不符合您的需求時,VisualTreeHelper 類別提供在視覺物件上進行點擊測試的方法。 您可以使用 VisualTreeHelper 類別中的 HitTest 方法來判斷幾何或點座標值是否在指定物件 (例如控制項或圖形元素) 的界限內。 例如,您可以使用點擊測試來判斷物件的週框矩形內的滑鼠點擊是否落於圓形的幾何內。您也可以選擇覆寫預設點擊測試實作,以執行您的自訂點擊測試計算。

如需點擊測試的詳細資訊,請參閱視覺分層中的點擊測試

列舉視覺化樹狀結構

VisualTreeHelper 類別提供用來列舉視覺化樹狀結構成員的功能。 若要擷取父代,請呼叫 GetParent 方法。 若要擷取視覺物件的子系或直接子系,請呼叫 GetChild 方法。 這個方法會傳回指定索引處之父代的子 Visual

下列範例示範如何列舉視覺物件的所有子系,如果您對將視覺物件階層的所有轉譯資訊序列化感興趣,這也會是您想要使用的技術。

// Enumerate all the descendants of the visual object.
static public void EnumVisual(Visual myVisual)
{
    for (int i = 0; i < VisualTreeHelper.GetChildrenCount(myVisual); i++)
    {
        // Retrieve child visual at specified index value.
        Visual childVisual = (Visual)VisualTreeHelper.GetChild(myVisual, i);

        // Do processing of the child visual object.

        // Enumerate children of the child visual object.
        EnumVisual(childVisual);
    }
}
' Enumerate all the descendants of the visual object.
Public Shared Sub EnumVisual(ByVal myVisual As Visual)
    For i As Integer = 0 To VisualTreeHelper.GetChildrenCount(myVisual) - 1
        ' Retrieve child visual at specified index value.
        Dim childVisual As Visual = CType(VisualTreeHelper.GetChild(myVisual, i), Visual)

        ' Do processing of the child visual object.

        ' Enumerate children of the child visual object.
        EnumVisual(childVisual)
    Next i
End Sub

在大多數情況下,邏輯樹狀結構是 WPF 應用程式中更有用的元素表示法。 雖然您不會直接修改邏輯樹狀結構,但應用程式的這個檢視適合用來了解屬性繼承和事件路由。 不同於視覺化樹狀結構,邏輯樹狀結構可以代表非視覺化資料物件,例如 ListItem。 如需邏輯樹狀結構的詳細資訊,請參閱 WPF 中的樹狀結構

VisualTreeHelper 類別提供用來傳回視覺物件週框的方法。 您可以呼叫 GetContentBounds 來傳回視覺物件的週框。 您可以呼叫 GetDescendantBounds 來傳回視覺物件所有子系 (包括視覺物件本身) 的週框。 下列程式碼示範如何計算視覺物件及其所有子系的週框矩形。

// Return the bounding rectangle of the parent visual object and all of its descendants.
Rect rectBounds = VisualTreeHelper.GetDescendantBounds(parentVisual);
' Return the bounding rectangle of the parent visual object and all of its descendants.
Dim rectBounds As Rect = VisualTreeHelper.GetDescendantBounds(parentVisual)

另請參閱