【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 children
foreach (UIElement element in base.InternalChildren)
{
//Ask each child how much space it would like,given the
//availableSize constraint
Size 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 = ;
double y = ;
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;
}
MeasureOverride
上面代码中的重要细节是检查LineBreakBefore属性。这实现了普遍WrapPanel面板没有提供的额外逻辑。
ArrangeOverride()方法的代码几乎相同。区别在于:面板在开始布局一行之前需要决定该行的最大高度(根据最高的元素确定)。这样,每个元素可以得到完整数量的可用空间,可用控件占用行的整个高度。与使用普通的WrapPanel面板进行布局时的过程相同。下面是完整的代码:
protected override Size ArrangeOverride(Size arrangeBounds)
{
int firstInLine = ; Size currentLineSize = new Size(); double accumulatedHeight = ; UIElementCollection elements = base.InternalChildren;
for (int i = ; 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 = ;
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;
}
}
ArrangeOverride
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>
下图显示了如何解释上面的标记:
【WPF学习】第六十七章 创建自定义面板的更多相关文章
- 【WPF学习】第二十七章 Application类的任务
上一章介绍了有关WPF应用程序中使用Application对象的方式,接下来看一下如何使用Application对象来处理一些更普通的情况,接下俩介绍如何初始化界面.如何处理命名行参数.如何处理支付窗 ...
- 【WPF学习】第十七章 鼠标输入
鼠标事件执行几个关联的任务.当鼠标移到某个元素上时,可通过最基本的鼠标事件进行响应.这些事件是MouseEnter(当鼠标指针移到元素上时引发该事件)和MouseLeave(当鼠标指针离开元素时引发该 ...
- 【WPF学习】第十七章 键盘输入
当用户按下键盘上的一个键时,就会发生一系列事件.下表根据他们的发生顺序列出了这些事件: 表 所有元素的键盘事件(按顺序) 键盘处理永远不会像上面看到的这么简单.一些控件可能会挂起这些事件中的某些事件, ...
- 《Programming WPF》翻译 第9章 3.自定义功能
原文:<Programming WPF>翻译 第9章 3.自定义功能 一旦你挑选好一个基类,你将要为你的控件设计一个API.大部分WPF元素提供属性暴露了多数功能,事件,命令,因为他们从框 ...
- “全栈2019”Java第六十七章:内部类、嵌套类详解
难度 初级 学习时间 10分钟 适合人群 零基础 开发语言 Java 开发环境 JDK v11 IntelliJ IDEA v2018.3 文章原文链接 "全栈2019"Java第 ...
- Docker学习(六)Dockerfile构建自定义镜像
Docker学习(六)Dockerfile构建自定义镜像 前言 通过前面一篇文章可以知道怎么去使用一个镜像搭建服务,但是,如何构造自己的一个镜像呢,docker提供了dockerfile可以让我们自己 ...
- Netty 学习(六):创建 NioEventLoopGroup 的核心源码说明
Netty 学习(六):创建 NioEventLoopGroup 的核心源码说明 作者: Grey 原文地址: 博客园:Netty 学习(六):创建 NioEventLoopGroup 的核心源码说明 ...
- 【WPF学习】第二十一章 特殊容器
内容控件不仅包括基本控件,如标签.按钮以及工具提示:它们还包含特殊容器,这些容器可用于构造用户界面中比较大的部分区域. 首先介绍ScrollViewer控件,该控件直接继承自ContentContro ...
- [汇编学习笔记][第十七章使用BIOS进行键盘输入和磁盘读写
第十七章 使用BIOS进行键盘输入和磁盘读写 17.1 int 9 中断例程对键盘输入的处理 17.2 int 16 读取键盘缓存区 mov ah,0 int 16h 结果:(ah)=扫描码,(al) ...
随机推荐
- MySQL----SQL操作
1.什么是SQL? Structured Query Language:结构化查询语言 其实就是定义了操作所有关系型数据库的规则.每一种数据库操作的方式存在不一样的地方,称为“方言”. 2.SQL通用 ...
- HTTP、TCP、IP协议面试题
HTTP.TCP.IP协议基本定义 HTTP: (HyperText Transport Protocol)是超文本传输协议的缩写,它用于传送WWW方式的数据,关于HTTP协议的详细内容请参考RFC2 ...
- MySQL优化之避免索引失效的方法
在上一篇文章中,通过分析执行计划的字段说明,大体说了一下索引优化过程中的一些注意点,那么如何才能避免索引失效呢?本篇文章将来讨论这个问题. 避免索引失效的常见方法 1.对于复合索引的使用,应按照索引建 ...
- OpenCV-Python 轮廓分层 | 二十五
目标 这次我们学习轮廓的层次,即轮廓中的父子关系. 理论 在前几篇关于轮廓的文章中,我们已经讨论了与OpenCV提供的轮廓相关的几个函数.但是当我们使用cv.findcontour()函数在图像中找到 ...
- Rasa Stack:创建支持上下文的人工智能助理和聊天机器人教程
相关概念 Rasa Stack 是一组开放源码机器学习工具,供开发人员创建支持上下文的人工智能助理和聊天机器人: • Core = 聊天机器人框架包含基于机器学习的对话管理 • NLU = 用于自然语 ...
- 4 Values whose Sum is 0 POJ - 2785(二分应用)
题意:输入一个数字n,代表有n行a,b,c,d,求a+b+c+d=0有多少组情况. 思路:先求出前两个数字的所有情况,装在一个数组里面,再去求后两个数字的时候二分查找第一个大于等于这个数的位置和第一个 ...
- Java中如何调用静态方法
Java中如何调用静态方法: 1.如果想要调用的静态方法在本类中,可直接使用方法名调用 2.调用其他类的静态方法,可使用类名.方法名调用 关于静态方法能被什么调用 1.实例方法 2.静态发放
- Web安全认证
一.HTTP Basic Auth 每次请求 API 时都提供用户的 username 和 password. Basic Auth 是配合 RESTful API 使用的最简单的认证方式,只需提供用 ...
- iOS 项目发布
一.Apple开发者账号 1.1 开发者账号类型 个人级 公司级 企业级 公司和企业的可多人协作. 在苹果的开发者平台登录后,可在 People 界面邀请其他人员协作开发,邀请的人需要注册一个 app ...
- 将Python执行代码打包成exe可执行文件
安装pyinstaller pip3 install pyinstaller 进入py文件目录,执行以下指令 pyinstaller -F -w <文件名.py>,-F代表生成可执行文件, ...