1. 程式人生 > >【WPF學習】第三十九章 理解形狀

【WPF學習】第三十九章 理解形狀

  在WPF使用者介面中,繪製2D圖形內容的最簡單方法是使用形狀(shape)——專門用於表示簡單的直線、橢圓、矩形以及多變形的一些類。從技術角度看,形狀就是所謂的繪圖圖元(primitive)。可組合這些基本元素來建立更復雜的圖形。

  關於WPF中形狀的重要細節是,它們都繼承自FrameworkElement類。因此,形狀是元素。這樣會帶來許多重要的結果:

  •   形狀繪製自身。不需要管理無效的情況和繪圖過程。例如,當移動內容、改變視窗尺寸或改變形狀屬性時,不需要手動重新繪製形狀。
  •   使用與其他元素相同的方式組織形狀。換句話說,可在前面學過的任何佈局容器中放置形狀(儘管Canvas明顯是最有用的容器,因為它允許在特定的座標位置放置形狀,當構建複雜的具有多個部分的圖畫時,這很重要)。
  •   形狀支援與其他元素相同的事件。這意味著為了處理焦點、按下鍵盤、移動滑鼠以及單擊滑鼠等,不必執行任何額外工作。可使用用於其他元素的相同事件集,並同樣支援工具提示、上下文選單和拖放操作。

一、Shape類

  每個形狀都繼承自抽象類System.Windows.Shapes.Shape。下圖顯示了形狀類的繼承層次。

 圖 WPF形狀類

  正如上面看到的,相對來說,只有很少一部分類繼承自Shape類。Line、Ellipse以及Rectangle都很直觀,Polyline是一系列相互連線的直線,Polygon是由一系列相互連線的直線形成的閉合圖形。最後,Path類功能強大,能將多個基本形狀組合成單獨的元素。

  儘管Shape類自身不能執行任何工作,但它定義了少量的重要屬性。下表列出了這些屬性。

表 Shape類的屬性

 二、矩形和橢圓

  矩形和橢圓是兩個最簡單的形狀。為建立矩形或橢圓,需要設定大家熟悉的Height和Width屬性(這兩個屬性繼承自FrameworkElement類)來定義形狀的尺寸,然後設定Fill或Stroke屬性(或同時設定這兩個屬性)使形狀可見。還可以使用MinHeigth、MinWidth、HorizontalAlignment、VerticalAlignment以及Margin等屬性。

  下面舉一個簡單示例,該例在StackPanel面板上放置了一個橢圓和一個矩形,效果圖如下所示:

<StackPanel>
            <Ellipse Fill="Yellow" Stroke="Blue" Height="50" Width="100" Margin="5" HorizontalAlignment="Left"></Ellipse>
            <Rectangle Fill="Yellow" Stroke="Blue" Height="50" Width="100" Margin="5" HorizontalAlignment="Left"></Rectangle>
</StackPanel>

   Ellipse類沒有增加任何屬性。Rectangle類只增加了兩個屬性:RadiusX和RadiusY。如果將這兩個屬性的值設為非零值,就可以創建出美觀的圓形拐角。

  可認為RadiusX和RadiusY屬性是用於填充矩形拐角的橢圓。例如,如果將這兩個屬性都設為10,WPF會使用10個單位寬的圓形邊緣繪製拐角。隨著半徑的增大,矩形拐角的更多部分會被替換。如果增加RadiusY屬性的值,使其大於RadiusX屬性的值,矩形拐角的左邊和右邊會更平緩,而頂部和底邊的邊緣會更尖銳。如果增大RadiusX屬性的值,使其等於矩形寬度,並增加RadiusY屬性的值,使其等於矩形的寬度,矩形最後會變成普通的橢圓。如下圖所示:

<Window x:Class="Drawing.RoundedRectangles"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="RoundedRectangles" Height="447.744" Width="300">
    <StackPanel>
        <TextBlock Margin="5,5,0,0">Corner radius of 5.</TextBlock>
        <Rectangle Fill="Yellow" Stroke="Blue" RadiusX="5" RadiusY="5"
               Width="100" Height="60" Margin="5"  HorizontalAlignment="Left">
        </Rectangle>
        <TextBlock Margin="5,5,0,0">Corner radius of 10.</TextBlock>
        <Rectangle Fill="Yellow" Stroke="Blue" RadiusX="10" RadiusY="10"
               Width="100" Height="60" Margin="5"  HorizontalAlignment="Left"></Rectangle>
        <TextBlock Margin="5,5,0,0">Corner radius of 10 (X) and 25 (Y).</TextBlock>
        <Rectangle Fill="Yellow" Stroke="Blue" RadiusX="10" RadiusY="25"
               Width="100" Height="60" Margin="5"  HorizontalAlignment="Left"></Rectangle>
        <TextBlock Margin="5,5,0,0">Corner radius of 100 (X) and 60 (Y).</TextBlock>
        <Rectangle Fill="Yellow" Stroke="Blue" RadiusX="100" RadiusY="60"
               Width="100" Height="60" Margin="5"  HorizontalAlignment="Left"></Rectangle>
    </StackPanel>
