技术总结:自动扩张WPF树型表格列宽

简介:

问题描述

    今天测试人员提了一个易用性的BUG,主要是说系统目前使用的树型控件不支持自动扩张列的宽度。其实客户那边已经对这个问题提了多次,不过由于对WPF只是入门级,所以一直都没改。这两天项目比较闲,就花了些时间把这个问题改了。原问题如下:

image 

图1 问题描述

 

背景

    树型控件在GIX4系统中已经被大量使用。这个控件是一年前其它同事在网上搜索到,再引入的。

    一开始的时候,要解决这个问题,想到的最直接的方案是这样的:找到第一列中的Expander控件(加号:image),然后监听它的“Expanded”事件;在事件处理程序中,计算所需要的宽度,然后设置为控件的宽度。

    按照这个方案去实际写代码时,发现并没有想象中那么简单,发现了很多问题。例如,Expander并不是Expander控件,而是一个ToggleButton,而且是写在模板中的,TreeGridRowPresenter中的Expander的类型也只是UIElement,也就是说,不能把Expander从UIElement转换为ToggleButton,这样程序会写得很死。又如,如何计算第一列的所需要宽度。

    虽然我们项目中是有整个控件的源码,但是整合进来后别的同事已经对它进行了很多修改,所以只有在网上找到最原始的源码来研究。发现,原来这个树型控件的方案是Avalon Team自己给出的:《TreeListView: Show Hierarchy Data with Details in Columns》。然后Ricciolo对它进行了一些研究:《Fun With GridView*RowPresenter》,最后他给出了一个较完整的版本:《A complete WPF TreeListView control》。

    学习并研究了它的源码,最后总结出以下几个子问题,这些问题是要上面提及的BUG所需要解决的:

 

四个待解决的问题

    1. 何时触发是最合适的?在何处触发调整宽度的代码?

    2. 如何找到树型控件的所有GridViewRowPresenter。

    3. GridViewRowPresenter中,如何把第一列的控件找到。

    4. 第一列控件的组成结构是怎么样的,它所需要的大小如何求出,是否可以直接使用Measure和DesiredSize。

 

一步一步解决

    第一个问题,何时触发这个功能?其实我是要在点击后,当子节点都加载好后,然后计算出合适的大小,再设置给列对象。我先在TreeListView的OnExpanded事件处理程序中尝试编写代码获取每一个TreeListView,但是发现这个事件在发生时,所有的子节点并没有生成,所以不能通过ItemContainerGenerator.GetContainerForItem方法获取到窗口,此方案失败。接着,我查看了ItemsControl的接口声明,发现ItemContainerGenerator属性有事件StatusChanged。所以我就改为监听这个事件,并判断如果当它的Status变为ContainersGenerated时,就表示所有子节点已经生成了。代码如下:

1
2
3
4
5
6
7
this .ItemContainerGenerator.StatusChanged += (o, e) =>
{
     if  ( this .ItemContainerGenerator.Status == GeneratorStatus.ContainersGenerated)
     {
         this .AdjustFirstColumnWidth();
     }
};

但是同样发现新的问题,这时候虽然窗口对象TreeListView已经生成,但是它下面的所有Visual Child都没有生成,这样同样无法获取到它里面用来显示每一行的GridRowPresenter。所以只有改成了这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public  TreeListViewItem()
{
     this .PrepareToAdjustFirstColumnWidth();
}
 
private  void  PrepareToAdjustFirstColumnWidth()
{
     this .ItemContainerGenerator.StatusChanged += (o, e) =>
     {
         if  ( this .ItemContainerGenerator.Status == GeneratorStatus.ContainersGenerated)
         {
             if  ( this .Items.Count > 0)
             {
                 var  item = this .Items[ this .Items.Count - 1];
                 var  treeItem = this .ItemContainerGenerator.ContainerFromItem(item) as  TreeListViewItem;
                 treeItem.Loaded += (oo, ee) =>
                 {
                     this .AdjustFirstColumnWidth();
                 };
             }
         }
     };
}

