65.9K
CodeProject 正在变化。 阅读更多。
Home

一个在单元格中包含持久控件的 C# WPF .NET 4.0 "DataGrid"

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.29/5 (10投票s)

2011年5月5日

CPOL

10分钟阅读

viewsIcon

49007

downloadIcon

2407

类似 DataGrid 的控件,在单元格内具有持久控件。

引言

WPF .NET 4.0 中的官方 DataGrid 在我看来表现不尽如人意。你必须点击单元格两次:一次激活它,一次编辑或将焦点设置到控件上。我认为从用户角度来看,这完全不切实际。

经过一番研究,我在网上找到了一些关于如何通过一次点击而不是两次来选中复选框的文章。查看代码后,我意识到这些只是充满了 bug 的补丁,并且只解决了 checkbox 的问题。

本文介绍了一个伪 DataGrid,其单元格填充了行为与普通控件相同的控件。它确实有一些缺点:单元格的高度是固定的,不能像 DataGrid 那样通过左侧滚动条进行调整,未实现选择模式……我确信用户会发现其他问题。我认为这些问题都不是关键的,并且总有可能在以后改进和修复代码。

然而,最重要的是,它对于应用程序用户来说实用,因为当您点击控件时,它们会立即获得焦点。此外,由于插入的控件本质上是标准控件,因此在应用主题时它们具有相同的外观和感觉。

screenshot.png

代码简述

在进行任何开发之前,我需要研究一些领域

  • 我需要知道如何在水平滚动时使标题与包含控件的表格同步,而在垂直滚动时保持固定。这可以通过 2 个 ScrollViewers 实现,其中一个使用样式 "ExtScrollView"。请参阅文件 DataGridPCtrls.xaml
  • 我认为另一个必要的领域是应该可以调整单元格宽度。为此目的创建了一个特殊的 UserControlSplitterGrid’。此控件只是在网格元素之间添加一个 GridSplitter。由于 GridSplitter 不能太宽,我扩展了它的捕获区域。请参阅 SplitGrid_MouseMove。如果您不扩展捕获区域,用户将很难将鼠标光标放在 GridSplitter 上。

一旦完成这项研究,开发进展非常顺利。标题由一个填充了 Header 用户控件的 SplitterGrid 组成。主网格只是一个 Column 用户控件的 StackPanelColumn 用户控件本身是另一个 StackPanel,但方向不同。

每个“单元格”由一个 Border 组成,其中包含单个控件。Border 只有两边可见,因此您可以看到网格方面。单元格中的控件的边框厚度设置为 0,以获得更好的外观。

Header 宽度改变时,相应的 Column 的宽度也需要改变。这是通过在 SplitterGrid 中引发 WidthChangedEventArgs 事件来完成的。然后在 DataGridPCtrls 中的 Headers_WidthChanged 函数中进行处理。(加 1 是为了考虑 GridSplitter 的宽度。)

有用于处理 Tab、Up、Down 等按键的代码。处理在“Column”中通过 ctrl_PreviewKeyDown 处理程序完成。一个委托 ‘KeyDownDelegate’ 被传递给 Column,并通过委托的回调功能将处理转移到 OnKeyDown

标题通常从主水平滚动条滚动。但是,如果您尝试 Shift + Tab 或 Tab,则可以在主水平滚动条未干预的情况下滚动标题。这导致标题和主要部分不再同步。为了纠正这个问题,添加了一个 RangeBase.ValueChanged 处理程序 'HeaderScroll_ValueChanged'。这将在少数需要的情况下使主要部分与标题同步。

在第二个版本中,我实现了排序。为此,我创建了一个包含两个成员的 SortStruct 结构体:一个控件列表 ‘RowList’ 和一个对象 ‘SortingObj’。每一行的所有控件都存储在 RowList 中,排序列的值存储在 SortingObj 中。对所有行完成此操作后,通过使用 LINQ 查询并利用 ‘SortingObj’ 上的 orderby ascending/descending 功能来执行排序。表格必须在再次填充之前清空所有控件。这是因为否则一个控件将是另外两个控件的逻辑子项,并会抛出异常。排序是对控件而不是其值进行的。这是因为除了值之外,一个控件可能还包含其他信息,这些信息使其与同一列上的其他控件区分开来。例如,对于 ComboBoxes,每个 ComboBox 可能在其下拉列表中包含不同的元素,就会发生这种情况。

在此控件的第三个版本中,我实现了样式可配置属性和主题支持。不这样做几乎是异端,因为这些功能几乎是 WPF 存在的理由。另一方面,我承认自己是 C#/WPF 的初学者,但学习速度很快。

只有 DataGridPCtrls 旨在由客户端应用程序使用,因此只将 DataGridPCtrls 属性在 XAML style 中配置——也就是说,将它们更改为依赖属性。

相关属性有 4 个:GridLinesColorGridBackColorCanUserResizeColumnsCanUserSortColumns。当设置这些属性时,它们都需要改变内部数据。我知道有两种方法可以做到这一点:

  • 使用 xxxChangedCallBack 静态函数注册依赖属性
  • 覆盖属性,使其调用 xxxCoerceValueCallBack 静态函数

我原以为只有第一种方法适用于此处,因为第二种方法似乎只在 DependencyProperty 被继承时使用,例如 BorderThicknessProperty。在这种情况下,我们想改变内部边框厚度,而不是外部边框厚度,这就是为什么这里需要覆盖该属性。

奇怪的是,即使 CanUserResizeColumnsCanUserSortColumns 这些属性没有被继承,只有第二种方法才有效。

除了依赖属性,此版本我还实现了主题支持。为此,我在 AssemblyInfo.cs 文件中做了常规更改,从

[assembly:ThemeInfo(ResourceDictionaryLocation.None, 
	ResourceDictionaryLocation. SourceAssembly)]

to

[assembly:ThemeInfo(ResourceDictionaryLocation.SourceAssembly, 
	ResourceDictionaryLocation.SourceAssembly)]

下一步是在 Theme 文件夹中创建常规的 .xaml 文件。我试图将这些文件的内容完全限制为主题特定的 XAML 代码。将 ‘Header’ 类从 UserControl 更改为 WPF 自定义控件是合理的,因此主题文件中有一些额外的代码在各处重复。我想知道一种避免这种情况的方法。将重复的代码放在单独的 XAML 文件中并调用 MergeDictionary 不起作用。

我需要调用 TryFindResource。例如,SplitterGrid 分隔符的颜色需要设置为与 Header 的边框相同。为了使 TryFindResource 工作,我们需要在 App.xaml 文件中添加以下代码:

<ResourceDictionary>
    <ResourceDictionary.MergedDictionaries>
        <ResourceDictionary Source="{ThemeDictionary PersistDataGrid}"/>     
    </ResourceDictionary.MergedDictionaries>
</ResourceDictionary> 

完成所有这些后,当我打开应用程序并更改主题时,布局仍然有问题。将 .XAML 文件中所有主题特定的资源从 StaticResource 更改为 DynamicResource 在很大程度上解决了问题。

需要使用 Loaded RoutedEventHandler 函数修复剩余问题,该函数在每次主题更改时调用:SplitterGrid 分隔符、排序箭头和行高。

我没有发现任何主题问题,并且已在 Windows 7、XP 和 Vista 中尝试了主题更改。Aero、Classic 和所有 Luna 类型都运行良好。

我觉得代码中其他一切都非常简单,没有必要进行额外的讨论。

Using the Code

DataGridPCtrls 的使用可以在 MainWindow.xamlMainWindow.xaml.cs 中看到。我用于操作的函数是:

  • AddColumn
  • AddRow
  • GetRowDetails
  • SetRowDetails
  • GetRowCount
  • GetControl

这些将在下面更详细地描述

public void AddColumn(string strHeader,
ColumnType ct = ColumnType.TextBox, double width = 145.0);

其中

  • strHeader:将显示在标题列中的文本
  • ColumnType:这是一个 enum,可以是 TextBoxCheckBoxComboBoxDatePicker 之一。默认为 TextBox
  • width:列的宽度。默认为 145.0
public void AddRow();
public void AddRow(object[] RowDetails);

如果未指定参数,则插入一个简单的空行。否则,指定一个对象数组,用于添加带有已填充字段的行。该对象必须是:

  • TextBoxComboBox 列的 string
  • CheckBox 列的布尔值
  • DatePicker 列的 DateTime
public void GetRowDetails(int row, out object[] RowDetails);

row 表示返回的行。从 0 开始计数。RowDetails 是一个对象数组,它遵循与 AddRow 中的 RowDetails 相同的规则。