</Window>
RoundedRectangles

 三、改變形狀的尺寸和放置形狀

  正如前面所知,贏編碼尺寸通常不是建立使用者介面的理想方法。它們會限制處理動態內容的能力,並會使應用程式本地化到其他語言變得更加困難。

  當繪製形狀時,不再總是關心這些問題。通常,需要更嚴格地控制形狀的位置。然而,在許多情況下仍需要靈活一點設計。Ellipse和Rectangle為了適應可用的空間,都能自動改變自身。

  如果為提供Height和Width屬性,形狀會根據它們的容器來設定自身的尺寸。在上一個示例中,如果刪除Height和Width值(並且不設定MinHeight和MinWidth值),就會導致形狀縮小到看不見,因為StackPanel面板為了適應其內容改變了尺寸。然而,如果強制StackPanel面板的寬度為整個視窗的寬度(通過將HorizontalAlignment屬性設定為Stretch),並將橢圓的HorizontalAlignment屬性設定為Stretch,刪除橢圓的Width屬性值,這時橢圓的寬度就是整個視窗的寬度。

  可使用Grid容器構造更好的示例。如果使用按比例改變行尺寸的行為(預設行為),就可使用下面更精簡的標記建立填滿視窗的橢圓:

<Grid>
    <Ellipse Fill="Yellow" Stroke="Blue"></Ellipse>
</Grid>

  在上面的標記中,Grid面板填滿了整個視窗。Grid面板包含了一個按比例改變尺寸的行,該行填滿了整個Grid面板。最後,橢圓填滿了整行。

  改變形狀尺寸的行為依賴於Stretch屬性的值(該屬性在Shape類中定義)。預設情況下,該屬性被設定為Fill。如果改變指定明確的尺寸,這一設定會拉伸形狀,使其填滿容器。下表列出了Stretch屬性的所有可能值。

表 Stretch列舉值

   下圖顯示了Fill、Uniform、UniformToFill列舉值之間的區別.

<Window x:Class="Drawing.FillModes"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="FillModes" Height="270" Width="477"
    >
    <Grid ShowGridLines="True" Margin="5">
        <Grid.ColumnDefinitions>
            <ColumnDefinition></ColumnDefinition>
            <ColumnDefinition></ColumnDefinition>
            <ColumnDefinition></ColumnDefinition>
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition></RowDefinition>
            <RowDefinition Height="Auto"></RowDefinition>
        </Grid.RowDefinitions>

        <Ellipse Fill="Yellow" Stroke="Blue"></Ellipse>
        <Ellipse Fill="Yellow" Stroke="Blue" Grid.Column="1" Stretch="Uniform"></Ellipse>
        <Ellipse Fill="Yellow" Stroke="Blue" Grid.Column="2" Stretch="UniformToFill "></Ellipse>

        <TextBlock Grid.Row="1" TextAlignment="Center">Fill</TextBlock>
        <TextBlock Grid.Row="1" Grid.Column="1" TextAlignment="Center">Uniform</TextBlock>
        <TextBlock Grid.Row="1" Grid.Column="2" TextAlignment="Center">UniformToFill</TextBlock>
    </Grid>
</Window>
FillModes

 

   通常,將Stretch的值設定為Fill相當於將HorizontalAlignment和VerticalAlignment屬性設定為Stretch。但如果選擇為形狀設定固定的寬度和高度,二者就有區別了。對於這種情況,會簡單地忽略HorizontalAlignment和VerticalAlignment值。而Stretch設定仍然起作用——該屬性決定如何在給定的範圍內改變形狀內容的尺寸。

  到目前位置,已看到如何改變Rectangle和Ellipse形狀的尺寸,但如何準確地將它們放到期望的位置呢?WPF形狀與其他元素使用相同的佈局系統。然而,有些佈局容器是不合適的。例如,通常不希望使用StackPanel、DockPanel以及WrapPanel面板,因為它們都被設計為獨立的元素。Grid面板更靈活一些,因為它允許在同一個單元格中放置任意多個元素(儘管不能在單元格中的不同部分定位矩形和橢圓)。理想容器是Canvas,該容器要求使用Left、Top、Right或Bottom附加屬性,為每個形狀指定座標。這樣可以完全控制形狀如何相互重疊:

<Canvas>
        <Ellipse Fill="Yellow" Stroke="Blue" Height="50" Width="100" Canvas.Top="50" Canvas.Left="100"></Ellipse>
        <Rectangle Fill="Yellow" Stroke="Blue" Height="50" Width="100" Canvas.Top="40" Canvas.Left="30"></Rectangle>
    </Canvas>

  如果使用Canvas容器,標籤的順序是很重要的。在上面的示例中,矩形疊加在橢圓之上,因為在標籤列表中首先出現的是橢圓,所以首先繪製橢圓。

   請記住,Canvas容器不在需要佔據整個視窗。例如,完全可以建立一個Grid面板,並在該Grid面板的某個單元格中使用Canvas容器,對於在可自由流動的動態使用者介面中鎖定一小部分繪圖邏輯,這是一種非常好的方法。

四、使用Viewbox控制元件縮放形狀

  使用Canvas控制元件的唯一限制是圖形不能改變自身的尺寸以適應更大或更小的視窗。對於按鈕這非常合理(在這些情況下,按鈕不改變尺寸),但是對於其他類似的圖形內容,情況就未必如此了。

  對於此類情況,WPF提供了簡便的解決方法。如果希望聯合Canvas控制元件的精確控制功能和方便的改變尺寸功能,可使用Viewbox元素。

  Viewbox是繼承自Decorator的簡單類。該類只接受一個子元素,並拉伸或縮小子元素以適應可用的空間。當然,這個單一的子元素可以是佈局容器,其中包含大量形狀(或其他元素),這些元素將同步地改變尺寸。然而,Viewbox更長用於向量影象而不是普通控制元件。

  儘管可在Viewbox元素中放置單個形狀,但這並不能提供任何實際的優點。反而,當需要封裝構成一幅圖畫(drawing)的一組形狀時,Viewbox元素才有用處。通常,將在Viewbox控制元件中放置Canvas,並在Canvas面板中放置形狀。

  下面的示例在Grid控制元件的第二行中放置了一個包含Canvas面板的Viewbox元素。Viewbox元素佔用改行的整個高度和寬度。該行佔用繪製自動改變尺寸的第一行剩餘的所有空間,下面是標記:

<Grid Margin="5">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"></RowDefinition>
            <RowDefinition Height="*"></RowDefinition>
        </Grid.RowDefinitions>
        <TextBlock> The first row of a grid</TextBlock>
        <Viewbox Grid.Row="1" HorizontalAlignment="Left" MaxHeight="500">
            <Canvas Width="200" Height="150">
                <Ellipse Fill="Yellow" Stroke="Blue" Canvas.Left="10"  Canvas.Top="50"
               Width="100" Height="50" HorizontalAlignment="Left"></Ellipse>
                <Rectangle Fill="Yellow" Stroke="Blue" Canvas.Left="30"  Canvas.Top="40"                 
                 Width="100" Height="50" HorizontalAlignment="Left"></Rectangle>
            </Canvas>
        </Viewbox>
    </Grid>

  下圖顯示了當改變視窗尺寸時,Viewbox控制元件如何調整自身。第一行沒有變化。然而為填滿額外控制元件,第二行進行了擴充套件。正如看到的,Viewbox控制元件中的形狀也根據視窗增大的比例改變了他們的大小。

  

   預設情況下,Viewbox元素按比例地執行縮放,保持它所包含內容的縱橫比。在當前示例中,這意味著即使包含行的形狀發生了變化(變寬或變高),內部形狀也不會變形。相反,Viewbox元素使用適應可用空間內部的最大縮放係數。然而,可使用Viewbox.Stretch屬性改變該行為。預設情況下,將該屬性設定為Uniform。可將其改變為Fill,Viewbox元素中的內容會在兩個方向上被拉伸以完全適應可用空間,即使可能會破壞原來的繪圖也會如此。還可通過使用StretchDirection屬性獲得更大的控制權。預設情況下,該屬性被設定為Both,但可使用UpOnly值建立只會增長而不會收縮超過其原始尺寸的內容,並且可以使用DownOnly建立只會縮小而不會增長的內容。

  為時Viewbox元素執行其縮放工作,需要能夠確定兩部分資訊:(如果不放在Viewbox元素中)內容應當具有的原始尺寸和希望內容具有的新尺寸。

  第二個細節——新尺寸——非常簡單。Viewbox元素根據Stretch屬性,讓其內部的內容使用所有可用空間,這意味著Viewbox元素越大,其內部的內容就越大。

  第一個細節——原始尺寸,不使用Viewbox空間時的尺寸——隱含在定義巢狀內容的方式中。在前面的示例中,Canvas的尺寸被明確設定為200X150單位大小。因此,Viewbox從該開始點縮放影象。例如,橢圓最初是100單位寬,這意味著它佔用Canvas面板一半的繪圖空間。隨著Canvas控制元件的增大,Viewbox元素會遵循這些比例,並且橢圓繼續佔用一半的可用控制元件。

  然而,如果刪除Canvas控制元件的Width和Height屬性,分析會發生什麼情況。現在,Canvas控制元件的尺寸被設定為0X0單位大小,所以Viewbox控制元件不能改變它的尺寸,並且巢狀在其中的內容不會顯示(這與只使用Canvas控制元件時的行為不同。因為儘管Canvas控制元件的尺寸仍設定為0X0,但只要Canvas.ClipToBounds屬性沒有被設定為true,就仍然允許在Canvas控制元件之外的區域繪製形狀。而Viewbox控制元件不能容忍這一錯誤)。

  現在分析一下,如果在按比例改變尺寸的Grid面板的單元格中封裝Canvas面板,並且沒有指定Canvas面板的尺寸,情況又會怎樣。如果沒有使用Viewbox元素,該方法可工作得很好——拉伸Canvas面板以填充單元格,並且內部的內容是可見的。但如果將所有內容放在Viewbox元素中,這種方法就會失效。Viewbox控制元件不能確定最初尺寸,因此不能響應地改變Grid面板的尺寸。

  可通過直接在能自動改變尺寸的容器(如Grid面板)中放置特定的形狀(如Rectangle和Ellipse)來避免這個問題。然後Viewbox控制元件就能評估Grid面板為了適合其內容所需的最小尺寸,並且縮放Grid面板以適應可用空間。然而,在ViewBox元素中獲取真正所希望的尺寸的最簡單方法,是在具有固定尺寸的元素中封裝內容,可以是Canvas面板、按鈕或其他控制元件。這樣,固定尺寸就變成了Viewbox控制元件進行計算所需要的原始尺寸。以這種方式硬編碼尺寸不會限制佈局的靈活性,因為Viewbox元素根據可用空間和佈局容器按比例改變尺寸。

