WPF中的自定义ListBox(一)

上周侯捷大师来京做了一次讲座,有幸和他聊聊。当别人排队,而我也在排队。有意思的是当别人在找侯捷大师签名时,而我确有幸为侯捷大师签了一次名,当然是在我的《葵花宝典——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里填充数据内容

 

代码
  <ListBox Margin="10" BorderThickness="1">
            
<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文件,然后再稍稍细致地讨论一下该类的关键属性。

 

代码
    <Window.Resources>
        
<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)。

 

<ListBox Grid.Column="1" Margin="10" BorderThickness="1" ItemsSource="{Binding Source={StaticResource Cities}, XPath=City[@Type\=1or@Type\=2]}"/>

 


 好了,Helloj2ee磨刀霍霍,剑指ListBox。

 

如何改变ListBox的外观 

改变一个控件的外观,无外乎几种方法。
(1)改变它的属性,比如设置背景色的颜色;
(2)通过样式改变,其实质还是定义一组需要改变的属性;
(3)通过Content模型来改变,因为每个ListBoxItem里能够任何放置任何东西。这是通过Content模型改变ListBox的前提;
(4)通过控件模板和数据模板,这是一种变形金刚似的改变,通常对程序员也要具备相当高的条件,定义一个良好完备的模板实在是一件不容易的事情;
(5)通过附加属性来扩展控件的功能,这一点可能绝大多数人都不理解,Helloj2ee曾经在参考文献【5】里列举过一个通过附加属性扩展控件的例子;
(6)所有的方法都无法满足您的要求的话,那么只剩下最后的王牌方法,就是自定义控件。即使自定义控件也是要分层次的。自定义控件的哲学,Helloj2ee还是老王卖瓜,自卖自夸。参考文献【5】里对这一部分进行详细的讨论。
这一次我们对ListBox的改变,实则一次手术刀似的巨变。上述六种方法,唯有第五种没有涉及。我们首先观察一下ListBox和ListBoxItem这样几个特殊的属性。

表 ListBox关于模板的属性

属性名

属性类型

描述

ItemsPanel

ItemsPanelTemplate

ItemsControlItemsPanel模板,他用来定义这个Panel的外观。

ItemTemplate

DataTemplate

定义每一个Item数据项的展示方法,它相当于ListBoxItemContentTemplate

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默认的PanelVirualizingStackPanelWPF所能够提供的Panel,没有一个能满足这种按照圆形排列的需求。因此Helloj2ee只能自定义一个Panel

 

自定义一个Panel绝对是一件颇有技术含量的事情。他的核心是要解决Panel里面的控件如何排列,以及尺寸的问题。实际上Panel布置它当中的控件位置和尺寸经历了两个阶段,第一个阶段是测量(Measure)阶段,在这个阶段中父元素会逐一询问子元素所期望的尺寸,从而确定自己所期望的尺寸。第二个阶段是布置(Arrange)阶段,在这个期间父元素会明确子元素的尺寸和位置。具体到编程模型里面,主要涉及到要重载两个函数,一个是MeasureOverride,另一个是ArrangeOverride[7]。
MeasureOverride函数的实现里面需要注意要做如下几件事:
(1)遍历所有包含的子元素,并且调用它们的Measure方法;
(2)调用完了Measure方法后,子元素的DesiredSize即是它们各自期望的尺寸;您可以获得它们的DesiredSize属性;
(3)根据所包含的子元素的尺寸,计算自己所期望的尺寸,并返回该值。注意MeasureOverride传递过来的参数,是父元素告诉子元素,它能够分配子元素的空间大小。当然子元素所期望的尺寸可以大于父元素给子元素分配的尺寸大小【8】。
现在我们再来看看CircularPanel的MeasureOverride函数的实现。相信看了上面一段话之后,不用Helloj2ee再逐字逐句去解释了。

 

代码
protected override Size MeasureOverride(Size availableSize)
        {
            Size resultSize 
= new Size(00);
            
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方法需要传递的参数是类型为RectfinalRect参数。他是决定控件位置和尺寸的最终决定因素。

好了,现在可以看看ArrangeOverride函数的实现了。对每一个ListBoxItem的位置计算取决于初始的角度(InitialAngle),每个ListBoxItem之间的间隔角度(AngleItem),半径Radius,以及旋转的中心点(Align)。

 

代码
protected override Size ArrangeOverride(Size finalSize)
        {
            
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

2Pavan Podila, Kevin Hoffman, 2009, WPF Control Development Unleashed

3】李响,《葵花宝典——WPF自学手册》第十四章数据绑定——桃花岛软件公司管理人员系统之始末,2010

4MSDNXmlDataProvider.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

6Charles PeztoldApplications = Code + Markup: A Guide to the Microsoft Windows Presentation FoundationChapter 25 Templates 2006

7】李响,《葵花宝典——WPF自学手册》第十章布局——药师的桃花岛 2010

8MSDNFrameworkElement..::.MeasureOverride Methodms-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

 

posted @ 2010-12-07 02:09  helloj2ee  阅读(6783)  评论(12编辑  收藏  举报