Win10 UWP開發中的重復性靜態UI繪制小技巧 1


介紹

在Windows 10 UWP界面實現的過程中,有時會遇到一些重復性的、靜態的界面設計。比如:畫許多等距的線條,畫一圈時鍾型的刻度線,同特別的策略排布元素,等等。

讀者可能覺得這些需求十分簡單,馬上就想到了通過for循環之類來實現。只需要在Loaded事件里添上這些元素就好了。

但這樣可能存在一些問題——如果這些UI元素只是靜態的,是裝飾性的——雖然code-behind不用白不用,但為了這些純靜態元素將代碼邏輯變得臃腫似乎略有不妥。

我們將就這些問題為讀者們介紹一些重復性的靜態界面繪制小技巧。

 

Shape.StrokeDashArray屬性

Windows.UI.Xaml.Shapes.Shape基類,以及繼承自它的Ellipse、Line、Path、Rectangle等類,都具有一些Stroke****之名的屬性,可以實現描邊效果。其中有一個比較特別的StrokeDashArray屬性,它能實現虛線型的描邊效果,加以擴展的話是我們實現重復性UI繪制的好幫手。

在XAML中,這一屬性表現為形如”1,2,3,4”的字符串格式,而本質上它是一個DoubleCollection。其中的數值兩兩配對,依次表示虛線的短划線和空白間隔的長度,並且能周期性地出現。如果數值個數只有奇數個,那么匹配不滿的那一組中,空白間隔的長度將和短划線的長度一致。

PS:關於這一屬性的具體語法,UWP MSDN[1]沒有很詳細的描述。不過舊版本的API MSDN[2]中有對其語法的接受,這部分內容可以參考舊版本的頁面。

 

直觀地看一下這個屬性的使用:

<Line Stroke="DeepSkyBlue" StrokeThickness="5" X2="400" StrokeDashArray="1,2,3,4"/>

可以看到構成了短划線和空白間隔長度依次為1,2,3,4的虛線。這里是指的單位長度,和StrokeThickness屬性有關,該屬性的值會被作為單位長度。而Line長度為400,故可以看到虛線按設定形成了8段。

 

我們現在修改一下參數:

<Line Stroke="DeepSkyBlue" StrokeThickness="50" X2="400" StrokeDashArray="0.1"/>

現在短划線和間隔的長度都是0.1單位長度,而當前的單位長度是50(也導致線段寬度更大,現在看起來像是並列的豎線了)。

我們還可以算出虛線段的數量為: 400 ÷((0.1+0.1) × 50) =40 段。

 

發散一下:

靈活運用這種方式,可以在XAML里直接畫出一些重復的UI元素了,比如這樣:

<Grid>
    <Grid Width="400" Height="200">
        <Canvas>
            <Line X2="400"
                  Y2="400"
                  Stroke="Red"
                  StrokeThickness="570"
                  StrokeDashArray="0.02 0.06">
                <Line.Clip>
                    <RectangleGeometry Rect="0 0 400 200" />
                </Line.Clip>
            </Line>
            <Line X2="400"
                  Y2="400"
                  Stroke="Blue"
                  StrokeThickness="570"
                  StrokeDashArray="0.02 0.06"
                  StrokeDashOffset="0.04">
                <Line.Clip>
                    <RectangleGeometry Rect="0 0 400 200"/>
                </Line.Clip>
            </Line>
        </Canvas>
        <Rectangle Margin="10" Fill="White" />
        <TextBlock FontSize="60" Margin="200,100,0,0">Hello!</TextBlock>
    </Grid>
</Grid>

這里Line是呈45°的,並將它的描邊寬度設的很大(超過400*400矩形的對角線長度),並用Clip限定400*200矩形范圍。

其中還用到了StrokeDashOffset屬性來設定StrokeDashArray的初始偏移量,這里也是指單位長度。

 

還可以用於Ellipse:

<Grid>
    <TextBlock Margin="20" Foreground="White" HorizontalAlignment="Center" FontSize="20">坐和放寬</TextBlock>
    <Grid Width="200" Height="200">
        <Ellipse Stroke="Gray" StrokeThickness="3"/>
        <!-- Wow! -->
        <Ellipse Stroke="DeepSkyBlue" StrokeThickness="3" StrokeDashArray="61.89,1000" RenderTransformOrigin="0.5,0.5">
            <Ellipse.RenderTransform>
                <RotateTransform Angle="-90"/>
            </Ellipse.RenderTransform>
        </Ellipse>
        <TextBlock Foreground="White" FontSize="50" HorizontalAlignment="Center" VerticalAlignment="Center">30%</TextBlock>
    </Grid>