五、直線

  Line形狀表示連線一個點和另一個點的一條直線。起點和重點由4個屬性設定:X1與Y1(用於第一個點)和X2與Y2(用於第二個點)。例如,下面是一條從點(0,0)伸展到點(10,100)的直線:

<Line Stroke="Blue" X1="0" Y1="0" X2="10" Y2="100"></Line>

   對於直線,Fill屬性不起作用,必須設定Stroke屬性。

  在直線中使用的座標是相對於放置直線的矩形區域左上角的座標。例如,如果在StackPanel面板上放置上面的直線,座標(0,0)指向在StackPanel面板上放置該矩形區域的位置,這可能是視窗的左上角,也可能不是。如果StackPanel面板的Margin屬性值不為0,或直線在其他元素之後,直線的開始點(0,0)與視窗頂部會有一定的距離。

  然而,在直線中使用負座標值是非常合理的。實際上,可為直線使用能超出為直線保留的空間的座標,從而在視窗的其他任意部分繪製直線。對於到目前位置介紹的Rectangle和Ellipse形狀;這是不可能的。然而,這一模型也有缺點,直線不能使用流內容模型。這意味著為直線設定Margin、HorizontalAlignment以及VerticalAlignment屬性是沒有意義的,因為它們沒有任何效果。對於Polyline和Polygon形狀具有相同的限制。

  如果在Canvas面板上放置了Line形狀,那麼仍應用附加的位置屬性(如Top和Left)。它們決定直線的開始位置。換句話說,兩個直線座標被平移一定的距離。分析下面的直線:

<Line Stroke="Blue" X1="0" Y1="0" X2="10" Y2="100"
    Canvas.Left="5" Canvas.Top="100"/>

   這條直線從點(0,0)伸展到點(10,100),使用的座標系統將Canvas控制元件上的點(5,100)作為點(0,0)。這相當於下面不使用Top和Left屬性的直線:

<Line Stroke="Blue" X1="5" Y1="100" X2="15" Y2="200"/>

  當在Canvas面板上放置Line形狀時,是否使用位置屬性由自己決定。通常,可通過選擇好的開始點簡化直線的繪製,還可以使移動部分圖畫變得容易。例如,如果在Canvas面板的特定位置繪製幾條直線和其他形狀,相對於附近的點繪製它們是不錯的主意(通過使用相同的Top和Left座標)。通過這種方法,可根據需要將整個圖畫移到新的位置。

六、折線

  可以通過Polyline類繪製一系列相互連線的直線。只需要使用Points屬性提供一系列X和Y座標。從技術角度看,Points屬性需要使用PointCollection物件,但在XAML中使用基於簡單字串的語法填充該集合。只需要提供點的列表,並在每個座標之間新增空格或逗號。

  Polyline形狀可能只有兩個點。例如下面的Polyline形狀,從點(5,100)伸展到點(15,200):

<Polyline Stroke="Blue" Points="5 100 15 200"/>

  為便於閱讀,可在每個X和Y座標之間使用逗號:

<Polyline Stroke="Blue" Points="5,100 15,200"/>

  下面是繪製的更復雜Polyline形狀。點不斷右移,並在更高的Y值——比如(50,160),和更低的Y值——比如(70,130)之間擺動:

<Canvas>
    <Polyline Stroke="Blue" StrokeThickness="5" 
        Points="10,150 30,140 50,160 70,130 90,170 110,120 130,180 150,110 170,190 190,100 210,240" >
    </Polyline>
</Canvas>

  下圖顯示了最終繪製的線條。

 

   對於這個示例,通過程式碼使用各種相應地自動增加X和Y值的迴圈填充Point集合可能更容易。如果需要建立高度動態的圖形,事實卻是如此——例如,根據從資料庫中提取的資料集改變其外觀的圖示。但是,如果只是希望構建固定的圖形內容,就根本不需要形狀的具體座標。相反,可使用另一個工具,如Express Design,繪製恰當的圖形,然後到處到XAML。

七、多邊形

  實際上,Polygon和Polyline是相同的。和Polyline類一樣,Polygon類也有包含一系列座標的Points集合。唯一的區別是:Polygon形狀新增最後一條線段,將最後一個點連線到開始點(如果最後一個點就是第一個點,Polygon類和Polyline類就沒有區別了)。可使用Fill畫刷填充該形狀的內部區域。通過修改上一節的示例,顯示Polygon:

<Canvas>
        <Polygon Stroke="Blue" StrokeThickness="5" Fill="Yellow" 
        Points="10,150 30,140 50,160 70,130 90,170 110,120 130,180 150,110 170,190 190,100 210,240" >
        </Polygon>
</Canvas>

  最終效果圖如下所示:

 

   對於線條從不相交的簡單形狀,填充其內部很容易做到。但有時會遇到更復雜的Polygon形狀,哪些部分屬於內部(並且應當被填充)以及哪些部分屬於外部並不明顯。

  下面一個示例,該形狀的特點是一條線段和其他多條線段相交,可能希望填充也希望不填充中央的不規則區域。顯然,可通過將該影象分割成更小的形狀來準確地控制填充區域。但不需要這麼做。

 <Polygon Stroke="Blue" StrokeThickness="1" Fill="Yellow" 
        Points="15,200 68,70 110,200 0,125 135,125" >
</Polygon>

 

   每個Polygon和Polyline形狀都有FillRule屬性,該屬性用於從兩種填充方法中選擇一種來填充區域。預設情況下,FillRule屬性被設定為EventOdd。為了確定是否填充區域,WPF計算為了到達形狀的外部必須穿過的直線的數量。如果是奇數,就填充區域;如果是偶數,就不填充區域。對於上圖顯示的中央區域,為了到達形狀外部就必須經過兩條直線,所以不會填充該區域。

  WPF還遵循NonZero填充規則,該規則更加複雜。本質上,當使用NonZero填充規則時,WPF使用和EventOdd填充規則相同的方法計算穿過的直線的數量,但是會考慮經過的每條直線的防線。如果在經過的直線中,在某個方向上(比如從左項右)直線的數量等於相反方向(從右向左)上直線的數量,就不會填充區域。如果這兩個直線數量的差不為0,就填充區域。對於上個示例,如果將FillRule屬性設定為NonZero,J就會填充內部區域。

<Polygon Stroke="Blue" StrokeThickness="1" Fill="Yellow" 
               FillRule="Nonzero"
         Points="15,200 68,70 110,200 0,125 135,125" >
</Polygon>

 

   有關NonZero規則的複雜問題在於填充設定依賴於形狀的繪製,而不是形狀自身的外觀。

八、直線線帽和直線交點

  當繪製Line和Polyline形狀時,可使用StartLineCap和EndLineCap屬性選擇如何繪製直線的開始端和結束端(這些屬性不影響其他形狀,因為其他形狀都是閉合的)。

  StartLineCap和EndLineCap屬性通常都設為Flat,這意味著直線在它的最後座標處立即終止。其他選擇包括Round(該設定會平滑地繪製拐角),Triangle(繪製直線的兩條側邊最後交於一點)以及Square(該設定使直線埠具有尖銳邊緣)。這兩個設定都會增加直線的長度——換句話說,它們使直線超出了其他情況下的結束位置。額外的距離是直線寬度的一半。下圖顯示了直線埠處不同線帽之間的區別。