public void SetRowDetails(int row, object[] RowDetails);

row 设置相关行的详细信息。从 0 开始计数。RowDetails 是一个对象数组,它遵循与 AddRowRowDetails 相同的规则。

public int GetRowCount(int iCol = 0);

iCol 指示列的行数(从 0 开始)。默认情况下,选择第一列的行数。目前,所有列的行数相同,但可以在代码中进行更改,使其不再如此。

public Control GetControl(int iRow, int iCol);

iRowiCol 表示行和列。它们从 0 开始计数。返回特定单元格中的控件(TextBoxComboBoxCheckBoxDatePicker)。

此外,还有 4 个额外的属性:

  • GridBackColorGridLinesColor,可用于配置网格线和背景颜色
  • CanUserResizeColumns - 如果设置为 False,则列不能再调整大小。
  • CanUserSortColumns - 如果设置为 False,则不再可能进行列排序和高亮显示。
<loc:DataGridPCtrls x:Name="MyPerDataGrid" Margin="22,56,22,0"
Height="192" GridBackColor="White" GridLinesColor="Blue" 
CanUserResizeColumns="True" CanUserSortColumns="True" VerticalAlignment="Top"/>

关注点

我最初尝试使用标准的 DataGrid,但花费了太多时间试图让它表现出不同的行为。这简直是浪费时间,因为 DataGrid 本质上并不是为了让单元格中的控件通过单击一次即可激活而设计的。

DataGridPCtrls 的设计和编码进展非常迅速。我花了一天时间进行开发前的研究,然后花了 3.5 天进行编码和 bug 修复。总共,初始版本花费了 4.5 天,我认为这是非常合理的,特别是我在 C#/WPF 方面仍然相当新手(但话又说回来,大多数人在此阶段可能都是如此,因为它太新了)。

由于最初的文章引起了极大的兴趣,我额外花了 2 天时间进行改进,主要用于实现列排序功能。

调整列宽时的滚动行为对我来说不太满意。有时,调整大小的列的左侧会移动,而实际上应该是右侧移动。这可能可以修复,但我尚未研究。在所有情况下,当用户向左滚动时,行为是正常的。我很难想象用户会花费大量时间来调整列宽来娱乐自己。无论如何,现在可以使用 CanUserResizeColumns 属性来防止列调整大小。

在第三个版本中,我学到了很多关于主题的知识:例如,何时使用 WPF UserControl 或 WPF Custom Control。如果需要将数据保存到 XAML 定义的字段中,Custom Control 就不实用。例如,这样的控件不适用于 DataGridPCtrls,因为您必须将数据保存到 StackPanel 中,并且每次更改主题时数据都会丢失。我学到了艰难(也可能是最好的)方法:通过实践!

否则,我没有发现任何其他重大问题,并且对这段代码的许多改进都很容易实现:行高调整……

请注意:我非常清楚这些改进,其中大部分都非常简单。在这个阶段,我想了解这个项目实际的实用性如何。

一些主题截图

我强制使用了主题,以便显示一些截图。这些截图如下所示:

Aero

Aero.png

经典

Classic.png

Luna

LunaNormal.png

Luna Homestead

LunaHomestead.png

Luna metallic

LunaMetallic.png

Royale

Royale.png

致谢

以下链接非常有帮助:

关于重新样式化的 ScrollViewer

这些链接中的第一个展示了如何使用 ScrollViewer 的样式。第二个提供了如何获得带有固定标题的 ScrollViewer 的提示(但我自己找到了方法)。

带有 GridSplitter 分隔符的标题

我的 SplitterGrid 用户控件受此启发,但代码已完全重写。

历史

  • 2011年5月3日:初始版本
  • 2011年5月12日:由于初始版本受到极大关注,现已实现列排序功能,以及 SetRowDetails 函数和 CanUserResizeColumnsCanUserSortColumns 属性。
  • 2011年5月15日:附上项目源代码的新版本。除了在文件 DataGridPCtrls.xaml.cs 中修复了一个 bug 外,没有其他更改。通过此更改,当光标位于标题上并用户将其移至滚动条时,右侧垂直滚动条会获得鼠标捕获并被高亮显示。
  • 2011年8月28日:已进行更改 - 支持主题和 XAML 样式属性
© . All rights reserved.