</Grid>

這段XAML中設置的StrokeDashArray="61.89,1000"可能很讓人摸不着頭腦。

我們可以看出這個圓形長寬都是200,周長最多不過600多,我們將代表空白部分的值設置為1000(遠大於200π),用於將進度條未滿的部分全部作為空白部分,隱藏掉。

至於前面一個數值是如何計算的,過程比較復雜:

首先要考慮的是圓的周長,但這是否是上面說的200π呢?實際上不是。200是來自於我們在XAML里設定的Width和Height,但Stroke在計算時采用的是ActualWidth和ActualHeight,這里可以理解為Shape控件的中心線段,即是我們在XAML設計器里選中一個Shape控件后可以看見的這條線(箭頭所指):

因此這里的ActualWidth = Width – StrokeThickness = 200 - 3 = 197。

再以此計算30%進度條的長度為:197π × 30% ÷ 3 = 61.889(不要忘了除以單位長度~)。

 

我們在項目里遇到的情況是:

需要畫出精准的時鍾刻度,一圈分針一圈時針:

<Grid>
    <Grid.Resources>
        <design:CircleStrokeDashArrayConverter x:Key="dashConverter"/>
    </Grid.Resources>

    <Grid HorizontalAlignment="Center" VerticalAlignment="Center"
          CacheMode="BitmapCache">
        <Ellipse Width="200" Height="200" 
                 StrokeThickness="8" 
                 Stroke="DeepSkyBlue" 
                 StrokeDashOffset="0.1"
                 StrokeDashArray="{Binding Converter={StaticResource dashConverter},
ConverterParameter=60,
Path=.,
RelativeSource={RelativeSource Mode=Self}}
"/> <Ellipse Width="200" Height="200" StrokeThickness="10" Stroke="DeepSkyBlue" StrokeDashOffset="0.2" StrokeDashArray="{Binding Converter={StaticResource dashConverter}, ConverterParameter=12, Path=., RelativeSource={RelativeSource Mode=Self}}"/> </Grid> </Grid>

大家注意到了這段XAML中應用了一個自定義的IValueConverter——CircleStrokeDashArrayConverter。此類源碼如下:

public class CircleStrokeDashArrayConverter : IValueConverter
{ 
    public object Convert(object value, Type targetType, object parameter, string language)
    {
        Debug.Assert(value is Shape);
        Shape shape = (Shape)value;
        double segNum = double.Parse(parameter.ToString());
 
        double offset = shape.StrokeDashOffset;
        double width = shape.Width;
        double thickness = shape.StrokeThickness;
 
        double visibleLen = offset * 2;
double length = (width - thickness) * Math.PI / segNum / thickness; return new DoubleCollection(new [] { visibleLen, (length - visibleLen) }); } public object ConvertBack(object value, Type targetType, object parameter, string language) { throw new NotImplementedException(); } }

使用時我們需要在Binding中將Ellipse本身傳入converter。Converter從Ellipse的屬性設置中獲取信息完成StrokeDashArray的構造(注意StrokeDashArray屬性的Binding要寫在最后,這樣才能獲取到正確的值),StrokeDashOffset導致的偏移是和Shape線條的方向相反的,我們用它來設定每段短划線的長度。並配合ConverterParameter(設定分段數)達到復用性。

 

PS:只能用於正圓。橢圓的周長無法簡單計算。其他的Shape就更不適用了。

對於這些無法計算path長度的Shape,需要自己調整和估計其長度。

 

不過StrokeDashArray的使用還有一個小問題

StrokeDashArray畫出的線段,在分段處的切口是和中心線段的切線垂直的(平行法線方向)。直觀印象如下:

(此為“坐和放寬”進度條終點處放大圖)

可以想象,包括我們畫出的鍾盤也是這種情況,每一個刻度並不是一道“線段”,而是一個小小的“扇形”,雖然在視覺上並不明顯。

 

其實這並不是什么嚴重的問題啦,當然有時會的確不能符合需求。就此我們將在另一篇博文中介紹更完善、更適用的解決方案。

 

參考

  1. Shape.StrokeDashArray屬性:https://msdn.microsoft.com/en-us/library/windows.ui.xaml.shapes.shape.strokedasharray.aspx
  2. Shape.StrokeDashArray屬性(for Sliverlight。語法詳細說明):https://msdn.microsoft.com/en-us/library/bb980148(v=vs.95).aspx


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM