在 WPF 中显示数据矩阵






4.94/5 (67投票s)
回顾一个可以轻松绑定数据和视觉样式化的矩阵控件。
引言
本文介绍了一个WPF控件以及一组相关类,它们可以轻松创建和显示数据矩阵。除了了解矩阵控件的使用方法外,我们还将探讨它是如何工作的。
背景
显示一组相关数据实体的一种常见方法是使用矩阵,这样可以方便地进行相互比较。与数据网格一样,矩阵也包含行和列。与数据网格不同的是,矩阵的行也有标题,每个标题显示与所显示实体相关的属性或值。通过包含行标题,矩阵可以展现出标准数据网格无法显示的额外数据维度。
矩阵的构成
在本文及相关源代码中,我们通过名称引用矩阵的特定部分。下面带有注释的屏幕截图指出了每个部分。
介绍 MatrixControl 和 MatrixBase
本文顶部的源代码包包含一个Visual Studio 2008解决方案,其中有两个项目。MatrixLib项目包含MatrixControl
类,它是一个UI控件,可用于显示数据矩阵。MatrixControl
继承自ItemsControl
,并以网格布局排列其子元素。您可以轻松地将MatrixControl
放置在任何用户界面中,并将其ItemsSource
属性绑定到应显示为矩阵的对象集合。
然而,事情不止于此。将一组数据实体转化为一维对象集合,使其能够绑定到并显示在二维网格布局中,需要一些额外的逻辑。您需要以某种方式让MatrixControl
知道每个项目应放置在网格的哪个“槽”中。为了在XAML中轻松地为矩阵应用视觉样式,您需要以某种方式区分列标题、行标题和单元格。此外,您还需要找到一种方法来获取每个单元格中应显示的值(即对应于每个行/列交叉点的_值)。为了使这些任务更轻松、更快地完成,我创建了MatrixBase<TRow, TColumn>
类。您只需从MatrixBase
派生一个类并覆盖几个方法;所有繁重的工作都将为您处理好。
本文接下来的两个部分将演示如何使用MatrixControl
和MatrixBase
。
演示 1 - 国家矩阵

本文源代码包中的WpfMatrixDemo项目包含两个使用MatrixControl
的示例。在本节中,我们将看到如何创建一个矩阵,该矩阵显示国家列表以及关于这些国家的各种属性。
此演示的数据驻留在Country
对象中。下面可以看到Country
类。
class Country
{
public double ExportsInMillions { get; set; }
public double ExternalDebtInMillions { get; set; }
public string FlagIcon { get; set; }
public double GDPInMillions { get; set; }
public double LifeExpectancy { get; set; }
public string Name { get; set; }
}
Database
类创建了一个Country
对象数组。
public static Country[] GetCountries()
{
return new Country[]
{
new Country
{
Name = "Switzerland",
ExportsInMillions = 172700,
ExternalDebtInMillions = 1340000,
FlagIcon = "Flags/switzerland.png",
GDPInMillions = 492595,
LifeExpectancy = 80.62
},
new Country
{
Name = "United Kingdom",
ExportsInMillions = 468700,
ExternalDebtInMillions = 10450000,
FlagIcon = "Flags/uk.png",
GDPInMillions = 2674085,
LifeExpectancy = 78.7
},
new Country
{
Name = "United States",
ExportsInMillions = 1377000,
ExternalDebtInMillions = 13703567,
FlagIcon = "Flags/usa.png",
GDPInMillions = 14264600,
LifeExpectancy = 78.06
}
};
}
Country
对象数组被加载到下面声明的CountryMatrix
中。
/// <summary>
/// A matrix that displays countries in the columns
/// and attributes of a country in the rows.
/// </summary>
class CountryMatrix : MatrixBase<string, Country>
{
// ...
}
请注意,MatrixBase
是一个泛型类,有两个类型参数。第一个类型参数TRow
指定将放置在行标题中的对象类型。第二个类型参数TColumn
指示将放置在列标题中的对象类型。CountryMatrix
具有以下初始化代码。
public CountryMatrix()
{
_countries = Database.GetCountries();
_rowHeaderToValueProviderMap = new Dictionary<string, CellValueProvider>();
this.PopulateCellValueProviderMap();
}
void PopulateCellValueProviderMap()
{
// Use the American culture to force currency
// formatting to use the dollar sign ($).
CultureInfo culture = new CultureInfo("en-US");
_rowHeaderToValueProviderMap.Add(
"Exports (millions)",
country => country.ExportsInMillions.ToString("c0", culture));
_rowHeaderToValueProviderMap.Add(
"External Debt (millions)",
country => country.ExternalDebtInMillions.ToString("c0", culture));
_rowHeaderToValueProviderMap.Add(
"GDP (millions)",
country => country.GDPInMillions.ToString("c0", culture));
_rowHeaderToValueProviderMap.Add(
"Life Expectancy",
country => country.LifeExpectancy.ToString("f2"));
}
// Fields
readonly Country[] _countries;
readonly Dictionary<string, CellValueProvider> _rowHeaderToValueProviderMap;
/// <summary>
/// This delegate type describes the signature of a method
/// used to produce the value of a cell in the matrix.
/// </summary>
private delegate object CellValueProvider(Country country)
_rowHeaderToValueProviderMap
字段将行标题中显示的值与用于生成该行中每个单元格的值的回调方法相关联。该回调接收一个Country
对象作为参数(来自列标题),并返回一个值以显示在该单元格中。当我们查看CountryMatrix
的覆盖方法时,可以看到此技术是如何被应用的。
protected override IEnumerable<Country> GetColumnHeaderValues()
{
return _countries;
}
protected override IEnumerable<string> GetRowHeaderValues()
{
return _rowHeaderToValueProviderMap.Keys;
}
protected override object GetCellValue(
string rowHeaderValue, Country columnHeaderValue)
{
return _rowHeaderToValueProviderMap[rowHeaderValue](columnHeaderValue);
}
如果您打开AppWindow.xaml文件,您会看到CountryMatrix
的一个实例被设置为MatrixControl
的DataContext
,并且其MatrixItems
属性是控件ItemsSource
属性绑定的源。
<mx:MatrixControl ItemsSource="{Binding Path=MatrixItems}">
<mx:MatrixControl.DataContext>
<local:CountryMatrix />
</mx:MatrixControl.DataContext>
<mx:MatrixControl.Resources>
<ResourceDictionary Source="CountryMatrixTemplates.xaml" />
</mx:MatrixControl.Resources>
</mx:MatrixControl>
上面声明的MatrixControl
注入了一个包含在CountryMatrixTemplates.xaml文件中的ResourceDictionary
。该文件为矩阵的各个部分包含了DataTemplate
。下面是该文件的一个截短版本。唯一保持不变的模板是用于显示列标题的模板。在此演示中,每个列标题都包含一个国旗图标和国家名称。
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mx="clr-namespace:MatrixLib.Matrix;assembly=MatrixLib"
>
<!-- Shared Resources -->
<SolidColorBrush x:Key="BackBrush" Color="LightBlue" />
<SolidColorBrush x:Key="BorderBrush" Color="LightBlue" />
<Thickness x:Key="BorderThickness" Left="0" Top="0" Right="0.5" Bottom="0.5" />
<SolidColorBrush x:Key="HeaderForeground" Color="DarkBlue" />
<DataTemplate DataType="{x:Type mx:MatrixColumnHeaderItem}">
<Border
Background="{StaticResource BackBrush}"
BorderBrush="{StaticResource BorderBrush}"
BorderThickness="{StaticResource BorderThickness}"
Padding="0,4"
>
<DockPanel>
<Image
DockPanel.Dock="Left"
Margin="3,0,0,0"
Source="{Binding Path=ColumnHeader.FlagIcon}"
Width="18" Height="12"
/>
<TextBlock
FontWeight="Bold"
Foreground="{StaticResource HeaderForeground}"
Text="{Binding Path=ColumnHeader.Name}"
TextAlignment="Center"
/>
</DockPanel>
</Border>
</DataTemplate>
<DataTemplate DataType="{x:Type mx:MatrixEmptyHeaderItem}">
<!-- ... -->
</DataTemplate>
<DataTemplate DataType="{x:Type mx:MatrixRowHeaderItem}">
<!-- ... -->
</DataTemplate>
<DataTemplate DataType="{x:Type mx:MatrixCellItem}">
<!-- ... -->
</DataTemplate>
</ResourceDictionary>
上面显示的每个DataTemplate
都针对一个表示矩阵某个部分的类型,例如MatrixColumnHeaderItem
的模板用于渲染列标题。所有这些类型都继承自MatrixItemBase
,如下面的类图所示。在创建用于渲染这些类型实例的模板时,此信息非常有用。

在本文稍后,我们将回顾MatrixControl
如何在内部创建和排列这些类型。现在,只需接受MatrixBase
将自动将这些类型的实例放入MatrixControl
中。
演示 2 - 人员矩阵

在之前的演示中,我们看到了如何显示一个矩阵,其中包含国家名称作为列标题,国家的属性作为行标题,以及每个国家每个属性的值作为单元格。在此演示中,我们将创建一个不同类型的矩阵。该矩阵将人员姓名显示为列标题,将这些人居住的唯一国家列表显示为行标题,并在单元格中显示一个视觉指示符,以表明某人是否居住在某个国家。我们不显示人员的各种属性作为行标题,而是显示人员一个属性的唯一值列表作为行标题。
以下是Database
类中创建Person
对象数组的方法。
public static Person[] GetPeople()
{
return new Person[]
{
new Person
{
Name= "Brennon",
CountryOfResidence = "United Kingdom"
},
new Person
{
Name="Josh",
CountryOfResidence ="United States"
},
new Person
{
Name="Karl",
CountryOfResidence= "United States"
},
new Person
{
Name="Laurent",
CountryOfResidence="Switzerland"
},
new Person
{
Name="Sacha",
CountryOfResidence= "United Kingdom"
}
};
}
在此演示中,PersonMatrix
类继承自MatrixBase
。下面是该类的完整列表。
/// <summary>
/// A matrix that displays people in the columns
/// and countries in which people live in the rows.
/// </summary>
public class PersonMatrix : MatrixBase<string, Person>
{
public PersonMatrix()
{
_people = Database.GetPeople();
}
protected override IEnumerable<Person> GetColumnHeaderValues()
{
return _people;
}
protected override IEnumerable<string> GetRowHeaderValues()
{
// Return a sorted list of unique country names.
return
from person in _people
orderby person.CountryOfResidence
group person by person.CountryOfResidence into countryGroup
select countryGroup.Key;
}
protected override object GetCellValue(
string rowHeaderValue, Person columnHeaderValue)
{
return rowHeaderValue == columnHeaderValue.CountryOfResidence;
}
readonly Person[] _people;
}
由于此矩阵不需要每个数据实体的多个属性值,因此不需要第一演示中使用的行到单元格值映射技术。GetCellValue
方法仅当指定的人居住在指定国家时返回true
,否则返回false
。
以下是AppWindow.xaml中配置MatrixControl
以显示PersonMatrix
实例的标记。
<mx:MatrixControl ItemsSource="{Binding Path=MatrixItems}">
<mx:MatrixControl.DataContext>
<local:PersonMatrix />
</mx:MatrixControl.DataContext>
<mx:MatrixControl.Resources>
<ResourceDictionary Source="PersonMatrixTemplates.xaml" />
</mx:MatrixControl.Resources>
</mx:MatrixControl>
PersonMatrixTemplates.xaml文件包含用于视觉样式化此矩阵的DataTemplate
。该文件中一个有趣的点是渲染每个矩阵单元格的模板。它使用DataTrigger
来隐藏视觉指示符,如果某人不住在与该单元格所在行关联的国家。
<DataTemplate DataType="{x:Type mx:MatrixCellItem}">
<Border
x:Name="bd"
Background="#110000FF"
BorderBrush="{StaticResource BorderBrush}"
BorderThickness="{StaticResource BorderThickness}"
>
<Ellipse
x:Name="ell"
Fill="DarkBlue"
HorizontalAlignment="Center"
Width="16" Height="16"
VerticalAlignment="Center"
/>
</Border>
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding Path=Value}" Value="False">
<Setter TargetName="ell" Property="Visibility" Value="Collapsed" />
<Setter TargetName="bd" Property="Background" Value="White" />
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
至此,我们已经回顾了使用MatrixControl
和MatrixBase
的两个示例。本文的其余部分将探讨这些类的工作原理。
MatrixControl的工作原理
MatrixControl
类非常简单。它只是一个ItemsControl
子类,带有一个自定义的ItemsPanel
和ItemContainerStyle
。其代码隐藏文件为空,除了在构造函数中调用InitializeComponent
的标准样板代码外。以下XAML是MatrixControl
的全部内容。
<ItemsControl
x:Class="MatrixLib.Matrix.MatrixControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:layout="clr-namespace:MatrixLib.Layout"
>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<layout:MatrixGrid />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemContainerStyle>
<!--
Bind each ContentPresenter's attached Grid
properties to MatrixItemBase properties.
-->
<Style TargetType="{x:Type ContentPresenter}">
<Setter Property="Grid.Row" Value="{Binding Path=GridRow}" />
<Setter Property="Grid.Column" Value="{Binding Path=GridColumn}" />
</Style>
</ItemsControl.ItemContainerStyle>
</ItemsControl>
此控件的实际逻辑存在于MatrixGrid
布局面板、MatrixBase
以及各种MatrixItemBase
子类中。让我们接下来看看它们是如何工作的。
MatrixGrid的工作原理
如上所述,MatrixControl
是一个ItemsControl
子类,它使用MatrixGrid
作为其项目面板。MatrixGrid
继承自标准的Grid
面板,并能够向自身添加适当数量的行和列,以便正确地容纳其子元素。截至目前,当MatrixGrid
的视觉子元素被移除或移动到不同的行或列时,它不会从自身移除行或列,仅仅是因为我没有用例需要此功能。
MatrixGrid
监视其视觉子元素(即其“子元素”)。它在其子元素上建立绑定,特别是对其子元素的Grid.Row
和Grid.Column
附加属性进行绑定。当它检测到子元素上任一附加属性的新值时,它将根据需要向自身添加更多的行或列,以便允许该子元素定位在其所需的位置。数据绑定源是子元素,目标绑定是一个MatrixGridChildMonitor
,其定义如下:
/// <summary>
/// Exposes two dependency properties which are bound to in
/// order to know when the visual children of a MatrixGrid are
/// given new values for the Grid.Row and Grid.Column properties.
/// </summary>
class MatrixGridChildMonitor : DependencyObject
{
public int GridRow
{
get { return (int)GetValue(GridRowProperty); }
set { SetValue(GridRowProperty, value); }
}
public static readonly DependencyProperty GridRowProperty =
DependencyProperty.Register(
"GridRow",
typeof(int),
typeof(MatrixGridChildMonitor),
new UIPropertyMetadata(0));
public int GridColumn
{
get { return (int)GetValue(GridColumnProperty); }
set { SetValue(GridColumnProperty, value); }
}
public static readonly DependencyProperty GridColumnProperty =
DependencyProperty.Register(
"GridColumn",
typeof(int),
typeof(MatrixGridChildMonitor),
new UIPropertyMetadata(0));
}
每个子元素在被添加到MatrixGrid
时都会绑定到其中一个监视器对象。以下是MatrixGrid
中建立绑定的代码。
protected override void OnVisualChildrenChanged(
DependencyObject visualAdded, DependencyObject visualRemoved)
{
base.OnVisualChildrenChanged(visualAdded, visualRemoved);
if (visualAdded != null)
this.StartMonitoringChildElement(visualAdded);
else
this.StopMonitoringChildElement(visualRemoved);
}
void StartMonitoringChildElement(DependencyObject childElement)
{
// Create a MatrixGridChildMonitor in order to detect
// changes made to the Grid.Row and Grid.Column attached
// properties on the new child element.
MatrixGridChildMonitor monitor = new MatrixGridChildMonitor();
BindingOperations.SetBinding(
monitor,
MatrixGridChildMonitor.GridRowProperty,
this.CreateMonitorBinding(childElement, Grid.RowProperty));
BindingOperations.SetBinding(
monitor,
MatrixGridChildMonitor.GridColumnProperty,
this.CreateMonitorBinding(childElement, Grid.ColumnProperty));
_childToMonitorMap.Add(childElement, monitor);
}
Binding CreateMonitorBinding(DependencyObject childElement, DependencyProperty property)
{
return new Binding
{
Converter = _converter,
ConverterParameter = property,
Mode = BindingMode.OneWay,
Path = new PropertyPath(property),
Source = childElement
};
}
Dictionary<DependencyObject, MatrixGridChildMonitor> _childToMonitorMap;
MatrixGridChildConverter _converter;
您可能想知道,仅仅绑定到元素上的Grid.Row
和Grid.Child
的值如何能让MatrixGrid
知道它应该为自己创建多少行和列。答案在于使用一个值转换器,称为MatrixGridChildConverter
。该转换器会拦截从子元素传输到其关联的MatrixGridChildMonitor
的值,并通知MatrixGrid
新值。以下是该值转换器中的Convert
方法。
public object Convert(
object value, Type targetType,
object parameter, CultureInfo culture)
{
if (value is int)
{
int index = (int)value;
if (parameter == Grid.RowProperty)
_matrixGrid.InspectRowIndex(index);
else
_matrixGrid.InspectColumnIndex(index);
}
return value;
}
当MatrixGrid
通过InspectRowIndex
或InspectColumnIndex
方法得知其子元素上新的行或列索引时,它会向自身添加适当数量的行/列。以下是MatrixGrid
中添加正确行数的代码。
internal void InspectRowIndex(int index)
{
// Delay the call that adds rows in case the RowDefinitions
// collection is currently read-only due to a layout pass.
base.Dispatcher.BeginInvoke(new Action(delegate
{
while (base.RowDefinitions.Count - 1 < index)
{
base.RowDefinitions.Add(new RowDefinition());
// Make the column headers just tall
// enough to display their content.
if (base.RowDefinitions.Count == 1)
base.RowDefinitions[0].Height =
new GridLength(1, GridUnitType.Auto);
}
}));
}
InspectColumnIndex
方法与上面看到的方法非常相似。
MatrixBase的工作原理

这个难题的最后一块是MatrixBase
类。如前所述,您可以从该类派生,覆盖几个方法,然后使用该类的一个实例作为MatrixControl
的数据源。MatrixControl
的ItemsSource
属性应绑定到MatrixBase
的MatrixItems
属性,该属性定义如下:
/// <summary>
/// Returns a read-only collection of all cells in the matrix.
/// </summary>
public ReadOnlyCollection<MatrixItemBase> MatrixItems
{
get
{
if (_matrixItems == null)
{
_matrixItems = new ReadOnlyCollection<MatrixItemBase>(this.BuildMatrix());
}
return _matrixItems;
}
}
当BuildMatrix
方法执行时,子类的覆盖方法会被调用,以检索列标题列表和行标题列表。然后,MatrixBase
开始构建MatrixItemBase
派生对象,并将它们注入由子类覆盖方法返回的任何对象。当每个MatrixCellItem
被创建时,子类会被要求为该单元格提供一个值。下面列出了来自MatrixBase
的完整算法。
List<MatrixItemBase> BuildMatrix()
{
List<MatrixItemBase> matrixItems = new List<MatrixItemBase>();
// Get the column and row header values from the child class.
List<TColumn> columnHeaderValues = this.GetColumnHeaderValues().ToList();
List<TRow> rowHeaderValues = this.GetRowHeaderValues().ToList();
this.CreateEmptyHeader(matrixItems);
this.CreateColumnHeaders(matrixItems, columnHeaderValues);
this.CreateRowHeaders(matrixItems, rowHeaderValues);
this.CreateCells(matrixItems, rowHeaderValues, columnHeaderValues);
return matrixItems;
}
void CreateEmptyHeader(List<MatrixItemBase> matrixItems)
{
// Insert a blank item in the top left corner.
matrixItems.Add(new MatrixEmptyHeaderItem
{
GridRow = 0,
GridColumn = 0
});
}
void CreateColumnHeaders(
List<MatrixItemBase> matrixItems, List<TColumn> columnHeaderValues)
{
// Insert the column header items in the first row.
for (int column = 1; column <= columnHeaderValues.Count; ++column)
{
matrixItems.Add(new MatrixColumnHeaderItem(columnHeaderValues[column - 1])
{
GridRow = 0,
GridColumn = column
});
}
}
void CreateRowHeaders(
List<MatrixItemBase> matrixItems, List<TRow> rowHeaderValues)
{
// Insert the row headers items in the first slot
// of each row after the column header row.
for (int row = 1; row <= rowHeaderValues.Count; ++row)
{
matrixItems.Add(new MatrixRowHeaderItem(rowHeaderValues[row - 1])
{
GridRow = row,
GridColumn = 0
});
}
}
void CreateCells(
List<MatrixItemBase> matrixItems,
List<TRow> rowHeaderValues,
List<TColumn> columnHeaderValues)
{
// Insert a cell item for each row/column intersection.
for (int row = 1; row <= rowHeaderValues.Count; ++row)
{
TRow rowHeaderValue = rowHeaderValues[row - 1];
for (int column = 1; column <= columnHeaderValues.Count; ++column)
{
// Ask the child class for the cell's value.
object cellValue = this.GetCellValue(
rowHeaderValue,
columnHeaderValues[column - 1]);
matrixItems.Add(new MatrixCellItem(cellValue)
{
GridRow = row,
GridColumn = column
});
}
}
}
请注意,上面代码中创建的每个MatrixItemBase
派生对象如何被赋值GridRow
和GridColumn
属性。这些属性通过MatrixControl
的ItemContainerStyle
进行绑定,因此承载MatrixItemBase
对象的ContentPresenter
将被赋予正确的Grid.Row
和Grid.Column
附加属性值。来自MatrixControl
的XAML完成了此绑定,如下所示。
<ItemsControl.ItemContainerStyle>
<!--
Bind each ContentPresenter's attached Grid
properties to MatrixItemBase properties.
-->
<Style TargetType="{x:Type ContentPresenter}">
<Setter Property="Grid.Row" Value="{Binding Path=GridRow}" />
<Setter Property="Grid.Column" Value="{Binding Path=GridColumn}" />
</Style>
</ItemsControl.ItemContainerStyle>
修订历史
- 2009年6月14日 – 文章发布到CodeProject