<Window x:Class="Drawing.LineCaps"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="LineCaps" Height="333" Width="376">
    <Grid Margin="15">
        <Grid.RowDefinitions>
            <RowDefinition></RowDefinition>
            <RowDefinition></RowDefinition>
            <RowDefinition></RowDefinition>
            <RowDefinition></RowDefinition>

        </Grid.RowDefinitions>

        <Grid.ColumnDefinitions>
            <ColumnDefinition></ColumnDefinition>
            <ColumnDefinition Width="Auto"></ColumnDefinition>
        </Grid.ColumnDefinitions>

        <Polyline Stroke="Blue" StrokeThickness="15" StrokeEndLineCap="Flat" SnapsToDevicePixels="True"
      Points="10,10 30,0 50,20 90,10 200,10" >
        </Polyline>
        <TextBlock Grid.Column="1">Flat Line Cap</TextBlock>

        <Polyline Stroke="Blue" StrokeThickness="15" Grid.Row="1" StrokeEndLineCap="Square" SnapsToDevicePixels="True"
      Points="10,10 30,0 50,20 90,10 200,10" >
        </Polyline>
        <TextBlock Grid.Row="1" Grid.Column="1">Square Line Cap</TextBlock>

        <Polyline Stroke="Blue" StrokeThickness="15" Grid.Row="2" StrokeEndLineCap="Round" SnapsToDevicePixels="True"
      Points="10,10 30,0 50,20 90,10 200,10" >
        </Polyline>
        <TextBlock Grid.Row="2" Grid.Column="1">Round Line Cap</TextBlock>

        <Polyline Stroke="Blue" StrokeThickness="15" Grid.Row="3" StrokeEndLineCap="Triangle"  SnapsToDevicePixels="True"
         Points="10,10 30,0 50,20 90,10 200,10" >
        </Polyline>
        <TextBlock Grid.Row="3" Grid.Column="1">Triangle Line Cap</TextBlock>
    </Grid>
</Window>
LineCaps

 

   除Line形狀外,所有形狀都執行使用StrokeLineJoin屬性扭曲它們的拐角,有4中選擇。Miter值(預設值)使用尖銳的邊緣,Bevel值切掉點邊緣,Round值平滑地過渡邊緣,Triangle值顯示尖點。下圖顯示StrokeLineJoin效果圖。

<Window x:Class="Drawing.LineJoins"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="LineJoins" Height="431" Width="303"
    >
    <Grid Margin="15">
        <Grid.RowDefinitions>
            <RowDefinition></RowDefinition>
            <RowDefinition></RowDefinition>
            <RowDefinition></RowDefinition>
            <RowDefinition></RowDefinition>
        </Grid.RowDefinitions>

        <Grid.ColumnDefinitions>
            <ColumnDefinition></ColumnDefinition>
            <ColumnDefinition Width="Auto"></ColumnDefinition>
        </Grid.ColumnDefinitions>

        <Polyline Stroke="Blue" StrokeThickness="14" StrokeLineJoin="Bevel" SnapsToDevicePixels="True"
      Points="10,60 30,10 50,70 90,40" >
        </Polyline>
        <TextBlock Grid.Column="1" VerticalAlignment="Center">Bevel Line Join</TextBlock>

        <Polyline Stroke="Blue" StrokeThickness="14" Grid.Row="1" StrokeLineJoin="Round"  SnapsToDevicePixels="True"
      Points="10,60 30,10 50,70 90,40" >
        </Polyline>
        <TextBlock Grid.Row="1" Grid.Column="1" VerticalAlignment="Center">Round Line Join</TextBlock>

        <Polyline Grid.Row="2" Stroke="Blue" StrokeThickness="14" StrokeLineJoin="Miter"  StrokeMiterLimit="1"
              SnapsToDevicePixels="True" 
      Points="10,60 30,10 50,70 90,40" >
        </Polyline>
        <TextBlock Grid.Row="2" Grid.Column="1" VerticalAlignment="Center">Miter Line Join</TextBlock>

        <Polyline Grid.Row="3" Stroke="Blue" StrokeThickness="14" StrokeLineJoin="Miter"  StrokeMiterLimit="3"
                SnapsToDevicePixels="True" 
        Points="10,60 30,10 50,70 90,40" >
        </Polyline>
        <TextBlock Grid.Row="3" Grid.Column="1" VerticalAlignment="Center">Miter Line Join With Limit of 3</TextBlock>

    </Grid>
