【翻译】Pro.Silverlight.5.in.CSharp.4th.Edition - 第三章 布局 03
目錄:點擊這里
上一篇:【翻譯】Pro.Silverlight.5.in.CSharp.4th.Edition - 第三章 布局 02
使用Canvas基于坐標布局
到目前為止我們還剩下Canvas沒有學習到。Canvas可以讓我們使用精確坐標給元素設置位置。對于設計一個以數據為主導的窗體和標準對話框來說Canvas不是一個非常合適的選擇,但是如果要創建某些不一樣的內容(比如給圖形控件設計界面)時則可能非常有用。Canvas也是最輕量級的布局容器,因為它在處理它的子元素的尺寸的時候不涉及到特別復雜的布局邏輯,而是按照子元素所設置的精確尺寸和位置直接展現出來。
?????? 我們需要使用Canvas.Left和Canvas.Top這兩個附加屬性來給元素設置在Canvas中的具體位置。Canvas.Left用于設置元素的左邊緣距離Canvas的左邊線的距離的像素值;Canvas.Top用于設置元素的上邊緣距離Canvas的上邊線的距離的像素值。
?????? 實際情況中,我們可以用Width和Height這兩個屬性精確地設置元素的尺寸。而在Canvas這種布局容器中處理的時候則使用得更頻繁,因為相對于其他布局容器來說Canvas沒有自身的布局邏輯。如果我們不設置Width和Height這兩個屬性,那么元素會調整自己的尺寸到合適的值——換句話說,元素的尺寸會增大到能容納元素的所有內容。在這種情況下再調整Canvas的尺寸就對內部元素無效了。
?????? 下面這個示例的Canvas中包含了四個按鈕:
<Canvas Background="White"><Button Canvas.Left="10" Canvas.Top="10" Content="(10,10)"></Button><Button Canvas.Left="120" Canvas.Top="30" Content="(120,30)"></Button><Button Canvas.Left="60" Canvas.Top="80" Width="50" Height="50" Content="(60,80)"></Button><Button Canvas.Left="70" Canvas.Top="120" Width="100" Height="50" Content="(70,120)"></Button> </Canvas>?
?? ? ? ?相應的運行效果如圖3-16所示:
圖3-16 按鈕在Canvas中通過精確的坐標來布局
?????? 和其它布局容器一樣,Canvas也可以嵌套在用戶界面中。這意味著我們可以在頁面中的某部分區域用Canvas來繪制一些細節內容,同時其它的元素用更標準的布局容器來處理。
使用ZIndex分層
如果頁面中的元素出現了重疊,那么我們可以使用附加屬性Canvas.ZIndex來控制重疊元素的層級關系。
?????? 默認情況下,所有元素的ZIndex值都是0。當元素的ZIndex值一樣的時候,它們在頁面上顯示的順序和她們在Canvas.Children這個集合中的順序一致——取決于她們在XAML標記內容中定義的順序。標記中之后聲明的元素(比如上個示例中的按鈕(70,120))將疊放在之前聲明的元素之上(比如上個示例中的按鈕(60,80))。
?????? 通過增大元素的ZIndex屬性值,我們可以將相應的元素顯示在更外一層。這是因為頁面在順序上是先呈現ZIndex值小的元素,然后呈現ZIndex值大的元素。通過這個技術,我們可以改變前一個示例的呈現順序:
<Button Canvas.Left="60" Canvas.Top="80" Canvas.ZIndex="1" Width="50" Height="50" Content="(60,80)"></Button> <Button Canvas.Left="70" Canvas.Top="120" Width="100" Height="50" Content="(70,120)"</Button>備注:Canvas.ZIndex屬性的具體值沒有實際意義,可以是任意的正整數和負整數;重點是元素之間這個屬性值的大小的比較。
? ? ? ?在通過后臺代碼來改需要變元素的位置的時候,這個ZIndex屬性顯得尤其重要。我們只需要調用Canvas.SetZIndex()這個方法,傳入兩個參數(一個是要修改的元素,一個是要設置的新的ZIndex值)即可。不過可惜的是沒有類似將元素的呈現順序往前或者往后調一個單位的方法——這就意味著需要我們自己記住目前Canvas中所用到的全部ZIndex值的最大和最小值,并根據這個范圍來自己控制以便達到我們所需要的布局效果。
裁剪
Canvas有一方面和直覺相違背。大多數布局容器都會將其內容限制在容器的可用空間中。比如,我們創建了一個高度為100像素的StackPanel并且這個StackPanel放了一列按鈕,這列按鈕的高度超過了100像素,我們知道超出的部分會在StackPanel底部開始才剪掉。然而Canvas就沒有遵循這個常識(不走尋常路);相反地,它會把所有的子元素都繪制出來,即使超出了Canvas的尺寸限制。這意味著:即使我們將之前的那個示例的Canvas的Height和Width這兩個屬性都設置為0,最后頁面呈現的效果還是一樣的。
?????? Canvas按這個方式來工作是出于性能的原因——坦率地說,繪制出所有子元素之后再檢查每個子元素是否在Canvas的尺寸范圍內的效率更高。不過,這種布局行為并非一直是我們所想要的。比如在第11章中有一個動畫游戲:將炸彈投擲出游戲區域(用Canvas實現)的邊緣。在這種情況下,炸彈應該是只在Canvas中的時候可見——當炸彈飛出去后就應該消失掉,而不是疊放在其他元素之上。
?????? 幸運的是,Canvas支持裁剪(clipping),這可以使得在某指定區域之外的元素(或者元素的一部分)被剪掉,就像在StackPanel或者Grid中的表現形式一樣。我們唯一需要額外手動處理的是給相應的裁剪區域設置Canvas.Clip屬性。
?????? 從技術的角度來說,Clip屬性的值是一個幾何圖形(Geometry)對象(在第8章繪制部分內容我們會了解到這是一個非常有用的對象)。Silverlight有許多針對不同形狀的Geometry派生類,包括正方形和矩形(RectangleGeometry)、圓形和橢圓形(EllipseGeometry)和其它一些更復雜的形狀(PathGeometry)。下面這個示例則是將裁剪區域設置成為一個和Canvas的尺寸一致的矩形:
<Canvas x:Name="canvasBackground" Width="200" Height="500" Background="AliceBlue"><Canvas.Clip><RectangleGeometry Rect="0,0,200,500"></RectangleGeometry></Canvas.Clip>... </Canvas>?
? ? ? ?本例中,裁剪區域相當于是一個左上角的坐標為(0,0)、寬度200像素、高度500像素的矩形。左上角的坐標是相對于Canvas自身來說的,因此通常來說都是將這個值設置為(0,0),除非我們要給這個Canvas的上邊或者左邊區域留出一些空余區域。
?????? 某些情況下,在標記中設置裁剪區域并不是最合適的做法。比如碰到Canvas需要動態地設置尺寸以便能適應一個尺寸可變的容器或者瀏覽器窗體這種情況的時候,裁剪區域最好也應該通過后臺代碼來動態處理。還算幸運的是,我們只需要增加一個簡單的事件處理程序:Canvas的尺寸變化的時候,在相應的Canvas.SizeChanged事件處理函數中實現裁剪區域的尺寸的相應的調整即可。(在Canvas首次創建的時候這個事件也會激活,因此我們必須要注意裁剪區域的初始化設置。)
private void canvasBackground_SizeChanged(object sender, SizeChangedEventArgs e) {RectangleGeometry rect = new RectangleGeometry();rect.Rect = new Rect(0, 0, canvasBackground.ActualWidth,canvasBackground.ActualHeight);canvasBackground.Clip = rect; }?
? ? ? ?我們可以用下面的方式來注冊事件處理程序:
<Canvas x:Name="canvasBackground" SizeChanged="canvasBackground_SizeChanged" Background="AliceBlue">? ? ? ?在第11章中我們會通過那個投彈游戲來實際學習這個技術。
選擇合適的布局容器
按照一般的經驗,Grid和StackPanel最合適用于業務類型的應用程序(比如展示數據表單或者數據文檔)。它們善于處理因為窗體尺寸變化或者動態內容(比如會根據實際情況增長或者收縮的文本塊)所帶來的調整;另外它們也讓修改、本地化、應用程序的主題設置變得比較容易,因為臨近的元素在尺寸調整的時候會相互影響(翻譯補充一句:調整尺寸后,臨近的元素的位置也會隨之發生改變,這樣我們能夠及時的發現調整后所連帶的其它效果并檢查這個效果是否是我們需要的,因此這樣的形式對我們來說比較方便)。而且,Grid和StackPanel和普通的HTML頁面的工作方式最接近。
Canvas則完全不同了。由于其子元素都是通過固定坐標來排列,因此我們需要做更多的工作來調整子元素的位置(如果是要針對新元素或者新格式調整優化布局,那就會耗費更多的精力)。盡管如此,在某些圖形界面的富應用程序(比如游戲)中,Canvas則非常有意義。在這樣的應用程序中,我們需要非常細致的控制文本、經常重疊的圖形,而且還會經常通過后臺代碼來改變坐標。這種情況下,靈活性不是重點,實現特定的視覺效果才是,因此就必須使用Canvas。
自定義布局容器
盡管Silverlight提供了這么多可靠的布局容器,但是這些并不能滿足我們的所有要求。為了讓應用程序的XAP盡可能“苗條”,很多Silverlight開發者做了許多特定的布局容器。
?????? 當然,我們也可以做自己需要的布局容器。簡單的說,只需要創建一個繼承于Panel的類并實現相應的布局邏輯即可。如果來勁了,還可以將自定義容器的布局邏輯和其他Silverlight特性結合起來。比如,我們可以創建一個處理鼠標懸停事件的面板,并且讓面板中的元素支持拖拽(類似第4章中要介紹的拖拽示例);或者創建一個用動畫效果展示子元素的面板。
?????? 在本章接下來的內容中,我們會逐漸了解布局的整個過程的工作方式,然后我們會學習如何創建一個自定義的布局容器。后面將有一個名叫UniformGrid的示例——這個定制的grid控件實現了將元素鋪貼在一個均分單元格大小的網格中。
布局的兩步驟
每種面板都使用相同的處理方式:分別負責尺寸和子元素排列的兩個過程。第一階段是尺寸測量,關鍵任務是確定面板其子元素大小;第二個階段是布局設計,關鍵任務是確定每個子元素的具體排列。這兩個步驟是必不可少的,因為在面板決定如何分配可用空間之前需要考慮所有子元素的布局要求。
?????? 我們可以通過重寫MeasureOverride()和ArrangeOverride()這兩個方法來分別完成上面所述的兩個步驟所需要的邏輯實現,這兩個方法屬于Silverlight布局機制的一部分,定義在FrameworkElement類中。這兩個方法名稱中含有一個單詞Override,意思就是要告訴我們:這兩個方法是用來代替定義在UIElement類中的MeasureCore()和 ArrangeCore()的;這兩個“Core”方法不可重寫。
MeasureOverride()
第一個階段是在MeasureOverride()方法中決定每個子元素所需要的空間大小。當然,就算是在MeasureOverride()中,子元素也不能給無限制的尺寸,而是限制在面板的可用空間里。有時候我們可能需要對子元素有更嚴格的限制手段。比如,一個均分為兩行的Grid限制其子元素的高度是Grid高度的一半;StackPanel給第一個子元素提供所有可用空間,而第二個子元素則是在第一個元素占據之后剩下的空間中進行布局,依次往后。
?????? MeasureOverride()的具體實現是遍歷所有子元素并對每個子元素調用Measure()方法。調用Measure()方法的時候會設置了一個邊界框——決定子控件的最大可用空間的一個Size對象。MeasureOverride()方法最后會返回現實所有子元素所需要的空間大小。
?????? 下面的示例代碼是MeasureOverride()方法的一個基本結構:
protected override Size MeasureOverride(Size panelSpace) {// 遍歷所有子元素foreach (UIElement element in this.Children){// 查詢每個子元素所需要的空間大小, 并給出相應的尺寸限制Size availableElementSize = new Size(...);element.Measure(availableElementSize);// (這里可以讀取element.DesiredSize來獲得上面所設置的尺寸值) }// 此處指明面板需要的空間大小// 此處可設置面板的DesiredSize屬性return new Size(...); }?
? ? ? ?Measure()方法沒有返回值。對子元素調用Measure()之后,DesiredSize屬性的值便是之前請求的值。這個信息可以用作子元素的尺寸分配(以及面板所需要的全部空間)的依據。
?????? 每個子元素都必須調用Measure()方法,就算我們不需要限制元素的尺寸或者使用其DesiredSize屬性。許多元素都是在調用了Measure()之后才能呈現在頁面中。如果要讓某個子元素完全按照自己的尺寸要求來自動調整,那就將Size對象的兩個參數值都設置為Double.PositiveInfinity。(滾動條就是按照這個方式來實現的,因此它能容納的元素沒有尺寸限制。)這樣,子元素會返回其內容所需要的全部空間大小。否則,子元素會選擇它的內容需要的空間或者全部可用空間這兩者的小的一方。
?????? 在MeasureOverride()方法的最后,布局容器必須返回它所預期的尺寸。對于結構較簡單的面板來說,它的預期尺寸是所有子元素的尺寸的總和。
備注:不能草率地將傳給MeasureOverride()方法的Size參數作為面板的預期尺寸值。盡管這樣做似乎是一種不錯的設置所有可用空間的方式,但是當傳入的Size對象的兩個參數存在Double.PositiveInfinity值(意味著尺寸沒有界限)的時候會存在問題:無限制的尺寸可以用作尺寸的限制,但是不能用作最終的尺寸值,因為Silverlight無法判斷出元素應該需要多大的尺寸。而且,我們實際上也不需要超出的空間,因為這樣會導致最終的布局效果有些額外的空白或者本該顯示的元素被擠出窗體可見區域之外。
?
? ? ??留心的同學可能發現在每個子元素上調用的Measure()方法和這個面板布局邏輯的第一階段必須的MeasureOverride()方法非常相似。事實上,Measure()方法會觸發MeasureOverride()方法。因此,如果我們將一個布局容器放在另一個布局容器之中,那么在我們調用Measure()方法之后,最終能得到布局容器和它的所有子元素所需要的全部尺寸。
?????? 尺寸測量這個階段分兩步(用Measure()方法觸發MeasureOverride()方法)來處理的其中一個原因是要處理間距(Margin)。在調用Measure()方法的時候,傳入的參數是全部可用的空間尺寸;而在Silverlight調用MeasureOverride()方法的時候,它會自動將減去間距的空間后剩余部分作為可用空間(除非是Size設置了Double.PositiveInfinity)
ArrangeOverride()
測量完所有的元素之后就該將他們布局在相應的可用空間上。布局系統會調用面板的ArrangeOverride()方法,然后在其中遍歷所有的子元素調用Arrange()方法為其分配空間尺寸。(我們很明顯會猜到Arrange()方法會觸發ArrangeOverride(),就像之前已經了解到的Measure()會觸發MeasureOverride()一樣)。
?????? 在測量階段的Measure()方法中,我們傳入了一個Size類型的參數,這個參數定義了可用空間的限定范圍;而在元素布局的Arrange()方法中,我們傳入的參數是一個System.Windows.Rect 類型的對象,這個參數定義了元素的尺寸和位置。這個時候,相當于每個元素通過類似Canvas中的決定布局容器和元素的左邊距(上邊距)的X(Y)坐標概念確定了具體位置。
?????? 下面的示例代碼是ArrangeOverride()方法的一個基本結構:
protected override Size ArrangeOverride(Size panelSize) {// 檢查所有子節點.foreach (UIElement element in this.Children){// 給子元素分配尺寸和位置.Rect elementBounds = new Rect(...);element.Arrange(elementBounds);// (element.ActualHeight 和 element.ActualWidth這兩個屬性就是// 元素所使用的高度和寬度 }// 此處可以用設置面板的ActualHeight和ActualWidth屬性 // 設置面板占用的空間大小 return arrangeSize; }?
? ? ? ?這個函數傳入的Size參數不能是無限制的。不過,我們可以將DesiredSize屬性值作為參數,這樣元素在排列的時候就會按照它所期望的尺寸來布局。我們也可以讓傳入的Size參數比元素所需要的實際尺寸要大。實際上這種處理方式非常普遍。比如,一個垂直的StackPanel給個子元素所需要的高度以及StackPanel自身的完整寬度。類似地,Grid也可以將固定值或者比按比例的行高設置得比相應行中的元素的實際尺寸要高。而且即使我們將元素放在尺寸正好相匹配的容器中,元素的尺寸也可能因為使用了Height和Width這兩個屬性設置了具體的尺寸而變大。
?????? 當元素尺寸變得比其預期的尺寸要大的時候,HorizontalAlignment和VerticalAlignment屬性就要起作用了。這兩個屬性的設置決定了元素內容的具體位置。
?????? 因為ArrangeOverride()方法收到的參數必須是有大小限制的,所以我們可以將這個Size參數作為函數的返回值,也即是面板的最終的尺寸大小。事實上,很多布局容器都是按照這個方式來占據給它分配的所有空間。我們不必擔心會占據其他控件的空間,因為在測量尺寸的階段已經保證了不會得到需求之外的其他空間,除非有多余的可用空間。
自定義容器UniformGrid
到目前為止,我們對布局系統有了一定程度的了解,現在很有必要試著做一個具有Silverlight自帶的面板不支持的功能的自定義布局容器。在本小節中,我們會看到一個示例:UniformGrid,這個布局容器是WPF的一個標準控件,可以將子元素排列在自動生成的等分單元格中,現在我們用Silverlight來實現這個控件。
備注:UniformGrid作為常規Grid的一個輕量級的代替品非常有用,因為它不需要明確的行列的定義,而且也不會強制要求我們手動將子元素放在合適的單元格中。在需要展示一組平鋪的圖片的時候,UniformGrid顯得尤其合適。事實上,在整個.NET 框架中,WPF中存在這個控件的一個稍微完善的版本。
? ? ? ?和所有的自定義面板一樣,UniformGrid的定義也是繼承于基類Panel:
public class UniformGrid : System.Windows.Controls.Panel { ... }備注:我們可以直接在我們的Silverlight應用程序中直接創建這個UniformGrid類。但是如果想各種不同的應用程序中重用我們的自定義布局容器,更合適的方式是將這個類放在一個新的Silverlight類庫中,這樣,當我們需要在另一個應用程序中使用這個自定義的布局容器的時候,只需要添加相應的類庫的引用即可。
?
?? ? ? ?UniformGrid的概念非常簡單。它會檢查可用空間、計算需要的單元格的數量(以及最終單元格的大小),然后將這些子元素一個接一個地展示出來。UniformGrid可以使用Rows和Columns這兩個屬性來自定義控件的行為。Rows和Columns可以獨立設置,也可以關聯設置:
public int Columns { get; set; } public int Rows { get; set; }下面幾條內容是Rows和Columns這兩個屬性的一些特性:
- 如果這兩個屬性都設置了,那么UniformGrid會知道要創建多大的網格。它只需要將可用空間等分然后找到每個單元格的尺寸。如果元素的個數超過單元格的個數,那么超出的元素就不顯示。
- 如果只設置了一個屬性,那么UniformGrid會在要顯示出所有元素的前提下自動計算另一個屬性值。比如說,如果我們將Columns設置為3并且要放置的元素個數是8,那么UniformGrid將把可用空間分為3行。
- 如果兩個屬性都沒有設置,那么UniformGrid會在要顯示出所有元素的前提下,假定行列數相同,然后將這兩個值都計算出來。(不過UniformGrid不會創建一個完全空的行或者列。相反,如果不能精確地匹配上行和列的數量,UniformGrid會增加額外的一列。)
?????? 為了實現這種機制,UniformGrid需要記錄下真實的行列數。如果Rows和Columns這兩個屬性都設置了,那么真實的行列數就是這兩個屬性的值;如果沒有設置,那么Grid會調用一個自定義的方法CalculateColumns(),先獲得子元素的數量,然后再決定網格的行列規格。這個方法要在布局的第一階段調用。
?
private int realColumns; private int realRows; private void CalculateColumns() {// 計算子元素的數量// 如果面板是空的,則函數直接返回double elementCount = this.Children.Count;if (elementCount == 0) return;realRows = Rows;realColumns = Columns;// 如果 Rows 和 Columns 屬性都設置了,那么就直接使用if ((realRows != 0) && (realColumns != 0))return;//如果 Rows 和 Columns 屬性都沒有設置,那么先計算列數if ((realColumns == 0) && realRows == 0)realColumns = (int)Math.Ceiling(Math.Sqrt(elementCount));// 如果只設置了Rows, 那么就計算列數.if (realColumns == 0)realColumns = (int)Math.Ceiling(elementCount / realRows);// 如果只設置了Columns, 那么就計算列數.if (realRows == 0)realRows = (int)Math.Ceiling(elementCount / realColumns); }? ? ? ?Silverlight布局系統通過調用UniformGrid中的MeasureOverride()方法開始這個布局進程。這個方法需要調用上面的那個CalculateColumns()方法(目的是要確保行數和列數都設置好),然后可用空間將會被等分成一系列單元格。
protected override Size MeasureOverride(Size constraint) {CalculateColumns();// 將可用空間等分.Size childConstraint = new Size(constraint.Width / realColumns, constraint.Height / realRows);...// 下面繼續接.
? ? ? ?方法執行到這一步,UniformGrid內部的元素需要開始測量尺寸。不過,這個時候有個意外的情況——元素調用了Measure()方法后可能會返回一個更大的值,這說明最小尺寸比分配的空間還要大。UniformGrid記錄下了最大的請求寬度和高度值。最后,在整個測量過程完成后,UniformGrid需要計算尺寸,確保單元格的大小能足夠適應最大寬度和高度。這個計算出來的尺寸則作為MeasureOverride()這個方法的返回值。
// 接上面內容 ...// 記錄每個元素所需要的最大的請求尺寸.Size largestCell = new Size();// 遍歷面板中的每個子元素.foreach (UIElement child in this.Children){// 獲取子元素的期望尺寸.child.Measure(childConstraint);// 記錄下最大的請求尺寸.largestCell.Height = Math.Max(largestCell.Height, child.DesiredSize.Height);largestCell.Width = Math.Max(largestCell.Width, child.DesiredSize.Width);} // 使用最大的請求尺寸的高度和寬度來計算網格所需要的最大尺寸 return new Size(largestCell.Width * realColumns, largestCell.Height * realRows); }? ? ? ?ArrangeOverride()方法所處理的工作類似。不過,它不再測量子元素。相反,它記錄最終測量的空間大小、計算單元格的尺寸并將每個子元素放在合適的限定區域中。如果到了網格的最后單元格還有多的子元素(只有在Columns和Rows這兩個屬性值設置較小的情況下才發生),那么這些額外的子元素會放在一個0×0的區域中,換句話說就是隱藏起來了。
protected override Size ArrangeOverride(Size arrangeSize) {// 計算每個單元格的尺寸.double cellWidth = arrangeSize.Width / realColumns;double cellHeight = arrangeSize.Height / realRows;// 設置每個子元素要占據的空間.Rect childBounds = new Rect(0, 0, cellWidth, cellHeight);// 遍歷面板中的子元素.foreach (UIElement child in this.Children){// 給子元素布局.child.Arrange(childBounds);// 將界限移到下一個位置.childBounds.X += cellWidth;if (childBounds.X >= cellWidth * realColumns){// 換到下一行.childBounds.Y += cellHeight;childBounds.X = 0;// 如果子元素個數多余單元格個數,就像額外的元素隱藏,if (childBounds.Y >= cellHeight * realRows)childBounds = new Rect(0, 0, 0, 0);}}// 返回面板實際占用的尺寸return arrangeSize; }?
? ? ? ?UniformGrid的使用很簡單。我們只需要在XAML中映射對應的命名空間,然后就像定義其它布局容器那樣定義UniformGrid即可。下面這個示例是在一個帶有文本塊的StackPanel中放置了一個UniformGrid。這個示例可以幫助我們證實UniformGrid的尺寸的計算是正確的,以及證實UniformGrid的內容的布局形式。
<UserControl x:Class="Layout.UniformGridTest"xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"xmlns:local="clr-namespace:Layout" ><StackPanel Background="White"><TextBlock Margin="5" Text="Content above the WrapPanel."></TextBlock><local:UniformGrid Margin="5" Background="LawnGreen"><Button Height="20" Content="Short Button"></Button><Button Width="150" Content="Wide Button"></Button><Button Width="80" Height="40" Content="Fixed Button"></Button><TextBlock Margin="5" Text="Text in the UniformGrid cell goes here"TextWrapping="Wrap" Width="100"></TextBlock><Button Width="80" Height="20" Content="Short Button"></Button><TextBlock Margin="5" Text="More text goes in here"VerticalAlignment="Center"></TextBlock><Button Content="Unsized Button"></Button><Button Content="Unsized Button"></Button> <!--原書多寫了這一行--></local:UniformGrid><TextBlock Margin="5" Text="Content below the WrapPanel."></TextBlock></StackPanel> </UserControl>?
? ? ? ?圖3-17展示了這段XAML的運行效果。 從UniformGrid中的子元素各自不同的粒度,我們可以看出它實際是如何布局的。比如,第一個按鈕(名叫“Short Button”)設置了一個具體的Height屬性值,因此這個按鈕的高度就沒有匹配上單元格的高度,而寬度則占據了單元格的整個寬度。第二個按鈕(Wide Button)設置了具體的Width屬性值,但是按鈕是UniformGrid中最寬的元素,這就意味著這個寬度決定了UniformGrid的單元格的寬度。最終這個按鈕的尺寸和后面的那個沒有設置Height和Width的按鈕(Unsized Button)的大小完全一致——都是占據了單元格的全部空間。同樣地,UniformGrid中的第四個元素TextBlock由于TextWrapping屬性值為“Wrap”,根據其文本內容的真實長度以及TextBlock的Width屬性,這個文本內容被分為三行,這個TextBlock就成了最高的元素,所以相應的UniformGrid的單元格的高度也因此確定下來。
圖3-17 UniformGrid的元素布局
備注:如果想看其他更絢的自定義布局容器,看看http://tinyurl.com/cwk6nz這個地址中的Radial Panel 射線面板,這個布局容器將子元素沿著一個不可見的圓圈的邊線等距離排列。
頁面尺寸的處理
到目前為止,我們對Silverlight提供的各種布局容器以及如何使用這些容器進行元素的布局有了大致的了解。不過,我們還有一個重點沒有提到——top-level頁面。所有的用戶界面都要基于這個top-level頁面才得以呈現。
?????? 我們已經了解到:這個用于承載應用中的Silverlight頁面的top-level容器是個繼承于UserControl的自定義類。UserControl類中只有一個屬性Content(數據類型是UIElement),是Silverlight的用戶界面元素的基礎結構。Content屬性接受一個元素,這個元素就是用戶控件的內容。
?????? 用戶控件并不包含任何特定的功能——它們僅僅是用來將一系列相關聯的元素組合起來。不過,用戶控件尺寸的處理不同會影響到最終界面的外觀,因此還是有必要研究一下的。
?????? 我們已經知道如何用各種不同的布局容器的各種布局屬性,使得元素尺寸能夠適應其內容、適應可用空間或者固定的尺寸。設置頁面的尺寸的手段有很多種,如下所示:
- 固定尺寸:設置用戶控件的Width和Height屬性,這樣頁面將得到固定尺寸。如果頁面中有元素的尺寸超出了頁面的尺寸,那么這樣的元素的超出部分會被裁剪掉。當控件尺寸是固定值的時候,我們通常將它的HorizontalAlignment 和 VerticalAlignment屬性設置為Center,這樣控件就會處于瀏覽器窗口的中間,而不是停靠在左上角。
- 瀏覽器尺寸:用戶控件沒有設置Width和Height屬性,這種情況下應用程序會占據整個Silverlight內容區域。(順便一提,VS生成的HTML入口頁面中將Silverlight內容區域設置為占據整個瀏覽器窗口。)如果我們使用這種方式,元素超出展現區域的可能性仍然存在,不過用戶會發現這個問題,而且可以通過調整瀏覽器窗口的大小從而將缺失的內容展現出來。用這種處理方式的時候,如果我們想在Silverlight頁面和瀏覽器窗口之間有一些留白空間,只需要設置用戶控件的Margin屬性即可。
- 約束性質的尺寸設置:即用MaxWidth、MaxHeight、MinWidth和 MinHeight這四個屬性來對控件的尺寸進行控制。這種情況下,用戶控件會在限定的尺寸范圍內進行調整以適應瀏覽器窗口,不會超出限定范圍,這樣就可以保證頁面不會變亂。
- 無限制的尺寸:在某些情況下,我們還是需要將Silverlight內容區域的尺寸超出整個瀏覽器窗口的大小。這時候就需要有一個滾動條,就像在一個較長的HTML頁面中那樣。要達到這個效果,我們需要去掉Width和Height屬性的設置,并且修改Silverlight內容的入口頁面(TestPage.html):去掉<object>元素的width="100%" 和 height="100%"這兩個屬性設置。這樣下來,Silverlight內容區域的尺寸就會增大以適應用戶控件的尺寸。
備注:記住,像VS和Blend這樣的設計工具會在用戶控件中自動加上DesignWidth和DesignHeight這兩個屬性。這兩個屬性只在設計階段對頁面的呈現效果起作用(表現形式和Width、Height類似)。在運行時這兩個屬性就失效了。這兩個屬性的主要作用是讓我們可以在設計階段就能比較真實的預覽到應用程序應該會呈現的效果。
? ? ? ?這四個處理方式各有各的使用前提條件。根據創建的用戶界面的類型的不同,我們選擇合適的那個。使用非固定尺寸的優點是通過布局的自適應調整能將瀏覽器窗口的額外空間加以利用;缺點是如果瀏覽器的窗口過大或者過小,那么頁面的內容就會變得比較難讀懂,也不易操作。我們可以通過頁面設計來解決這些問題,但是工作量較大。另一方面,固定尺寸這種設計的缺點是無論瀏覽器窗體是什么尺寸,我們的應用程序的尺寸永遠不變,這會導致如果固定尺寸值比瀏覽器窗口的尺寸小,那么瀏覽器窗體可能有很大的空白區域被浪費掉;或者如果固定尺寸值比瀏覽器窗口的尺寸大,那么應用程序可能無法使用(因為需要用的界面沒有顯示出來)。
?????? 根據經驗,尺寸可調整的頁面更為靈活,而且一般作為優先選擇考慮。這種方式通常是商業應用程序和那種用戶界面比較傳統、同時沒有過多圖形內容的應用程序的最佳選擇。另一方面,圖形富應用程序和游戲的頁面通常需要尺寸更加精確的控件,因此它們傾向于使用固定尺寸的方式。
提示:如果想測試這各種途徑的實際效果,建議將頁面的邊界弄得更明顯一點。有個簡單的方法是將top-level內容元素的背景設置為非白色(比如,把Grid的Background屬性設置為Yellow)。我們不能設置用戶控件自身的Background屬性,因為UserControl這個類本身就沒有提供Background屬性。另外還有一個方法是使用Border作為top-level元素,這樣的話,設置其 BorderBrush屬性和BorderThickness屬性,頁面的區域就相當于有了外邊線。
? ? ? ?本章后續內容還會介紹其它一些特殊的尺寸處理方式:滾動條,伸縮組件以及全屏展示。
滾動條
在有限的空間里展示大量的內容需要一個很關鍵的特性:滾動。之前所講到的容器都不支持滾動。在Silverlight中,通過ScrollViewer控件就可以實現滾動效果。
?????? 如果要實現滾動的效果,我們只需是使用ScrollViewer將需要實現滾動效果的內容包裹起來。ScrollViewer內容可以放任何類型的元素,一個典型的做法就是包裹一個布局容器。比如由一列文本框和一列按鈕組成的可以滾動的Grid,這個頁面是占滿瀏覽器窗口尺寸的,另一方面頁面上設置了邊距(Margin=”20”)用以將滾動條和環繞著滾動條的瀏覽器窗體區分開。下面的標記內容展示了這個示例的基本結構,為了節省篇幅,標記語言中只寫了第一行的元素,其它行的忽略了:
<UserControl x:Class="Layout.Scrolling" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Margin="20"><ScrollViewer Background="AliceBlue"><Grid Margin="3,3,10,3"><Grid.RowDefinitions><RowDefinition Height="Auto"></RowDefinition>...</Grid.RowDefinitions><Grid.ColumnDefinitions><ColumnDefinition Width="*"></ColumnDefinition><ColumnDefinition Width="Auto"></ColumnDefinition></Grid.ColumnDefinitions><TextBox Grid.Row="0" Grid.Column="0" Margin="3"Height="Auto" VerticalAlignment="Center"></TextBox><Button Grid.Row="0" Grid.Column="1" Margin="3" Padding="2"Content="Browse"></Button>...</Grid></ScrollViewer> </UserControl>圖3-18為最終運行的結果。
圖3-18 支持滾動條的頁面
?????? 如果調整頁面的大小是的頁面足夠能容納Grid的所有內容,那么滾動表就會變得不可用,不過滾動條依然是可見的。我們可以通過設置VerticalScrollBarVisibility屬性(其值來自ScrollBarVisibility枚舉)來控制這種行為。默認值為Visible使得垂直滾動條處于一直可見的狀態;如果設置為Auto,那么在滾動條需要的時候會顯示出來,而不需要的時候則不可見;如果根本不需要使用垂直滾動條,那么將它設置為Disabled即可。
備注:如果設置為Hidden,效果和Disabled相似不過略有不同。首先,隱藏的ScrollViewer仍然支持滾動效果。(我們可以通過鍵盤上的方向鍵實現滾動。)其次,兩種方式下,ScrollViewer內部的內容布局也不盡相同:設置為Disabled的時候,ScrollViewer中的內容的尺寸還是局限于ScrollViewer之中;如果設置為Hidden,那么ScrollViewer中的內容的尺寸就沒有了限制,這意味著它可能超出滾動區域。
? ? ? ?ScrollViewer也支持水平的滾動,不過HorizontalScrollBarVisibility屬性的值默認是Hidden。將這個屬性值設置為Visible或者Auto即可使用水平滾動效果。
Viewbox的伸縮功能
在本章前面的內容中,我們討論過Grid可以實現按比例分配尺寸以保證子元素能占據所有可用空間。因此Grid是用來創建能通過伸縮來適應瀏覽器窗口的界面的絕佳工具。
?????? 盡管我們常常要用到這種尺寸可調整的行為,但是這并不是永遠都適用的。改變控件尺寸的同時也會改變控件能容納的子元素的數量,也會使布局產生微小的移位。圖形富應用程序需要尺寸更精確的控件以保證元素之間能夠完美的排列起來。不過這并不意味著我們要使用固定尺寸來處理頁面。相反,我們可以使用另外一個技巧,叫做scaling(按比例伸縮)。
?????? 本質上,伸縮會調整控件的整個視覺外觀,而不僅僅是它的外邊界。不管伸縮的比例如何,控件所展現的內容都是一樣的——只是看起來不同。
?????? 圖3-19展現了這種不同。左邊的是正常尺寸下的效果,中間的是窗體增加后用傳統的尺寸調整方式后的效果,右邊的則是窗體增加后用伸縮方式實現的效果。
圖3-19 普通(左)、尺寸調整效果(中)和尺寸伸縮效果(右)的對比
?????? 要用上縮放,我們必須先使用形狀變換(transform)這個概念。在第8章中我們會了解到transform是Silverlight的2D繪圖框架的一個關鍵部分,它可以用來實現 縮放變換、傾斜變換、旋轉變換以及一些改變元素外觀的其他效果。本例中,我們需要使用ScaleTransform(縮放變換)來實現頁面的縮放效果。
?????? 有兩種方式來使用ScaleTransform。第一種方式是DIY。在代碼中響應UserControl.SizeChanged事件,檢查頁面的當前尺寸,然后經過適當的計算后用code-behind手工創建ScaleTransform。這么做雖然干得過,但是有點兒苦逼。我們可以換第二種方式:Viewbox控件,最終能實現相同的效果,重要的是沒那么多代碼量。
?????? 在編碼實現縮放效果的代碼之前,我們需要確保XAML標記的配置是符合如下要求的:
- 用戶控件的尺寸不能使確定值——相反,它必須能隨著瀏覽器窗口的尺寸的變化而相應的做出自適應調整。
- 為了保證能縮放至準確的尺寸范圍,我們需要知道其理想的尺寸值,也就是能精確地匹配上它內部的所有元素的尺寸大小。盡管這個尺寸大小不會設置在XAML標記中,但是會在代碼中的縮放計算部分進行處理。
????? 當這些細節準備就緒后我們就可以很輕松地創建一個支持縮放效果的頁面了。下面這段XAML標記就是圖3-19所對應的一個包含了幾組【文本框+按鈕】的理想尺寸為200×225像素的Grid。
<UserControl x:Class="Layout.Page" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"<!--這個Viewbox容器是實現縮放必須的 --><Viewbox><!--這個Grid則是普通用戶界面的布局內容.請注意其尺寸是固定值設置的. --><Grid Background="White" Width="200" Height="225" Margin="3,3,10,3"><Grid.RowDefinitions>...</Grid.RowDefinitions><Grid.ColumnDefinitions><ColumnDefinition Width="*"></ColumnDefinition><ColumnDefinition Width="Auto"></ColumnDefinition></Grid.ColumnDefinitions><TextBox Grid.Row="0" Grid.Column="0" Margin="3"Height="Auto" VerticalAlignment="Center" Text="Sample Text"></TextBox><Button Grid.Row="0" Grid.Column="1" Margin="3" Padding="2"Content="Browse"></Button>...</Grid></Viewbox> </UserControl>? ? ? ?本例中,Viewbox在縮放的過程中遵循了Grid的縱橫比。換句話說,它是以適應小維度(高度或者寬度)的原則來調整尺寸,而不是為了填充所有的可用空間而破壞控件的現有尺寸比例。如果我們不需要維持這個尺寸比例,只需將Stretch屬性設置為Fill即可。這種做法對頁面縮放功能來說并不是十分有用,但是如果是有其他目的(比如設置按鈕中的矢量圖的尺寸)就另說了。
?????? 最后我們可以做個有趣的測試:將Viewbox放在ScrollViewer中,這會產生一些有意思的效果。比如說,將Viewbox的Height和Width屬性設置大點讓Viewbox的尺寸超過可用空間的尺寸,然后在內部的被放大的元素上實現縮放。我們可以用這個技巧來創建一個可縮放的用戶界面,用戶可以通過拖動滑塊或者鼠標滾輪改變界面的縮放比例。第4章會介紹一個用這個技術實現的使用鼠標滾輪來調整頁面顯示的示例。
瀏覽器縮放的Silverlight支持
Silverlight運行在某些瀏覽器和操作系統(比如當前最近幾版的Firefox和IE)的時候,Silverlight應用程序能提供一種叫做自動縮放(autozoom)的特性。這意味著用戶可以通過改變縮放比例使得Silverlight應用程序變大或者變小。(在IE中,可以通過狀態欄右側來調整或者菜單欄-查看-縮放選項。)舉個栗子,如果用戶選擇縮放比例是110%,那么整個Silverlight應用程序,包括文字、圖表以及各種空間都會放大10%。
多半情況下,這種行為都有意義——而且顯示的效果和預期一致。然而,如果我們想讓應用程序能夠提供其自身的縮放特性的話,那么瀏覽器的自動縮放功能就不太適合了。這種情況下,我們需要禁用瀏覽器的自動縮放功能,做法是:在HTML入口頁面中增加一個enableAutoZoom參數并將屬性值設置為false,如下所示:
<div id="silverlightControlHost"><object data="data:application/x-silverlight-2,"type="application/x-silverlight-2" width="100%" height="100%"><param name="enableAutoZoom" value="false" />...</object><iframe style='visibility:hidden;height:0;width:0;border:0px'></iframe> </div>?
全屏模式
Silverlight應用程序也可以實現全屏顯示,使應用能在脫離瀏覽器之外運行。在全屏模式下,Silverlight插件會占據整個顯示區域,并且將其他所有應用程序(包括瀏覽器)疊放在本應用程序的下方。
?????? 全屏模式有幾個嚴重的局限性:
- 只有在接收到用戶輸入響應后才能切換到全屏模式:換句話說,當用戶點擊一個按鈕或者鍵盤上按下一個鍵,系統才能切換到全屏模式。系統不能在應用程序加載之后立刻切換到全屏模式。(就算是用代碼實現我們期望的效果,系統也會忽略這段代碼。)系統有這樣的限制的目的是為了防止Silverlight應用程序被設計成一個可能誤導用戶、讓用戶以為自己進入了一個本地的應用程序或者系統的窗口中的狀態。
- 在全屏模式下,鍵盤輸入受到了限制:除了Tab、回車、Home、End、Page Up、Page Down、空格和方向鍵以外,其他的鍵都被屏蔽掉了。這就意味著我們可以做一個簡單的全屏游戲,但是不能使用文本框或者其他支持輸入的控件。設計這樣一個限制的原因是為了防止不法分子用來騙取密碼——比如,將系統設計成窗體對話框的樣子去欺騙用戶輸入密碼。例外情況是創建受信任的應用程序沒有這樣的限制(具體見第18章)
備注:全屏模式主要用于設計大屏的視頻展示。對于簡單圖形應用程序(比如照片瀏覽器)和游戲只需要那幾個按鍵能響應就足夠了。要處理輸入類型的控件之外的按鍵,只需要使用標準的KeyPress事件句柄即可(比如第4章就會介紹在應用程序的根布局容器中增加一個KeyPress事件來捕獲鍵盤按鍵。)
? ? ? ?下面這段代碼實現的點擊相應的按鈕使應用程序切換到全屏模式:
private void Button_Click(object sender, RoutedEventArgs e) {Application.Current.Host.Content.IsFullScreen = true; }?
? ? ? ?當程序切換進入全屏模式的時候,屏幕中央會顯示一個如圖3-20的消息。這個消息包含了應用程序所在的站點的域名。如果是一個ASP.NET站點并且內嵌于VS中的web服務器,那么我們會看到域名是http://localhost;如果是用一個硬盤上的HTML測試頁面寄宿了應用程序,那么我們會看到域名是file://。這個消息也提示用戶通過按ESC鍵來退出全屏模式。另外,通過將IsFullScreen屬性為false也可以退出全屏模式。
圖3-20 全屏模式時提示的消息
?????? 如果應用程序要使用全屏模式,那么最頂層的用戶控件不應該把Height和Width設置為固定值,這樣就可以讓用戶控件能自動調整尺寸來適應可用空間。在全屏模式下,我們也可以使用前面所討論過的縮放技術通過變換(transform)將程序中的元素的尺寸變大。
?????? 還有另外一種退出Silverlight應用程序全屏模式的方法:切換到另一個應用的窗口上。一般來講,這種方式效果還不錯。但是當你使用多個顯示器的時候,這個方式所實現的效果可能與你想象的不一樣了:從全屏的Silverlight應用中切換出去的時候,它不能保證Silverlight應用仍然在這個顯示器中處于全屏狀態,而在另一個顯示器中展現你切換過去的應用程序。
?????? 如果想阻止這種行為的發生,我們可以用下面的代碼將應用程序的全屏模式“固定”住,即使應用程序失去焦點,它也同樣處于全屏模式下。
Application.Current.Host.Content.FullScreenOptions = FullScreenOptions.StaysFullScreenWhenUnfocused;?
? ? ? ?這段代碼必須在切換到全屏模式之前使用。在這之后再設置IsFullScreen屬性,用戶會得到一個是否讓應用程序一直處于全屏狀態的確認提示(圖3-21)。這個確認對話框中也包含了一個是否要記住用戶的選擇的復選框,勾上之后,下次用戶在切換進入全屏模式的時候就不會再次彈出這個提示框了。
?????? 如果用戶選擇了“是”,窗口會保持全屏模式直到用戶按下ESC鍵或者系統執行了將IsFullScreen屬性設置為false的代碼。如果用戶選擇“否”,那么應用程序會按照正常的方式進入全屏模式,也就意味著當應用程序失去焦點的時候就退出全屏狀態。
圖3-21 切換到全屏模式的時候彈出的是否保持全屏狀態的確認對話框
本章總結
本章中,我們對Silverlight布局模型進行了深入的了解,學習了如何將元素放在StackPanel、Grid等布局容器中。我們嘗試通過嵌套的各種布局容器呈現一個復雜的布局界面,另外還用GridSplitter實現了布局尺寸的可調整。然后我們還學習了如何創建一個自定義的布局容器。最后,我們還學習了通過改變尺寸、縮放和全屏等技術來控制最頂層的用戶控件的展現效果。
轉載于:https://www.cnblogs.com/xtechnet/archive/2012/08/20/Silverlight5_Chapter03_Part03.html
總結
以上是生活随笔為你收集整理的【翻译】Pro.Silverlight.5.in.CSharp.4th.Edition - 第三章 布局 03的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: LitePal使用详解
- 下一篇: 核心标签库(转)