WPF 创建自定义面板
前面兩個章節分別介紹了兩個自定義控件:自定義的ColorPicker和FlipPanel控件。接下來介紹派生自定義面板以及構建自定義繪圖控件。
創建自定義面板是一種特殊但較常見的自定義控件開發子集。前面以及介紹過有關面板方面的知識,了解到面板駐留一個或多個子元素,并且實現了特定的布局邏輯以恰當地安排子元素。如果希望構建自己的可拖動的工具欄或可停靠的窗口系統,自定義面板是很重要的元素。當創建需要非標準特定布局的組合控件時,自定義面板通常很有用的,例如停靠工具欄。
接下里介紹一個基本的Canvas面板部分以及一個增強版本的WrapPanel面板兩個簡單的示例。
一、兩步布局過程
每個面板都使用相同的設備:負責改變子元素尺寸和安排子元素的兩步布局過程。第一階段是測量階段(measure pass),在這一階段面板決定其子元素希望具有多大的尺寸。第二個階段是排列階段(layout pass),在這一階段為每個控件指定邊界。這兩個步驟是必需的,因為在決定如何分割可用空間時,面板需要考慮所有子元素的期望。
可以通過重寫名稱為MeasureOverride()和ArrangeOverride()方法,為這兩個步驟添加自己的邏輯,這兩個方法是作為WPF布局系統的一部分在FrameworkElement類中定義的。奇特的名稱使用標識MeasureOverride()和ArrangeOverride()方法代替在MeasureCore()和ArrangeCore()方法中定義的邏輯,后兩個方法在UIElement類中定義的。這兩個方法是不能被重寫的。
1、MeasureOverride()方法
第一步是首先使用MeasureOverride()方法決定每個子元素希望多大的空間。然而,即使是在MeasureOverride()方法中,也不能為子元素提供無限空間,至少,也應當將自元素限制在能夠適應面板可用空間的范圍之內。此外,可能希望更嚴格地限制子元素。例如,具有按比例分配尺寸的兩行的Grid面板,會為子元素提供可用高度的一般。StackPanel面板會為第一個元素提供所有可用空間,然后為第二個元素提供剩余的空間等等。
每個MeasureOverride()方法的實現負責遍歷子元素集合,并調用每個子元素的Measure()方法。當調用Measure()方法時,需要提供邊界框——決定每個子空間最大可用空間的Size對象。在MeasureOverride()方法的最后,面板返回顯示所有子元素所需的空間,并返回它們所期望的尺寸。
下面是MeasureOverride()方法的基本結構,其中沒有具體的尺寸細節:
protected override Size MeasureOverride(Size constraint) {//Examine all the childrenforeach (UIElement element in base.InternalChildren){//Ask each child how much space it would like,given the//availableSize constraintSize availableSize=new Size{...};element.Measure(availableSize);//(you can now read element.DesiredSize to get the requested size.)}//Indicate how mush space this panel requires.//This will be used to set the DesiredSize property of the panel.return new Size(...); }Measure()方法不返回數值。在為每個子元素調用Measure()方法之后,子元素的DesiredSize屬性提供了請求的尺寸。可以在為后續子元素執行計算是(以及決定面板需要的總空間時)使用這一信息。
因為許多元素直接調用了Measure()方法之后才會渲染它們自身,所以必須為每個子元素調用Measure()方法,即使不希望限制子元素的尺寸或使用DesiredSize屬性也同樣如此。如果希望讓所有子元素能夠自由獲得它們所希望的全部空間,可以傳遞在兩個方向上的值都是Double.PositiveInfinity的Size對象(ScrollViewer是使用這種策略的一個元素,原因是它可以處理任意數量的內容)。然后子元素會返回其中所有內容所需要的空間。否則,子元素通常會返回其中內容需要的空間或可用空間——返回較小值。
在測量過程的結尾,布局容器必須返回它所期望的尺寸。在簡單的面包中,可以通過組合每個子元素的期望尺寸計算面板所期望的尺寸。
Measure()方法觸發MeasureOverride()方法。所以如果在一個布局容器中放置另一個布局容器,當調用Measure()方法時,將會得到布局容器及其所有子元素所需要的總尺寸。
2、ArrangeOverride()方法
測量完所有元素后,就可以在可用的空間中排列元素了。布局系統調用面板的ArrangeOverride()方法,而面板為每個子元素調用Arrange()方法,以高速子元素為它分配了多大的控件(Arrange()方法會觸發ArrangeOverride()方法,這與Measure()方法會觸發MeasureOverride()方法非常類似).
當使用Measure()方法測量條目時,傳遞能夠定義可用空間邊界的Size對象。當使用Arrange()方法放置條目時,傳遞能夠定義條目尺寸和位置的System.Windows.Rect對象。這時,就像使用Canvas面板風格的X和Y坐標放置每個元素一樣(坐標確定布局容器左上角與元素左上角之間的距離)。
下面是ArrangeOverride()方法的基本結構。
protected override Size ArrangeOverride(Size arrangeBounds) {//Examine all the children.foreach(UIElement element in base.InternalChildren){//Assign the child it's bounds.Rect bounds=new Rect(...);element.Arrange(bounds);//(You can now read element.ActualHeight and element.ActualWidth to find out the size it used ..)}//Indicate how much space this panel occupies.//This will be used to set the AcutalHeight and ActualWidth properties//of the panel.return arrangeBounds; }當排列元素時,不能傳遞無限尺寸。然而,可以通過傳遞來自DesiredSize屬性值,為元素提供它所期望的數值。也可以為元素提供比所需尺寸更大的空間。實際上,經常會出現這種情況。例如,垂直的StackPanel面板為其子元素提供所請求的高度,但是為了子元素提供面板本身的整個寬度。同樣,Grid面板使用具有固定尺寸或按比例計算尺寸的行,這些行的尺寸可能大于其內部元素所期望的尺寸。即使已經在根據內容改變尺寸的容器中放置了元素,如果使用Height和Width屬性明確設置了元素的尺寸,那么仍可以擴展該元素。
當使元素比所期望的尺寸更大時,就需要使用HorizontalAlignment和VerticalAlignment屬性。元素內容被放置到指定邊界內部的某個位置。
因為ArrangeOverride()方法總是接收定義的尺寸(而非無限的尺寸),所以為了設置面板的最終尺寸,可以返回傳遞的Size對象。實際上,許多布局容器就是采用這一步驟來占據提供的所有空間。
二、Canvas面板的副本
理解這兩個方法的最快捷方法是研究Canvas類的內部工作原理,Canvas是最簡單的布局容器。為了創建自己的Canvas風格的面板,只需要簡單地繼承Panel類,并且添加MeasureOverride()和ArrangeOverride()方法,如下所示:
public class CanvasClone:System.Windows.Controls.Panel{...}Canvas面板在他們希望的位置放置子元素,并且為子元素設置它們希望的尺寸。所以,Canvas面板不需要計算如何分割可用空間。這使得MeasureOverride()方法非常簡單。為每個子元素提供無限的空間:
protected override System.Windows.Size MeasureOverride(System.Windows.Size availableSize) {Size size = new Size(double.PositiveInfinity, double.PositiveInfinity);foreach (UIElement element in base.InternalChildren){element.Measure(size);}return new Size(); }注意,MeasureOverride()方法返回空的Size對象。這意味著Canvas 面板根本不請求人和空間,而是由用戶明確地為Canvas面板指定尺寸,或者將其放置到布局容器中進行拉伸以填充整個容器的可用空間。
ArrangeOverride()方法包含的內容稍微多一些。為了確定每個元素的正確位置,Canvas面板使用附加屬性(Left、Right、Top以及Bottom)。附加屬性使用定義類中的兩個輔助方法實現:GetProperty()和SetProperty()方法。
下面是用于排列元素的代碼:
protected override System.Windows.Size ArrangeOverride(System.Windows.Size finalSize){foreach (UIElement element in base.InternalChildren){double x = 0;double y = 0;double left = Canvas.GetLeft(element);if (!DoubleUtil.IsNaN(left)){x = left;}double top = Canvas.GetTop(element);if (!DoubleUtil.IsNaN(top)){y = top;}element.Arrange(new Rect(new Point(x, y), element.DesiredSize));}return finalSize;}三、更好的WrapPanel面板
WrapPanel面板執行一個簡單的功能,該功能有有時十分有用。該面板逐個地布置其子元素,一旦當前行的寬度用完,就會切換到下一行。但有時候需要采用一種方法來強制立即換行,以便在新行中啟動某個特定控件。盡管WrapPanel面板原本沒有提供這一功能,但通過創建自定義控件可以方便地添加該功能。只需要添加一個請求換行的附加屬性即可。此后,面板中的子元素可使用該屬性在適當位置換行。
下面的代碼清單顯示了WrapBreakPanel類,該類添加了LineBreakBeforeProperty附加屬性。當將該屬性設置為true時,這個屬性會導致在元素之前立即換行。
public class WrapBreakPanel : Panel{public static DependencyProperty LineBreakBeforeProperty;static WrapBreakPanel(){FrameworkPropertyMetadata metadata = new FrameworkPropertyMetadata();metadata.AffectsArrange = true;metadata.AffectsMeasure = true;LineBreakBeforeProperty = DependencyProperty.RegisterAttached("LineBreakBefore", typeof(bool), typeof(WrapBreakPanel), metadata);}...}與所有依賴項屬性一樣,LineBreakBefore屬性被定義成靜態字段,然后在自定義類的靜態構造函數中注冊該屬性。唯一的區別在于進行注冊時使用的是RegisterAttached()方法而非Register()方法。
用于LineBreakBefore屬性的FrameworkPropertyMetadata對象明確指定該屬性影響布局過程。所以,無論何時設置該屬性,都會觸發新的排列階段。
這里沒有使用常規屬性封裝器封裝這些附加屬性,因為不在定義它們的同一個類中設置它們。相反,需要提供兩個靜態方法,這來改那個方法能夠使用DependencyObject.SetValue()方法在任意元素上設置這個屬性。下面是LineBreakBefore屬性需要的代碼:
/// <summary> /// 設置附加屬性值 /// </summary> /// <param name="element"></param> /// <param name="value"></param> public static void SetLineBreakBefore(UIElement element, Boolean value) {element.SetValue(LineBreakBeforeProperty, value); }/// <summary> /// 獲取附加屬性值 /// </summary> /// <param name="element"></param> /// <returns></returns> public static Boolean GetLineBreakBefore(UIElement element) {return (bool)element.GetValue(LineBreakBeforeProperty); }唯一保留的細節是當執行布局邏輯時需要考慮該屬性。WrapBreakPanel面板的布局邏輯以WrapPanel面板的布局邏輯為基礎。在測量階段,元素按行排列,從而使面板能夠計算需要的總空間。除非太大或LineBreakBefore屬性被設置為true。否則每個元素都唄添加到當前行中。下面是完整的代碼:
protected override Size MeasureOverride(Size constraint){Size currentLineSize = new Size();Size panelSize = new Size();foreach (UIElement element in base.InternalChildren){element.Measure(constraint);Size desiredSize = element.DesiredSize;if (GetLineBreakBefore(element) ||currentLineSize.Width + desiredSize.Width > constraint.Width){// Switch to a new line (either because the element has requested it// or space has run out).panelSize.Width = Math.Max(currentLineSize.Width, panelSize.Width);panelSize.Height += currentLineSize.Height;currentLineSize = desiredSize;// If the element is too wide to fit using the maximum width of the line,// just give it a separate line.if (desiredSize.Width > constraint.Width){panelSize.Width = Math.Max(desiredSize.Width, panelSize.Width);panelSize.Height += desiredSize.Height;currentLineSize = new Size();}}else{// Keep adding to the current line.currentLineSize.Width += desiredSize.Width;// Make sure the line is as tall as its tallest element.currentLineSize.Height = Math.Max(desiredSize.Height, currentLineSize.Height);}}// Return the size required to fit all elements.// Ordinarily, this is the width of the constraint, and the height// is based on the size of the elements.// However, if an element is wider than the width given to the panel,// the desired width will be the width of that line.panelSize.Width = Math.Max(currentLineSize.Width, panelSize.Width);panelSize.Height += currentLineSize.Height;return panelSize;} 上面代碼中的重要細節是檢查LineBreakBefore屬性。這實現了普遍WrapPanel面板沒有提供的額外邏輯。
ArrangeOverride()方法的代碼幾乎相同。區別在于:面板在開始布局一行之前需要決定該行的最大高度(根據最高的元素確定)。這樣,每個元素可以得到完整數量的可用空間,可用控件占用行的整個高度。與使用普通的WrapPanel面板進行布局時的過程相同。下面是完整的代碼:
protected override Size ArrangeOverride(Size arrangeBounds){int firstInLine = 0;Size currentLineSize = new Size();double accumulatedHeight = 0;UIElementCollection elements = base.InternalChildren;for (int i = 0; i < elements.Count; i++){Size desiredSize = elements[i].DesiredSize;if (GetLineBreakBefore(elements[i]) || currentLineSize.Width + desiredSize.Width > arrangeBounds.Width) //need to switch to another line{arrangeLine(accumulatedHeight, currentLineSize.Height, firstInLine, i);accumulatedHeight += currentLineSize.Height;currentLineSize = desiredSize;if (desiredSize.Width > arrangeBounds.Width) //the element is wider then the constraint - give it a separate line{arrangeLine(accumulatedHeight, desiredSize.Height, i, ++i);accumulatedHeight += desiredSize.Height;currentLineSize = new Size();}firstInLine = i;}else //continue to accumulate a line{currentLineSize.Width += desiredSize.Width;currentLineSize.Height = Math.Max(desiredSize.Height, currentLineSize.Height);}}if (firstInLine < elements.Count)arrangeLine(accumulatedHeight, currentLineSize.Height, firstInLine, elements.Count);return arrangeBounds;}private void arrangeLine(double y, double lineHeight, int start, int end){double x = 0;UIElementCollection children = InternalChildren;for (int i = start; i < end; i++){UIElement child = children[i];child.Arrange(new Rect(x, y, child.DesiredSize.Width, lineHeight));x += child.DesiredSize.Width;}} WrapBreakPanel面板使用起來十分簡便。下面的一些標記演示了使用WrapBreakPanel面板的一個示例。在該例中,WrapBreakPanel面板正確地分割行,并且根據其子元素的尺寸計算所需的尺寸:
下圖顯示了如何解釋上面的標記:
往期精彩回顧
【.net core】電商平臺升級之微服務架構應用實戰
.Net Core微服務架構技術棧的那些事
Asp.Net Core 中IdentityServer4 授權中心之應用實戰
Asp.Net Core 中IdentityServer4 授權中心之自定義授權模式
Asp.Net Core 中IdentityServer4 授權流程及刷新Token
Asp.Net Core 中IdentityServer4 實戰之 Claim詳解
Asp.Net Core 中IdentityServer4 實戰之角色授權詳解
Asp.Net Core 中間件應用實戰中你不知道的那些事
Asp.Net Core Filter 深入淺出的那些事-AOP
Asp.Net Core EndPoint 終結點路由工作原理解讀
ASP.NET CORE 內置的IOC解讀及使用
總結
以上是生活随笔為你收集整理的WPF 创建自定义面板的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 微服务统计,分析,图表,监控, 分布式追
- 下一篇: 如何实时主动监控你的网站接口是否挂掉并及