技術總結:自動擴張WPF樹型表格列寬

作者: BloodyAngel  來源: 博客園  發布時間: 2010-11-08 15:53  閱讀: 1717 次  推薦: 1   原文鏈接   [收藏]  
摘要:在這里我們將從作者所經歷的一個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。所以只有改成了這樣:

 
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》。所以我可以使用以下方法找到它,而不用考慮新的模板是否有它:

 
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屬性中各行對這些可視元素進行維護,讓它們顯示得跟表格一樣。

  至此,第三個問題解決了:

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

     最后一個問題,是過程中最麻煩的一個問題。我們看到,圖2中該行下的第一個元素是第一列的顯示元素,顯示了“2.1”。但是文本左邊的Expander控件卻是TreeGridViewRowPresenter的最后一個可視化孩子。而且縮進并不是一個控件。那么這是怎么一回事呢?看了TreeGridViewRowPresenter的源碼后,發現原來是它主動把Expander放在了最后:

 
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;
}
}
}
而文本前面先顯示縮進,然后再顯示Expander的原因是由于TreeGridViewRowPresenter類重寫了FrameworkElement.ArrangeOverride方法。在該方法中,它把第一列的元素顯示的長度變短,在之前顯示一段縮進的空白和Expander控件:
 
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;
}

  分析到這里,就知道如何計算出第一列的最終寬度了:

 
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的設計過程(見之前的文章)。

1
0
 
標簽:WPF .NET
 
 

文章列表

arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

    大師兄 發表在 痞客邦 留言(0) 人氣()