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面板正确地分割行,并且根据其子元素的尺寸计算所需的尺寸:

<Window x:Class="CustomControlsClient.WrapBreakPanelTest"xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"xmlns:lib="clr-namespace:CustomControls;assembly=CustomControls"Title="WrapBreakPanelTest" Height="300" Width="300"><StackPanel><StackPanel.Resources><Style TargetType="{x:Type Button}"><Setter Property="Margin" Value="3"></Setter><Setter Property="Padding" Value="5"/></Style></StackPanel.Resources><TextBlock Padding="5" Background="LightGray">Content above the WrapBreakPanel.</TextBlock><lib:WrapBreakPanel><Button>No Break Here</Button><Button>No Break Here</Button><Button>No Break Here</Button><Button>No Break Here</Button><Button lib:WrapBreakPanel.LineBreakBefore="True" FontWeight="Bold">Button with Break</Button><Button>No Break Here</Button><Button>No Break Here</Button><Button>No Break Here</Button><Button>No Break Here</Button></lib:WrapBreakPanel><TextBlock Padding="5" Background="LightGray">Content below the WrapBreakPanel.</TextBlock></StackPanel>
</Window>

  下图显示了如何解释上面的标记:

往期精彩回顾



  • 【.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解读及使用


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/310386.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

vue.js中mock本地json数据

vue.js中mock本地json数据 新版本的vue项目中已经将dev-server.js&#xff0c;dev-client.js两个js文件合并到了webpack.dev.conf.js文件中&#xff0c;以下分别是新旧版本的build目录结构&#xff1a; 新版本&#xff1a; 旧版本&#xff1a; 本次验证mock&#xff1a;运…

互联网40岁失业是一个无法打破的魔咒吗?

最近刚刚过完生日&#xff0c;又大了一岁&#xff0c;距离40岁又进了一步。年纪大了&#xff0c;就要多复盘。最近几天思考的比较多&#xff0c;因为身边失业的朋友开始多了起来。我又有点陷入担忧、焦虑的心态了。好在我一直是个有阿Q精神的中年油腻男&#xff0c;很快安抚好自…

数据结构与算法--复杂链表的复制

复杂链表的复制 题目&#xff1a;实现一个函数complexListNode 复制一个复杂链表。在链表中&#xff0c;每个节点除了有一个next指针指向下一个节点&#xff0c;还有另外一个before节点&#xff0c;before节点指向链表中任意一个节点&#xff0c;或者null节点。链表节点定义使…

如何实时主动监控你的网站接口是否挂掉并及时报警

“ 阅读本文大概需要 10 分钟。 ”最近我在公司负责的业务已经正式投入上线了&#xff0c;既然是线上环境&#xff0c;那么就需要保证其可用性。我负责的业务其中就包括一个 Web Service&#xff0c;我需要保证 Service 的每个接口都是可用的&#xff0c;如果某个时间流量大了或…

数据结构与算法--二叉查找树转顺序排列双向链表

二叉查找树转顺序排列双向链表 题目&#xff1a;输入一颗二叉查找树&#xff0c;将二叉查找树转成一个排序的双向链表&#xff0c;要求不能创建任何新节点&#xff0c;只调整树节点中指针的指向。例如下图所示&#xff1a; 本次二叉查找树节点定义使用之前文章 数据结构与算法…

5种避免C#.NET中因事件造成内存泄漏的技术

原文来自互联网&#xff0c;由长沙DotNET技术社区编译。 5种避免C&#xff03;.NET中事件造成的内存泄漏的技术C&#xff03;&#xff08;通常是.NET&#xff09;中的事件注册是内存泄漏的最常见原因。至少从我的经验来看。实际上&#xff0c;我从事件中看到了太多的内存泄漏&a…

数据结构与算法--字符串的排列组合问题

字符串的全排列 题目&#xff1a;输入一个字符串&#xff0c;打印出改字符串中所有字符的所有排列。例如输入字符串abc&#xff0c;那么打印出由a&#xff0c;b&#xff0c;c字符组成的所有字符串&#xff1a;abc&#xff0c;acb&#xff0c;bac&#xff0c;bca&#xff0c;cab…

[GitHub] 75+的 C# 数据结构和算法实现

C#中标准数据结构和算法的即插即用类库项目GitHub&#xff1a;https://github.com/aalhour/C-Sharp-AlgorithmsWatch: 307 Star: 3.4k Fork: 910o---o | |/ --O---O--O | |\ --O---O--o---o | |O o o--o o--o o---o o-O-o …

我是如何一步步的在并行编程中将lock锁次数降到最低实现无锁编程

在并行编程中&#xff0c;经常会遇到多线程间操作共享集合的问题&#xff0c;很多时候大家都很难逃避这个问题做到一种无锁编程状态&#xff0c;你也知道一旦给共享集合套上lock之后&#xff0c;并发和伸缩能力往往会造成很大影响&#xff0c;这篇就来谈谈如何尽可能的减少lock…

常用Arthas命令

jad反编译 检查线上代码是否修改成功&#xff0c;例如修改interface后看Jar包是否引入新的&#xff0c;或者代码是否最新的。 jad com.zhenai.counseling.business.provider.facade.supremecourse.RedeemRecordFacadeImpl //反编译只展示源码 jad --source-only com.zhenai.c…

关于分布式锁的面试题都在这里了

「我今天班儿都没上&#xff0c;就为了赶紧把这篇文章分布式锁早点写完。我真的不能再贴心了。」「边喝茶边构思&#xff0c;你们可不要白嫖了&#xff01;三连来一遍&#xff1f;」引言为什么要学习分布式锁&#xff1f;最简单的理由就是作为一个社招程序员&#xff0c;面试的…

Git 15周年:当年的分道扬镳,成就了今天的开源传奇

4 月 7 日&#xff0c;全球最主流的版本控制系统 —— Git 迎来 15 周年纪念日&#xff0c;项目主要维护者 Junio C Hamano&#xff08;滨野 纯&#xff09; 先生发邮件庆祝了这一日子。我们知道&#xff0c;所有的软件项目在整个生命周期中都要经过不断迭代&#xff0c;在一个…

使用 docker 编译运行 abp 项目

在前面的两篇文章中&#xff0c;介绍了如何在华为鲲鹏架构及其Euler系统上运行dotnet core, 使用docker运行了默认的mvc模板项目&#xff0c;这篇文章继续介绍在docker中运行更复杂的dotnet core项目&#xff0c;这里以业内鼎鼎大名的abp vnext框架&#xff0c;版本 2.6 为例。…