WPF 中的第一步:如何填充网格中的单元格?
First steps in WPF: how to fill in a cell in a grid?
我来自 Delphi、Java、...,现在我需要制作我的第一个 WPF 程序。
我正在制作一个程序来读取和写入现有 tables 的信息。
我的想法很简单:
- 创建网格并将其放在窗体上
- 阅读table:
- 首先阅读列名。将它们添加到网格的第一个单元格 (
var_Grid.Cells[0,i] = "Column_Name";
)
- 其次读取所有数据,当前在table,填入格子(
var_grid.Cells[i,j] = "data";
- 做进一步处理
因为我想先在我的网格上做一些处理,所以我想使用一个网格组件,而不是链接到数据库(所以没有 TDBGrid
,对于那些知道 Delphi ).
我很快意识到 WPF Grid
用于将组件添加到表单(对于 Java 程序员来说就像 JPanel
和 GridLayout
,所以我想选择 DataGrid
组件。
然而,当我查看 how to select a cell in a DataGrid
时:我看到 DataGridCellInfo
、ItemContainerGenerator
、ContainerFromIndex
、...,所有这些只是为了获得一个单元格? (甚至没有用于在该单元格中输入数据的代码)
我觉得DataGrid
太复杂了,肯定有WPF可视化组件,比那个更容易填,但是在工具箱里搜索Grid
没有答案。
有人知道使用 WPF 的更简单方法吗DataGrid
或者有人知道更简单的组件可以满足我的要求吗?
第一条评论后编辑
我从 ListView
和 GridView
开始。我很快就让 GridViewColumn
工作了,但后来我遇到了一个问题,我不知道 ItemsSource
会有多少列(我尝试使用 List<List<string>>
但这似乎太天真了:-)
第二次编辑
显然之间存在不匹配:
- 我想做什么,并且:
- 为什么我想这样做。
我想做的是创建一个网格,在运行时,我可以定义行数和列数并填充单元格。
然而,评论中的讨论充斥着我为什么要这样做。
所以我想重新表述一下我的问题:是否有一个 WPF 可视化网格组件,我可以在运行时设置和更改行号和列号并逐个单元格地填充?
如果该网格组件有可能有一个 header 行,那就太好了。
有人知道这样的可视化 WPF 组件吗?
提前致谢
你的实现方式根本就错了。
您想要直接操作 WPF UI 元素的内容。
但是 WPF 是建立在一个非常不同的概念之上的。
WPF中获取数据的主要方式是使用Data Context。
UI 元素本身通过Bindings为其属性获取值。
默认情况下,绑定使用数据上下文作为源。
因此,为了正确实施,您必须首先创建一个实体,该实体将在其属性中为其一个 DataGrid 行提供数据。
然后用这些实体创建一个可观察的集合。
并将此集合绑定到 DataGrid.ItemsSource.
DataGrid 会自动为实体的每个 属性 创建一列,并在其中显示 属性 值。
如果您愿意,您可以自己自定义所需的 DataGrid 视图及其单元格。
因此,您的任务将简化为处理集合及其元素,而不是在 DataGrid 及其可视化树中进行相当复杂的搜索。
DataGrid 的通用来源之一可以是 DataTable。
当您无法为一行的实体预定义所有必需的属性时,应使用此类型。
如果您打算动态修改 DataGrid
的列,您应该选择 DataTable
作为网格的数据模型。
请注意,adding/removing 列需要更新每一行以便为新列提供值,这会对性能产生显着影响,具体取决于 table 模型的大小(特别是行数)。
在这种情况下,您应该实施数据虚拟化。您将跟踪视口,即视图中的当前项目并根据需要更新待处理的行数据更改。
例子
这是一个非常基本的示例,展示了如何在 DataTable
class.
的帮助下在运行时对 DataGrid
的列进行修改
该示例使用了数据绑定(极大地简化了逻辑)和命令:
此外,该示例依赖于 DataGrid
的列自动生成(此功能默认启用)——当然,否则解决方案的复杂性会增加。如果没有自动生成列,您将不得不向视图添加(简单的)逻辑。
MainViewModel.cs
这个 class 使用了一个 RelayCommand
。您可以在 Microsoft Docs: Relaying Command Logic.
找到示例实现
public class MainViewModel : INotifyPropertyChanged
{
public MainViewModel()
{
this.TableData = new DataTable();
AddColumn<int>("ID");
AddColumn<string>("Username");
AddColumn<string>("Mail");
AddRow(1, "Me", "me@mail.com");
}
private void AddColumn<TColumnData>(string columnName, int columnIndex = -1)
{
var newColumn = new DataColumn(columnName, typeof(TColumnData));
this.TableData.Columns.Add(newColumn);
if (columnIndex > -1)
{
newColumn.SetOrdinal(columnIndex);
}
int newColumnIndex = this.TableData.Columns.IndexOf(newColumn);
// Initialize existing rows with a default value for the new column.
// In this example the default value of the column's data type is used.
foreach (DataRow row in this.TableData.Rows)
{
row[newColumnIndex] = default(TColumnData);
}
}
// Ensure that column values are ordered by their column's index
private void AddRow(params object[] columnValues)
{
DataRow rowModelWithCurrentColumns = this.TableData.NewRow();
this.TableData.Rows.Add(rowModelWithCurrentColumns);
for (int columnIndex = 0; columnIndex < this.TableData.Columns.Count; columnIndex++)
{
rowModelWithCurrentColumns[columnIndex] = columnValues[columnIndex];
}
}
// Force the binding target (DataGrid) to create new column templates
// by changing the binding source instance
private void OnDataTableColumnsChanged() => this.TableData = this.TableData.DefaultView.ToTable();
// Add columns dynamically via an ICommand
public ICommand AddColumnCommand => new RelayCommand(commandParameter =>
{
AddColumn<DateTime>($"Timestamp {this.TableData.Columns.Count}");
OnDataTableColumnsChanged();
});
public ICommand AddRowCommand => new RelayCommand(commandParameter => AddRow(2, "You", "you@mail.com", DateTime.Now));
private DataTable tableData;
public DataTable TableData
{
get => this.tableData;
set
{
this.tableData = value;
OnPropertyChanged();
}
}
/*** Implementation of INotifyPropertyChanged ***/
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = "")
=> this.PropertyChanged?.Invoke(new PropertyChangedEventArgs(propertyName));
}
备注:
或者,成员 AddColumn
和 AddRow
可以作为扩展方法实现。
您可以从任何呼叫者呼叫 AddColumn
和 AddRow
- 不仅来自 ICommand
。确保在所有列 added/removed.
之后调用 OnDataTableColumnsChanged
将列更改提升到视图
在视图中,行不需要创建任何视图模板,因此这些行数据更改立即可见。列的模板中定义了如何呈现行的单元格。
另一方面,查看列需要一个视图模板来告诉 DataGrid
如何呈现列及其单元格。调用 OnDataTableColumnsChanged
强制绑定目标 (DataGrid
) 为新列生成列模板。
如果 DataTable
的数据源实际上是一个数据库,则在每次查询后都会创建一个 DataTable
的新实例,这将使对 OnDataTableColumnsChanged
的调用变得多余。
MainWindow.xaml
<Window>
<Window.DataContext>
<!-- Don't forget to qualify the type with the defined XAML namespace (xmlns) -->
<MainViewModel />
</Window.DataContext>
<StackPanel>
<Button Content="Add Column" Command="{Binding AddColumnCommand}" />
<Button Content="Add Row" Command="{Binding AddRowCommand}" />
<DataGrid ItemsSource="{Binding TableData}" />
</StackPanel>
</Window>
正如其他人指出的那样,有很多方法可以通过 DataGrid、ListView 等以表单形式实现数据内容显示。通过采用 MVVM 模式,您的模型显然是数据源来自和存储到,例如某种类型的 SQL 数据库,Excel 通过 OleDbConnection 连接等
ViewModel 是实现所有你如何获取数据和如何保存数据的核心,而且还公开按钮单击操作、用户输入验证等。你的 ViewModel 成为“DataContext”视图的绑定源基线。您通过 getter/setter 制作属性 public 来公开实际数据,例如
public DataTable MyListOfCustomers {get; set;}
然后,在您的视图模型中的某处,您 运行 您的查询除了将记录加载到此数据 table 对象中外什么都不做。
视图显然是您向最终用户显示的内容。这是您 post 中简短且模棱两可的解释所不清楚的。在几乎任何应用程序中,您可能有许多数据网格代表许多不同事物的数据,但这些数据网格通常总是代表相同的内容。例如,客户屏幕将始终显示客户列表并允许搜索、排序、特定列等。库存屏幕将始终显示可用项目等。
因此,在开发应用程序时,与其依赖如何让它始终动态可用,不如自己定义每个可视化数据网格组件,这样它始终相同并基于已知来源.然后,只需将该用户控件添加到您需要的视图中即可。如果需要对该数据网格进行任何更改,只需更改一次,使用它的任何其他视图都会受到相同的影响。
说了这么多,而且绑定本身非常简单,您可能想要编辑您的 post,而不是通过有限的评论。 Post 关于您正在尝试做的事情的更明确的上下文。所呈现的实际数据类型。这不像是你在做一些你不能透露的全新和绝密的事情。但也许该系统将具有的工作类型的总体背景。它会基于会计做数字(甚至基于股票销售)、库存系统等吗?这个数据网格是否会更改其自身显示的内容,或者它会更像我描述的每个辅助视图甚至子区域单个视图显示不同的数据网格内容。
提供含糊不清的信息,即使来自另一个开发领域,在寻求帮助时也不会很好地照顾您。
我来自 Delphi、Java、...,现在我需要制作我的第一个 WPF 程序。
我正在制作一个程序来读取和写入现有 tables 的信息。
我的想法很简单:
- 创建网格并将其放在窗体上
- 阅读table:
- 首先阅读列名。将它们添加到网格的第一个单元格 (
var_Grid.Cells[0,i] = "Column_Name";
) - 其次读取所有数据,当前在table,填入格子(
var_grid.Cells[i,j] = "data";
- 首先阅读列名。将它们添加到网格的第一个单元格 (
- 做进一步处理
因为我想先在我的网格上做一些处理,所以我想使用一个网格组件,而不是链接到数据库(所以没有 TDBGrid
,对于那些知道 Delphi ).
我很快意识到 WPF Grid
用于将组件添加到表单(对于 Java 程序员来说就像 JPanel
和 GridLayout
,所以我想选择 DataGrid
组件。
然而,当我查看 how to select a cell in a DataGrid
时:我看到 DataGridCellInfo
、ItemContainerGenerator
、ContainerFromIndex
、...,所有这些只是为了获得一个单元格? (甚至没有用于在该单元格中输入数据的代码)
我觉得DataGrid
太复杂了,肯定有WPF可视化组件,比那个更容易填,但是在工具箱里搜索Grid
没有答案。
有人知道使用 WPF 的更简单方法吗DataGrid
或者有人知道更简单的组件可以满足我的要求吗?
第一条评论后编辑
我从 ListView
和 GridView
开始。我很快就让 GridViewColumn
工作了,但后来我遇到了一个问题,我不知道 ItemsSource
会有多少列(我尝试使用 List<List<string>>
但这似乎太天真了:-)
第二次编辑
显然之间存在不匹配:
- 我想做什么,并且:
- 为什么我想这样做。
我想做的是创建一个网格,在运行时,我可以定义行数和列数并填充单元格。
然而,评论中的讨论充斥着我为什么要这样做。
所以我想重新表述一下我的问题:是否有一个 WPF 可视化网格组件,我可以在运行时设置和更改行号和列号并逐个单元格地填充?
如果该网格组件有可能有一个 header 行,那就太好了。
有人知道这样的可视化 WPF 组件吗?
提前致谢
你的实现方式根本就错了。 您想要直接操作 WPF UI 元素的内容。 但是 WPF 是建立在一个非常不同的概念之上的。 WPF中获取数据的主要方式是使用Data Context。 UI 元素本身通过Bindings为其属性获取值。 默认情况下,绑定使用数据上下文作为源。
因此,为了正确实施,您必须首先创建一个实体,该实体将在其属性中为其一个 DataGrid 行提供数据。 然后用这些实体创建一个可观察的集合。 并将此集合绑定到 DataGrid.ItemsSource.
DataGrid 会自动为实体的每个 属性 创建一列,并在其中显示 属性 值。 如果您愿意,您可以自己自定义所需的 DataGrid 视图及其单元格。
因此,您的任务将简化为处理集合及其元素,而不是在 DataGrid 及其可视化树中进行相当复杂的搜索。
DataGrid 的通用来源之一可以是 DataTable。 当您无法为一行的实体预定义所有必需的属性时,应使用此类型。
如果您打算动态修改 DataGrid
的列,您应该选择 DataTable
作为网格的数据模型。
请注意,adding/removing 列需要更新每一行以便为新列提供值,这会对性能产生显着影响,具体取决于 table 模型的大小(特别是行数)。
在这种情况下,您应该实施数据虚拟化。您将跟踪视口,即视图中的当前项目并根据需要更新待处理的行数据更改。
例子
这是一个非常基本的示例,展示了如何在 DataTable
class.
DataGrid
的列进行修改
该示例使用了数据绑定(极大地简化了逻辑)和命令:
此外,该示例依赖于 DataGrid
的列自动生成(此功能默认启用)——当然,否则解决方案的复杂性会增加。如果没有自动生成列,您将不得不向视图添加(简单的)逻辑。
MainViewModel.cs
这个 class 使用了一个 RelayCommand
。您可以在 Microsoft Docs: Relaying Command Logic.
public class MainViewModel : INotifyPropertyChanged
{
public MainViewModel()
{
this.TableData = new DataTable();
AddColumn<int>("ID");
AddColumn<string>("Username");
AddColumn<string>("Mail");
AddRow(1, "Me", "me@mail.com");
}
private void AddColumn<TColumnData>(string columnName, int columnIndex = -1)
{
var newColumn = new DataColumn(columnName, typeof(TColumnData));
this.TableData.Columns.Add(newColumn);
if (columnIndex > -1)
{
newColumn.SetOrdinal(columnIndex);
}
int newColumnIndex = this.TableData.Columns.IndexOf(newColumn);
// Initialize existing rows with a default value for the new column.
// In this example the default value of the column's data type is used.
foreach (DataRow row in this.TableData.Rows)
{
row[newColumnIndex] = default(TColumnData);
}
}
// Ensure that column values are ordered by their column's index
private void AddRow(params object[] columnValues)
{
DataRow rowModelWithCurrentColumns = this.TableData.NewRow();
this.TableData.Rows.Add(rowModelWithCurrentColumns);
for (int columnIndex = 0; columnIndex < this.TableData.Columns.Count; columnIndex++)
{
rowModelWithCurrentColumns[columnIndex] = columnValues[columnIndex];
}
}
// Force the binding target (DataGrid) to create new column templates
// by changing the binding source instance
private void OnDataTableColumnsChanged() => this.TableData = this.TableData.DefaultView.ToTable();
// Add columns dynamically via an ICommand
public ICommand AddColumnCommand => new RelayCommand(commandParameter =>
{
AddColumn<DateTime>($"Timestamp {this.TableData.Columns.Count}");
OnDataTableColumnsChanged();
});
public ICommand AddRowCommand => new RelayCommand(commandParameter => AddRow(2, "You", "you@mail.com", DateTime.Now));
private DataTable tableData;
public DataTable TableData
{
get => this.tableData;
set
{
this.tableData = value;
OnPropertyChanged();
}
}
/*** Implementation of INotifyPropertyChanged ***/
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = "")
=> this.PropertyChanged?.Invoke(new PropertyChangedEventArgs(propertyName));
}
备注:
或者,成员 AddColumn
和 AddRow
可以作为扩展方法实现。
您可以从任何呼叫者呼叫 AddColumn
和 AddRow
- 不仅来自 ICommand
。确保在所有列 added/removed.
之后调用 OnDataTableColumnsChanged
将列更改提升到视图
在视图中,行不需要创建任何视图模板,因此这些行数据更改立即可见。列的模板中定义了如何呈现行的单元格。
另一方面,查看列需要一个视图模板来告诉 DataGrid
如何呈现列及其单元格。调用 OnDataTableColumnsChanged
强制绑定目标 (DataGrid
) 为新列生成列模板。
如果 DataTable
的数据源实际上是一个数据库,则在每次查询后都会创建一个 DataTable
的新实例,这将使对 OnDataTableColumnsChanged
的调用变得多余。
MainWindow.xaml
<Window>
<Window.DataContext>
<!-- Don't forget to qualify the type with the defined XAML namespace (xmlns) -->
<MainViewModel />
</Window.DataContext>
<StackPanel>
<Button Content="Add Column" Command="{Binding AddColumnCommand}" />
<Button Content="Add Row" Command="{Binding AddRowCommand}" />
<DataGrid ItemsSource="{Binding TableData}" />
</StackPanel>
</Window>
正如其他人指出的那样,有很多方法可以通过 DataGrid、ListView 等以表单形式实现数据内容显示。通过采用 MVVM 模式,您的模型显然是数据源来自和存储到,例如某种类型的 SQL 数据库,Excel 通过 OleDbConnection 连接等
ViewModel 是实现所有你如何获取数据和如何保存数据的核心,而且还公开按钮单击操作、用户输入验证等。你的 ViewModel 成为“DataContext”视图的绑定源基线。您通过 getter/setter 制作属性 public 来公开实际数据,例如
public DataTable MyListOfCustomers {get; set;}
然后,在您的视图模型中的某处,您 运行 您的查询除了将记录加载到此数据 table 对象中外什么都不做。
视图显然是您向最终用户显示的内容。这是您 post 中简短且模棱两可的解释所不清楚的。在几乎任何应用程序中,您可能有许多数据网格代表许多不同事物的数据,但这些数据网格通常总是代表相同的内容。例如,客户屏幕将始终显示客户列表并允许搜索、排序、特定列等。库存屏幕将始终显示可用项目等。
因此,在开发应用程序时,与其依赖如何让它始终动态可用,不如自己定义每个可视化数据网格组件,这样它始终相同并基于已知来源.然后,只需将该用户控件添加到您需要的视图中即可。如果需要对该数据网格进行任何更改,只需更改一次,使用它的任何其他视图都会受到相同的影响。
说了这么多,而且绑定本身非常简单,您可能想要编辑您的 post,而不是通过有限的评论。 Post 关于您正在尝试做的事情的更明确的上下文。所呈现的实际数据类型。这不像是你在做一些你不能透露的全新和绝密的事情。但也许该系统将具有的工作类型的总体背景。它会基于会计做数字(甚至基于股票销售)、库存系统等吗?这个数据网格是否会更改其自身显示的内容,或者它会更像我描述的每个辅助视图甚至子区域单个视图显示不同的数据网格内容。
提供含糊不清的信息,即使来自另一个开发领域,在寻求帮助时也不会很好地照顾您。