这样,最后一个孩子的可视内容都加载好后,才会触发调整宽度的代码。

    第二个问题比较简单,看了TreeListView的源码后,发现它在TreeListViewItem类的模板中使用了GridViewRowPresenter类,然后为它定义了名字:“PART_Header”。在模板中以PART_起头的控件是控件的约定,具体内容见:《WPF Parts Control Model》。所以我可以使用以下方法找到它,而不用考虑新的模板是否有它:

1
2
3
4
5
private  TreeGridViewRowPresenter FindGridRow()
{
     var  rowPresenter = this .Template.FindName( "PART_Header" , this ) as  TreeGridViewRowPresenter;
     return  rowPresenter;
}

    要解决第三个问题,我们需要知道GridViewRowPresenter中如何生成一行,并知道最后生成的控件结构。先看看GridViewRowPresenter最后生成的控件结构,这里我使用的是Snoop:

image

图2 用Snoop查看TreeGridViewRowPresenter的可视化结构

我们发现,GridViewRowPresenter下只是简单的包含了几个可视元素,它们刚好是每一列所显示的内容。再查看GridViewRowPresenter的源代码,发现它拥有以下属性:public GridViewColumnCollection Columns{get;set;}、internal UIElementCollection InternalCollection{get;set;},进一步分析后,我猜测性地得出以下结论:GridViewRowPresenter.InternalCollection简单地包含了所有列的显示元素,它会根据Columns属性中各行对这些可视元素进行维护,让它们显示得跟表格一样。

至此,第三个问题解决了:

1
var  firstColumn = VisualTreeHelper.GetChild(rowPresenter, 0) as  UIElement;

    最后一个问题,是过程中最麻烦的一个问题。我们看到,图2中该行下的第一个元素是第一列的显示元素,显示了“2.1”。但是文本左边的Expander控件却是TreeGridViewRowPresenter的最后一个可视化孩子。而且缩进并不是一个控件。那么这是怎么一回事呢?看了TreeGridViewRowPresenter的源码后,发现原来是它主动把Expander放在了最后:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public  class  TreeGridViewRowPresenter : GridViewRowPresenter
{
     protected  override  System.Windows.Media.Visual GetVisualChild( int  index)
     {
         // Last element is always the expander
         // called by render engine
         if  (index < base .VisualChildrenCount) return  base .GetVisualChild(index);
         if  (index == base .VisualChildrenCount) return  this .lbRowNo;
         return  this .Expander;
     }
 
     protected  override  int  VisualChildrenCount
     {
         get
         {
             // Last element is always the expander
             if  ( this .Expander != null )
                 return  base .VisualChildrenCount + 2;
             else
                 return  base .VisualChildrenCount + 1;
         }
     }
}
1
而文本前面先显示缩进,然后再显示Expander的原因是由于TreeGridViewRowPresenter类重写了FrameworkElement.ArrangeOverride方法。在该方法中,它把第一列的元素显示的长度变短,在之前显示一段缩进的空白和Expander控件:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
protected  override  Size ArrangeOverride(Size arrangeSize)
{
     Size s = base .ArrangeOverride(arrangeSize);
 
     if  ( this .Columns == null  || this .Columns.Count == 0) return  s;
     UIElement expander = this .Expander;
 
     double  current = 0;
     double  max = arrangeSize.Width;
     for  ( int  x = 0; x < this .Columns.Count; x++)
     {
         GridViewColumn column = this .Columns[x];
         // Actual index needed for column reorder
         UIElement uiColumn = (UIElement) base .GetVisualChild(( int )ActualIndexProperty.GetValue(column, null ));
 
         // Compute column width
         double  w = Math.Min(max, (Double.IsNaN(column.Width)) ? ( double )DesiredWidthProperty.GetValue(column, null ) : column.Width);
 
         // First column indent
         if  (x == 0 && expander != null )
         {
             double  indent = FirstColumnIndent + expander.DesiredSize.Width;
             uiColumn.Arrange( new  Rect(current + indent, 0, w - indent, arrangeSize.Height));
         }
         else
         {
             uiColumn.Arrange( new  Rect(current, 0, w, arrangeSize.Height));
         }
         max -= w;
         current += w;
     }
 
     // Show expander
     if  (expander != null )
     {
         expander.Arrange( new  Rect( this .FirstColumnIndent, 0, expander.DesiredSize.Width, expander.DesiredSize.Height));
     }
 
     return  s;
}