</Window>
LineJoins

 

   當為較寬並且角度非常小的直線拐角使用尖銳的邊緣時,尖銳的拐角會不切實際地延伸很長一段距離。對於這種情況,可使用Bevel或Round設定修剪拐角。也可使用StrokeMiterLimit屬性,當達到特定的最大長度時,該屬性自動地剪下邊緣。StrokeMiterLimit屬性是一個係數,該係數是用於銳化拐角的長度和直線寬度的一半的比值。如果將該屬性設定為1(這是預設值),就允許拐角延長直線寬度的一半距離。如果設定為3,就允許拐角延長直線寬度的1.5倍距離。如上圖的最後一條直線使用了更高的銳化範圍,從而具有更狹長的拐角。

九、點劃線

  除了為形狀的邊框繪製乏味的實線外,還可繪製點劃線(dashed line)——根據指定的模式使用空白斷開的直線。當在WPF中建立一條點劃線時,不限制進行特定的預先設定。相反,可通過設定StrokeDashArray屬性來選擇實線段的長度和斷開空白(空白)的長度。例如,分析下面的這條直線:

<Polyline Stroke="Blue" StrokeThickness="10"
              StrokeDashArray="1 2" 
      Points="10,30 60,0 90,40 120,10 350,10" SnapsToDevicePixels="True">
        </Polyline>

  這條點劃線的實線段長度值為1,空白長度值為2.這些值都是相對於直線寬度的。因此,如果直線寬度是10個單位(本例中設定的寬度),實線部分的長度就為10個單位,後面跟著20個單位的空白部分。直線在整個長度中重複該模式。

  另一方面,如果像下面這種交換這兩個值:

StrokeDashArray="2 1"

  直線的實現部分就是20個單位長,空白部分為10個單位長。下圖顯示了這著兩條直線。正如將會注意到得,當一條非常粗的線段位於拐角處時,它會被不均勻地割斷。

<Window x:Class="Drawing.DashedLines"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="DashedLines" Height="401" Width="589"
    >
    <Grid Margin="15">
        <Grid.RowDefinitions>
            <RowDefinition></RowDefinition>
            <RowDefinition></RowDefinition>
            <RowDefinition></RowDefinition>
            <RowDefinition></RowDefinition>
            <RowDefinition></RowDefinition>

        </Grid.RowDefinitions>

        <Grid.ColumnDefinitions>
            <ColumnDefinition></ColumnDefinition>
            <ColumnDefinition Width="Auto"></ColumnDefinition>
        </Grid.ColumnDefinitions>

        <Polyline Stroke="Blue" StrokeThickness="10"
              StrokeDashArray="1 2" 
      Points="10,30 60,0 90,40 120,10 350,10" SnapsToDevicePixels="True">
        </Polyline>
        <TextBlock Grid.Column="1" VerticalAlignment="Center">Dash Pattern "1 2"</TextBlock>

        <Polyline Stroke="Blue" StrokeThickness="10" Grid.Row="1" 
               StrokeDashArray="2 1" SnapsToDevicePixels="True"
      Points="10,30 60,0 90,40 120,10 350,10" >
        </Polyline>
        <TextBlock Grid.Row="1" Grid.Column="1" VerticalAlignment="Center">Dash Pattern "2 1"</TextBlock>

        <Polyline Stroke="Blue" StrokeThickness="10" Grid.Row="2"
              StrokeDashArray="5 0.2 3 0.2" SnapsToDevicePixels="True"
      Points="10,30 60,0 90,40 120,10 350,10" >
        </Polyline>
        <TextBlock Grid.Row="2" Grid.Column="1" VerticalAlignment="Center">Dash Pattern "5 0.2 3 0.2"</TextBlock>

        <Polyline Stroke="Blue" StrokeThickness="10" Grid.Row="3" SnapsToDevicePixels="True"
              StrokeDashArray="3 0.5 2"
  Points="10,30 60,0 90,40 120,10 350,10" >
        </Polyline>
        <TextBlock Grid.Row="3" Grid.Column="1" VerticalAlignment="Center">Uneven Dash Pattern "2 0.5 2"</TextBlock>

        <Polyline Stroke="Blue" StrokeThickness="10" Grid.Row="4" SnapsToDevicePixels="True"
             StrokeDashArray="1 2"  StrokeDashCap="Round"
 Points="10,30 60,0 90,40 120,10 350,10" >
        </Polyline>
        <TextBlock Grid.Row="4" Grid.Column="1" VerticalAlignment="Center">Dash Pattern with Rounded Caps</TextBlock>
    </Grid>
</Window>
DashedLines

 

   不見得非要使用整數值。例如,下面的StrokeDashArray屬性設定完全合理:

StrokeDashArray="5 0.2 3 0.2"

  這樣的設定提供了更復雜序列——5X10單位長的點劃線,然後是0.2X15單位長的空白,接下來是3X10單位長的實線和0.2X10單位長的空白。在該序列的尾部,直線從頭開始重複該模式。

  如果為StrokeDashArray屬性提供的數值的個數是奇數,將發生一個有趣的現象。分析下面的示例:

