上周侯捷大师来京做了一次讲座,有幸和他聊聊。当别人排队,而我也在排队。有意思的是当别人在找侯捷大师签名时,而我确有幸为侯捷大师签了一次名,当然是在我的《葵花宝典——WPF自学手册》上签下了自己难看的名字。
这不是重点,重点是他提到他的人生中几件关键的事情。其中一件,如果我的记忆没有错的话,应该是Windows 3.0来的时候的冲击,他当时还在一个台湾的研究所里工作,在考虑窗口,控件如何“Message Based,Event Driven”(以消息为基础,事件驱动之)。Windows3.0来了,一下他的模糊思路明晰起来,尽管侯大师考虑的只是一个雏形,而Windows是实实在在真正的产品。但这并不重要,重要的是他们的思路不谋而合。这样他不至于在DOS到Windows巨变的情况下“死在沙滩上”。很多程序员由于无法从DOS的编程思路迅速转换到Windows的“Message Based,Event Driven”,按照侯捷大师的话,“一半死在沙滩上”。
我是一个从Win32开始的程序员,经历了Win32,MFC,WinForm和WPF还有Silverlight。由于Silverlight的强势插足,我这个一直以来号称自己为桌面程序员,不得不改变其自己的身份,从现在起不能叫做桌面程序员,而是前端或者客户端程序员。每一次“死在沙滩上”发生在编程思路的变化上,尽管Windows客户端技术经历了上述五个阶段(划分不一定科学)。但是思路的转变是两次,第一次发生在从Win32到MFC。这是一个巨大的跳跃。传统的WinMain不见了,取而代之的是CWnd,CView等类。而第二次我认为发生在从WinForm到WPF。过去一种语言包打天下的时代不见了,取而代之的是XAML定义界面,C#或者Visual Basic实现业务逻辑。光是程序员重新去学一门语言,就增加了其困难。而且XAML并不是C++,C#或者Java这样的OO语言,而是一种Markup语言。但是更大的转变不在此,语言终究还是能学会的。思路!思路!是否能适应现在的编程思路才是最为核心的。而WPF当中最为光辉的思想,我认为莫过于控件模板。没有控件模板,改变控件的外观则成为一种空谈。这种改变绝不是改变颜色,改变字体,改变大小这样的小打小闹,而是巨变,真正的巨变,是大卸八块,再重新拼装起来的改变。
也许有人会问,我是一个“银光”爱好者,WPF对我如“浮云”。没有关系,这样的思路对WPF,Silverlight是普适的。如何学好控件模板呢?参阅《葵花宝典》无疑是需要的,但是只能学会呼吸,行走的方法。如果呼吸,行走你掌握了。那么不妨进入Helloj2ee的自定义控件的系列(我确实不是一个长性的人,天保佑我让这个系列长一点,阿门)。
自定义ListBox
大家都知道一个正常的ListBox是什么样的。如下图所示。
我们这个系列希望ListBox最终变成下面这样。他是一个半透明的窗口,而且和扑克牌一样成扇形铺开,当鼠标移到每一个ListBoxItem时,ListBoxItem会有一个从小变大的动画。
相信读过《葵花宝典——WPF自学手册》一书的读者,还是会惊奇于第一章1.2.1七十二变中的ListBox例子(参考文献【2】)。但是我并未详细地解释那个例子是如何做出来的。不过完成了这个例子,相信哪个ListBox的变种也不是难事。我们Step by Step来完成这样一个ListBox。
认识ListBox
认识ListBox首先要从他的类继承结构认识。如下图所示。
控件很多,类层次结构很复杂。如何把握住整体结构呢?Helloj2ee在这里提醒各位以下三点:
(1)他们都派生自Control,因此应该熟悉Control的特性;
(2)掌握住Content模型,Content模型曾被Helloj2ee比作北冥神功,他是可以容纳任何控件的。所谓“大舟小舟无不载,大鱼小鱼无不容”[1];
(3)在上面的基础上,就知道尽管控件分类繁杂,但是归纳起来从Control派系下来为四大类,相当Control的直系亲属,从Control旁系派生下来的为三大类,相当于Control的远亲[1]。如下图所示:
Content模型的4大直系(来自参考文献【1】)
Content模型的远亲(来自参考文献【1】)
而ListBox是从直系派生下来,从ItemsControl派生下来。ItemsControl有什么样的特点呢?人如其名,他的最大特点就是有一个Items属性。Items属性是一个集合,这个集合里几乎可以放置任何类型的对象。
再多的基础,Helloj2ee只能寄希望您阅读过葵花宝典——WPF自学手册,现在需要做的是自定义ListBox之前的一些准备工作,给ListBox绑定数据内容。
给ListBox绑定数据内容
ListBox除了Items属性可以让你直接在ListBox里填充数据内容
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![ExpandedBlockStart.gif](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
< ListBoxItem >
北京
</ ListBoxItem >
< ListBoxItem >
天津
</ ListBoxItem >
< ListBoxItem >
河北
</ ListBoxItem >
</ ListBox >
还可以用ItemsSource属性来绑定数据内容,比如XML文件。XML文件定义如下,假定名为Cities.xml,在工程的Data文件夹目录下:
Cities.xml文件如下:
< Cities xmlns ="" >
< City Type ="1" >
< ImageText > 北京 </ ImageText >
</ City >
< City Type ="3" >
< ImageText > 黑龙江 </ ImageText >
</ City >
……
</ Cities >
数据绑定也是WPF或者Silverlight当中一个相对难的话题。基本的数据绑定概念,您也可以参见参考文献【3】,或者其他WPF书籍。这里要讨论的是和XML文件的绑定,在WPF里绑定XML文件需要用到XMLDataProvider。我们先快速地绑定这个XML文件,然后再稍稍细致地讨论一下该类的关键属性。
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![ExpandedBlockStart.gif](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
< XmlDataProvider x:Key ="Cities" Source ="Data\Cities.xml" XPath ="Cities" />
</ Window.Resources >
< Grid >
< ListBox Margin ="10" BorderThickness ="1" ItemsSource =" {Binding Source={StaticResource Cities}, XPath=City} " >
</ ListBox >
</ Grid >
上面的代码做了两件事情,第一件事情是将一个XmlDataProvider作为一个Window的静态资源。第二件事情是通过ItemsSource将ListBox和这个资源绑定起来。这里面有一个属性,名曰XPath。XmlDataProvider有这个属性,Binding里也有这个属性。我们仅在Binding的XPath属性上做些文章。因为通过它不仅可以将所有的数据都绑定起来,还可以绑定符合一定条件的数据。比如需要从中只提取直辖市(Type=1表示为北京,而Type=2表示其他直辖市)。非常奇怪的语法,是吗?详情可以参见参考文献【4】。也可以参见我附带的例子(ListBoxDemo1)。
好了,Helloj2ee磨刀霍霍,剑指ListBox。
如何改变ListBox的外观
改变一个控件的外观,无外乎几种方法。
(1)改变它的属性,比如设置背景色的颜色;
(2)通过样式改变,其实质还是定义一组需要改变的属性;
(3)通过Content模型来改变,因为每个ListBoxItem里能够任何放置任何东西。这是通过Content模型改变ListBox的前提;
(4)通过控件模板和数据模板,这是一种变形金刚似的改变,通常对程序员也要具备相当高的条件,定义一个良好完备的模板实在是一件不容易的事情;
(5)通过附加属性来扩展控件的功能,这一点可能绝大多数人都不理解,Helloj2ee曾经在参考文献【5】里列举过一个通过附加属性扩展控件的例子;
(6)所有的方法都无法满足您的要求的话,那么只剩下最后的王牌方法,就是自定义控件。即使自定义控件也是要分层次的。自定义控件的哲学,Helloj2ee还是老王卖瓜,自卖自夸。参考文献【5】里对这一部分进行详细的讨论。
这一次我们对ListBox的改变,实则一次手术刀似的巨变。上述六种方法,唯有第五种没有涉及。我们首先观察一下ListBox和ListBoxItem这样几个特殊的属性。
表 ListBox关于模板的属性
属性名 | 属性类型 | 描述 |
ItemsPanel | ItemsPanelTemplate | ItemsControl的Items的Panel模板,他用来定义这个Panel的外观。 |
ItemTemplate | DataTemplate | 定义每一个Item数据项的展示方法,它相当于ListBoxItem的ContentTemplate |
Template | ControlTemplate | ListBox的模板,用来改变ListBox的外观 |
表 ListBoxItem关于模板的属性
属性名 | 属性类型 | 描述 |
Template | ControlTemplate | 定义ListBoxItem外观的模板 |
ContentTemplate | DataTemplate | 定义ListBoxItem里面内容的外观。 |
这样的几个属性,极易模糊。今天,我们就彻底把他们之间的关系搞清楚。想要搞清楚这个问题,我们需要一个小工具查看ListBox和ListBoxItem默认的模板结构。Helloj2ee曾经提供过一个查看模板的工具,不过发现Charles Peztold大师提供的查看模板工具更为好用,考虑更为周全。因此推荐大家使用Charles Peztold大师的DumpControlTemplate(见参考文献【6】),小工具的源码在随本章的附例中。如下图所示,您可以在第一个菜单里选择任意一个控件,如果它是一个ItemsControl,则不仅可以查看它的Template属性,也可以查看它的ItemsPanel的模板。
Helloj2ee不在这里把ListBox或者ListBoxItem的模板代码贴出来。而是绘制出它们的模板结构来,这样有利于表达其核心概念。还是以一个外观为如下图的ListBox为例来说明。
ListBox的模板结构如下图所示:
从上面的图中可以看出来以下两点:
(1)ListBox默认的Template里面包含了一个Border和一个ItemsPresenter;
(2)ItemsPresenter是一个非常特殊的类,它就好像一个占位符,是随时可以被其他Panel替换的,替换的依据就是模板,只不过这儿的模板类型为ItemsPanelTemplate。ListBox的ItemsPanel属性就是负责定义ListBox的项所处的Panel的外观。默认的ItemsPanel提供了一个VirualizingStackPanel。
接着往下看ListBoxItem的模板结构。
从上面的图中也可以看出来以下两点:
(1) ListBoxItem默认的Template属性定义了ListBox里包含Border和ContentPresenter;
(2)ContentPresenter也是一个非常特殊的类,它和ItemsPresenter一样。只不过区别在于替换ItemsPresenter是一个Panel,而这里更为宽泛,几乎任何一个控件都可以。这里ListBoxItem中内容的外观就取决于ListBoxItem的ContentTemplate属性。当然为了方便使用ListBox的ItemTemplate属性也是设定ListBoxItem的内容外观,它相当于ContentTemplate属性。
我们现在改变ListBox的外观,最重要的就是将这几个属性用到极致。
自定义Panel
第一步我们是需要将原有的ListBox纵向排列他的Item,变成按照圆形排列。那么这势必要改变ListBox他们所处的Panel。过去我们已经知道ListBox默认的Panel是VirualizingStackPanel。WPF所能够提供的Panel,没有一个能满足这种按照圆形排列的需求。因此Helloj2ee只能自定义一个Panel。
自定义一个Panel绝对是一件颇有技术含量的事情。他的核心是要解决Panel里面的控件如何排列,以及尺寸的问题。实际上Panel布置它当中的控件位置和尺寸经历了两个阶段,第一个阶段是测量(Measure)阶段,在这个阶段中父元素会逐一询问子元素所期望的尺寸,从而确定自己所期望的尺寸。第二个阶段是布置(Arrange)阶段,在这个期间父元素会明确子元素的尺寸和位置。具体到编程模型里面,主要涉及到要重载两个函数,一个是MeasureOverride,另一个是ArrangeOverride[7]。
MeasureOverride函数的实现里面需要注意要做如下几件事:
(1)遍历所有包含的子元素,并且调用它们的Measure方法;
(2)调用完了Measure方法后,子元素的DesiredSize即是它们各自期望的尺寸;您可以获得它们的DesiredSize属性;
(3)根据所包含的子元素的尺寸,计算自己所期望的尺寸,并返回该值。注意MeasureOverride传递过来的参数,是父元素告诉子元素,它能够分配子元素的空间大小。当然子元素所期望的尺寸可以大于父元素给子元素分配的尺寸大小【8】。
现在我们再来看看CircularPanel的MeasureOverride函数的实现。相信看了上面一段话之后,不用Helloj2ee再逐字逐句去解释了。
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![ExpandedBlockStart.gif](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
{
Size resultSize = new Size( 0 , 0 );
foreach (UIElement child in this .Children)
{
child.Measure(availableSize);
resultSize.Width = Math.Max(resultSize.Width, child.DesiredSize.Width);
resultSize.Height = Math.Max(resultSize.Height, child.DesiredSize.Height);
}
resultSize.Width =
double .IsPositiveInfinity(availableSize.Width) ?
resultSize.Width : availableSize.Width;
resultSize.Height =
double .IsPositiveInfinity(availableSize.Height) ?
resultSize.Height : availableSize.Height;
return resultSize;
}
再来说说ArrangeOverride函数。这是第二阶段的事情。在这一个阶段里Panel要最终确定控件的位置和尺寸大小。控件的位置和尺寸大小是通过调用每一个控件的Arrange方法来确定的。Arrange方法需要传递的参数是类型为Rect的finalRect参数。他是决定控件位置和尺寸的最终决定因素。
好了,现在可以看看ArrangeOverride函数的实现了。对每一个ListBoxItem的位置计算取决于初始的角度(InitialAngle),每个ListBoxItem之间的间隔角度(AngleItem),半径Radius,以及旋转的中心点(Align)。
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![ExpandedBlockStart.gif](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
{
this .Refresh();
return base .ArrangeOverride(finalSize);
}
private void Refresh()
{
int count = 0 ;
if ( double .IsNaN( this .Width))
{
this .Width = 200 ;
}
if ( double .IsNaN( this .Height))
{
this .Height = 200 ;
}
foreach (FrameworkElement element in this .Children)
{
RotateTransform r = new RotateTransform();
double alignX = 0 ;
double alignY = 0 ;
switch ( this .Align)
{
case AlignmentOptions.Left:
alignX = 0 ;
alignY = 0 ;
break ;
case AlignmentOptions.Center:
alignX = element.DesiredSize.Width / 2 ;
alignY = element.DesiredSize.Height / 2 ;
break ;
case AlignmentOptions.Right:
alignX = element.DesiredSize.Width;
alignY = element.DesiredSize.Height;
break ;
}
r.CenterX = alignX;
r.CenterY = alignY;
r.Angle = ( this .AngleItem * count ++ ) - this .InitialAngle;
element.RenderTransform = r;
double x = this .Radius * Math.Cos(Math.PI * r.Angle / 180 );
double y = this .Radius * Math.Sin(Math.PI * r.Angle / 180 );
if ( ! ( double .IsNaN( this .Width)) && ! ( double .IsNaN( this .Height)) && ! ( double .IsNaN(alignX)) && ! ( double .IsNaN(alignY)) && ! ( double .IsNaN(element.DesiredSize.Width)) && ! ( double .IsNaN(element.DesiredSize.Height)))
{
element.Arrange( new Rect(x + this .Width / 2 - alignX, y + this .Height / 2 - alignY, element.DesiredSize.Width, element.DesiredSize.Height));
}
}
}
当然这些属性都是依赖属性,也是自定义的依赖属性。这些只能请各位读者参考文献【9】和【10】了。文献【9】可以帮助大家理解自定义的依赖属性,而文献【10】则能用好依赖属性。由于Helloj2ee对自己的书一定会熟悉很多。因此在参考文献的引用上,多是引用自己所写的,因此难免会给读者一点广告之嫌,还请各位见谅。当然相关的概念看任何一本WPF的书都是OK的。
完成这个自定义ListBox的这条路还很漫长,敬请大家耐心等待Helloj2ee的第二篇。最后附上我和侯大师的一张合影。能够遇到侯大师,真是一件很幸运的事情。
参考文献
【1】 李响,《葵花宝典——WPF自学手册》第十一章控件与Content——北冥神功,2010
【2】Pavan Podila, Kevin Hoffman, 2009, WPF Control Development Unleashed
【3】李响,《葵花宝典——WPF自学手册》第十四章数据绑定——桃花岛软件公司管理人员系统之始末,2010
【4】MSDN,XmlDataProvider.XPath Property, ms-help://MS.VSCC.v90/MS.MSDNQTR.v90.en/fxref_system.windows.data/html/b9776844-ca43-58ac-6d05-3f3c98f66e39.htm
【5】李响,《葵花宝典——WPF自学手册》第二十章自定义数据控件——出手无招,何招可破2010
【6】Charles Peztold,Applications = Code + Markup: A Guide to the Microsoft Windows Presentation Foundation,Chapter 25 Templates, 2006
【7】李响,《葵花宝典——WPF自学手册》第十章布局——药师的桃花岛 2010
【8】MSDN,FrameworkElement..::.MeasureOverride Method,ms-help://MS.VSCC.v90/MS.MSDNQTR.v90.en/fxref_system.windows/html/f16effb3-da72-2bb9-290b-0fd6b9b79b4c.htm
【9】李响,《葵花宝典——WPF自学手册》第五章依赖属性——木木的汗血宝马 2010
【10】李响,《葵花宝典——WPF自学手册》第二十章 自定义控件——出手无招 何招可破2010
附件:
源码:ListBoxDemo /Files/helloj2ee/src.rar
查看控件模板工具:/Files/helloj2ee/DumpControlTemplate.rar