分析到这里,就知道如何计算出第一列的最终宽度了:

1
2
3
4
5
6
7
8
9
10
11
12
13
private  double  GetFirstColumnDesiredWidth()
{
     var  rowPresenter = this .FindGridRow();
     if  (VisualTreeHelper.GetChildrenCount(rowPresenter) <= 0) return  0;
 
     //GridViewRowPresenter中的每一个元素表示一列。
     var  firstColumn = VisualTreeHelper.GetChild(rowPresenter, 0) as  UIElement;
     var  desiredWidth = firstColumn.DesiredSize.Width;
 
     //需要的宽度前,需要加上列的缩进和Expander的宽度。
     var  indent = rowPresenter.FirstColumnIndent + rowPresenter.Expander.DesiredSize.Width;
     return  indent + desiredWidth + ENSURE_SIZE;
}

加上以下这段代码后,程序终于可以正确运行了。

 

总结

    解决这个问题,花了一天多的时间,主要原因还是因为对WPF还是处在入门的级别。其中学到了以下内容:

熟悉了TreeView、TreeViewItem、ItemsControl的使用及树型控件的原理。

树型表格控件TreeListView的设计过程(见之前的文章)。

熟悉了Measure的使用。

目录
相关文章
|
8天前
|
C# 开发者 Windows
基于Material Design风格开源、易用、强大的WPF UI控件库
基于Material Design风格开源、易用、强大的WPF UI控件库
|
4月前
|
C#
浅谈WPF之装饰器实现控件锚点
使用过visio的都知道,在绘制流程图时,当选择或鼠标移动到控件时,都会在控件的四周出现锚点,以便于修改大小,移动位置,或连接线等,那此功能是如何实现的呢?在WPF开发中,想要在控件四周实现锚点,可以通过装饰器来实现,今天通过一个简单的小例子,简述如何在WPF开发中,应用装饰器,仅供学习分享使用,如有不足之处,还请指正。
59 1
|
8月前
|
C# Windows
WPF技术之图形系列Polygon控件
WPF Polygon是Windows Presentation Foundation (WPF)框架中的一个标记元素,用于绘制多边形形状。它可以通过设置多个点的坐标来定义多边形的形状,可以绘制任意复杂度的多边形。
449 0
|
8月前
|
C# Windows
WPF技术之RichTextBox控件
WPF RichTextBox是Windows Presentation Foundation (WPF)中提供的一个强大的文本编辑控件,它可以显示富文本格式的文本,支持多种文本处理操作。
343 0
|
4月前
|
前端开发 C# 容器
浅谈WPF之控件拖拽与拖动
使用过office的visio软件画图的小伙伴都知道,画图软件分为两部分,左侧图形库,存放各种图标,右侧是一个画布,将左侧图形库的图标控件拖拽到右侧画布,就会生成一个新的控件,并且可以自由拖动。那如何在WPF程序中,实现类似的功能呢?今天就以一个简单的小例子,简述如何在WPF中实现控件的拖拽和拖动,仅供学习分享使用,如有不足之处,还请指正。
105 2
|
8月前
|
数据挖掘 数据处理 C#
WPF技术之DataGrid控件
WPF DataGrid是一种可以显示和编辑数据的界面控件。它可以作为表格形式展示数据,支持添加、删除、修改、排序和分组操作。
173 0
|
8天前
|
C# 开发者 C++
一套开源、强大且美观的WPF UI控件库
一套开源、强大且美观的WPF UI控件库
125 0
|
5月前
|
算法 C# UED
浅谈WPF之控件模板和数据模板
WPF不仅支持传统的Windows Forms编程的用户界面和用户体验设计,同时还推出了以模板为核心的新一代设计理念。在WPF中,通过引入模板,将数据和算法的“内容”和“形式”进行解耦。模板主要分为两大类:数据模板【Data Template】和控件模板【Control Template】。
94 8