由一个Bug引出的自动扩张WPF树型表格列宽问题

开发 后端
在这里我们将从作者所经历的一个Bug开始谈起,主要涉及自动扩张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. this.ItemContainerGenerator.StatusChanged += (o, e) =>    
  2.  {    
  3. if (this.ItemContainerGenerator.Status == GeneratorStatus.ContainersGenerated)    
  4.   {    
  5.       this.AdjustFirstColumnWidth();    
  6.    }    
  7.  };   

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

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

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

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

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

要解决第三个问题,我们需要知道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. public class TreeGridViewRowPresenter : GridViewRowPresenter    
  2. {    
  3.   protected override System.Windows.Media.Visual GetVisualChild(int index)    
  4.    {    
  5.      // Last element is always the expander    
  6.    // called by render engine    
  7.   if (index < base.VisualChildrenCount) return base.GetVisualChild(index);    
  8.  if (index == base.VisualChildrenCount) return this.lbRowNo;    
  9.  return this.Expander;    
  10.     }    
  11.   protected override int VisualChildrenCount    
  12.  {    
  13.   get   
  14.      {    
  15.         // Last element is always the expander    
  16.       if (this.Expander != null)    
  17.         return base.VisualChildrenCount + 2;    
  18.    else   
  19.       return base.VisualChildrenCount + 1;    
  20.    }    
  21.    }    
  22. }  

而文本前面先显示缩进,然后再显示Expander的原因是由于TreeGridViewRowPresenter类重写了FrameworkElement.ArrangeOverride方法。在该方法中,它把***列的元素显示的长度变短在之前显示一段缩进的空白和Expander控件:

  1. protected override Size ArrangeOverride(Size arrangeSize)    
  2.  {    
  3.  Size s = base.ArrangeOverride(arrangeSize);    
  4. if (this.Columns == null || this.Columns.Count == 0) return s;    
  5. UIElement expander = this.Expander;    
  6. double current = 0;    
  7. double max = arrangeSize.Width;    
  8.  for (int x = 0; x < this.Columns.Count; x++)    
  9. {    
  10.   GridViewColumn column = this.Columns[x];    
  11.   // Actual index needed for column reorder    
  12.   UIElement uiColumn = (UIElement)base.GetVisualChild((int)ActualIndexProperty.GetValue(column, null));    
  13.  // Compute column width    
  14.   double w = Math.Min(max, (Double.IsNaN(column.Width)) ? (double)DesiredWidthProperty.GetValue(column, null) : column.Width);    
  15.  // First column indent    
  16.   if (x == 0 && expander != null)    
  17.   {    
  18.     double indent = FirstColumnIndent + expander.DesiredSize.Width;    
  19.    uiColumn.Arrange(new Rect(current + indent, 0, w - indent, arrangeSize.Height));    
  20.       }    
  21.      else   
  22.     {    
  23.       uiColumn.Arrange(new Rect(current, 0, w, arrangeSize.Height));    
  24.      }    
  25.   max -= w;    
  26.      current += w;    
  27.     }    
  28.   // Show expander    
  29.  if (expander != null)    
  30.     {    
  31.   expander.Arrange(new Rect(this.FirstColumnIndent, 0, expander.DesiredSize.Width, expander.DesiredSize.Height));    
  32.   }    
  33.   return s;    
  34. }  

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

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

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

总结

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

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

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

熟悉了Measure的使用。

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

链接:http://www.cnblogs.com/zgynhqf/archive/2010/08/05/1793405.html

 

责任编辑:彭凡 来源: 博客园
相关推荐

2022-11-13 10:07:22

SpringSpringBoot

2021-09-01 08:58:15

项目 UTFailed

2021-06-06 16:15:57

地区接口项目

2020-06-09 08:06:31

RocketMQ消息耗时

2009-09-14 17:08:02

WebFormView

2021-10-08 07:50:57

软件设计程序

2017-10-10 15:14:23

BUGiOS 11苹果

2022-04-13 07:38:50

富文本编辑器设置表格列宽

2022-04-17 10:04:32

HerokuPaaSPorter

2014-02-26 09:13:39

2014-12-17 09:40:22

dockerLinuxPaaS

2022-05-16 08:42:26

Pandasbug

2019-08-01 12:59:21

Bug代码程序

2015-08-24 10:07:13

程序员bug

2022-06-15 08:14:40

Go线程递归

2023-03-13 08:09:03

Protobuffeature分割

2019-07-01 09:58:05

鸿蒙系统华为

2021-03-23 18:01:14

SQL数据库前端

2018-02-01 16:26:44

面试题static变量

2020-09-29 07:44:20

跨域前后端分离插件
点赞
收藏

51CTO技术栈公众号