StrokeDashArray="3 0.5 2"

  當繪製該直線時,WPF首先繪製3倍直線寬度長的實線,然後是0.5倍直線寬度長的空白,在接下來時2唄直線寬度長的實線。但當在從頭開始重複該模式時,首先是3倍直線寬度長的空白,接著是0.5被直線寬度長的實線,依次類推。本質上,點劃線線上段和空白之間交替其模式。

  如果希望從中間開始繪製模式,可使用StrokeDashOffset屬性,該屬性是一個從0開始的索引,該索引指向StrokeDashArray中的某個值。例如,在上一個示例中,如果將StrokeDashOffset屬性設定為1,直線將從0.5倍直線寬度長的空白開始。如果設定為2,直線將會從2倍直線寬度長的線段開始。

  最後,可控制如何為直線的斷開邊緣新增線帽。通常是一條平直的邊緣,但可將StrokeDashCap屬性設定為Bevel、Square以及Triangle等值。請記住,所有這些設定都會在點劃線的端點增加直線寬度的一半長距離。如果沒有考慮這一額外的距離,最終可能會使點劃線相互重疊。解決方法是增加額外的空白以進行補償。

十、畫素對齊

  WPF使用與裝置無關的繪圖系統。為字型和形狀等內容指定的數值使用“虛擬”畫素,在通常的96dpi顯示器上,“虛擬”畫素和正常畫素的大小相同,但是在更高dpi的顯示器上其尺寸會被縮放。換句話說,繪製50畫素寬的矩形,根據裝置的不同,實際上可能使用更多或更少的畫素進行渲染。裝置無關單位和物理畫素之間的轉換會自動進行,並且通常根本不需要考慮這個問題。

  不同dpi設定之間的畫素比很少是整數。例如, 在96dpi顯示器上的50個畫素,在120dpi顯示器上會變為62.4996個畫素(這不是一種錯誤的情況——實際上,當以裝置無關單位提供數值時,WPF始終執行使用非整數的雙精度值)。顯然,無法在畫素之間的點上放置一條邊緣。WPF使用反鋸齒特性進行補償。例如,當繪製一條62.4992個畫素長的紅線,WPF可正常填充前62個畫素,然後使用直線顏色(紅色)和背景色之間的顏色為第63個畫素著色。但在此存在一個問題。如果正在繪製直線、矩形或具有直角的多邊形,這種自動反鋸齒特性會在形狀邊緣導致一片模糊區域。

  可能會認為,僅在顯示解析度不是96dpi的顯示器上執行應用程式時,才會出現這個問題。然而,情況未必如此,因為所有形狀都可以都可以使用小數值得長度和座標設定尺寸,這會引起相同的問題。在繪製形狀時,儘管可能沒有使用小數值,但可以改變尺寸的形狀——那些因為尺寸依賴於容器或被放在Viewbox元素中而被拉伸的形狀——尺寸通常幾乎總是小數。類似地,奇數單位寬的直線在兩側的畫素數也是小數值。

  模糊邊緣問題未必是問題。實際上,根據正在繪製的影象型別,它可能看起來很正常。然而,如果不希望這種行為,可告訴WPF不要在特定形狀使用反鋸齒特性進行處理,反而WPF會將尺寸舍入到最近的裝置畫素。可通過將UIElement類的SnapsToDevicePixels屬性設定為true來啟動這個稱為畫素對齊(pixel Snapping)的特性。

  為檢視兩者之間的區別,可觀察下圖中被放大的視窗,該視窗比較兩個矩形。底部的矩形使用了畫素對齊特性,而頂部的矩形沒有使用。如果仔細觀察,就會發現在未使用畫素對齊特性的矩形的頂部和左部有一條很細的淡色邊緣。

<Window x:Class="Drawing.PixelSnapping"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="PixelSnapping" Height="300" Width="300">
    <Grid Margin="7">
        <Grid.RowDefinitions>
            <RowDefinition></RowDefinition>
            <RowDefinition></RowDefinition>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto"></ColumnDefinition>
            <ColumnDefinition></ColumnDefinition>
        </Grid.ColumnDefinitions>

        <TextBlock VerticalAlignment="Center">Not Snapped:</TextBlock>
        <Rectangle SnapsToDevicePixels="False" Grid.Column="1"
      Margin="10" Height="10" Fill="Red"></Rectangle>

        <TextBlock VerticalAlignment="Center" Grid.Row="1">Snapped:</TextBlock>
        <Rectangle SnapsToDevicePixels="True" Grid.Column="1" Grid.Row="1"
      Margin="10" Height="10" Fill="Red"></Rectangle>
    </Grid>
</Window>
PixelSnapping

&n