WPF数据网格粘贴
Posted
技术标签:
【中文标题】WPF数据网格粘贴【英文标题】:WPF datagrid pasting 【发布时间】:2011-05-06 08:09:15 【问题描述】:我无法从 csv 粘贴到 wpf 数据网格中 - 我已按照此处的建议进行操作
Link
并且代码执行没有问题 - 但是,似乎所有新行都已创建,但只有第一行填充了数据。数据似乎不断被覆盖,因此剪贴板数据中的最后一项填充在第一行中,而所有其他行都是空白的。我知道这一定是索引问题或其他问题,但我无法追踪。
此外,当我查看网格的可绑定集合中的对象时,它们都没有任何数据。列的 OnPastingCellClipboardContent 中是否有某些内容出错(可能是数据转换)?
任何想法(见下面的代码)
protected virtual void OnExecutedPaste(object sender, ExecutedRoutedEventArgs args)
// parse the clipboard data
List<string[]> rowData = ClipboardHelper.ParseClipboardData();
bool hasAddedNewRow = false;
// call OnPastingCellClipboardContent for each cell
int minRowIndex = Math.Max(Items.IndexOf(CurrentItem), 0);
int maxRowIndex = Items.Count - 1;
int minColumnDisplayIndex = (SelectionUnit != DataGridSelectionUnit.FullRow) ? Columns.IndexOf(CurrentColumn) : 0;
int maxColumnDisplayIndex = Columns.Count - 1;
int rowDataIndex = 0;
for (int i = minRowIndex; i <= maxRowIndex && rowDataIndex < rowData.Count; i++, rowDataIndex++)
if (CanUserAddRows && i == maxRowIndex)
// add a new row to be pasted to
ICollectionView cv = CollectionViewSource.GetDefaultView(Items);
IEditableCollectionView iecv = cv as IEditableCollectionView;
if (iecv != null)
hasAddedNewRow = true;
iecv.AddNew();
if (rowDataIndex + 1 < rowData.Count)
// still has more items to paste, update the maxRowIndex
maxRowIndex = Items.Count - 1;
else if (i == maxRowIndex)
continue;
int columnDataIndex = 0;
for (int j = minColumnDisplayIndex; j < maxColumnDisplayIndex && columnDataIndex < rowData[rowDataIndex].Length; j++, columnDataIndex++)
DataGridColumn column = ColumnFromDisplayIndex(j);
column.OnPastingCellClipboardContent(Items[i], rowData[rowDataIndex][columnDataIndex]);
【问题讨论】:
文森特的帖子已移至:docs.microsoft.com/en-us/archive/blogs/vinsibal/… 到处都提到,但我花了很长时间才找到它。微软在他们的博客上没有很好的索引。我在这个页面上从一个叫 Bob 的人那里找到它:social.msdn.microsoft.com/Forums/aspnet/en-US/… 谢谢 Bob!也许微软应该更新这篇文章? 【参考方案1】:谢谢,
Vincent 的帖子非常好,但 KlausG 在其中添加了一些更正,这些更正也应考虑与 FrameWork 4.0 一起使用。很重要
Vincent 的原始网站帖子:http://blogs.msdn.com/b/vinsibal/archive/2008/09/25/pasting-content-to-new-rows-on-the-wpf-datagrid.aspx
注意:如果您无法添加行,请确保您的项目有一个默认构造函数。
2018-03-06 更新(支持 DataGrid 列由用户重新排序)
2020 年 12 月 3 日更新(根据 Cesar Morigaki 解决方案更正和来自 Antonio Rodríguez 的消息)感谢两者!!!
用法
<myControl:CustomDataGrid ...
自定义数据网格
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Reflection;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
namespace HQ.Wpf.Util.MyControl
public class CustomDataGrid : DataGrid
public event ExecutedRoutedEventHandler ExecutePasteEvent;
public event CanExecuteRoutedEventHandler CanExecutePasteEvent;
// ******************************************************************
static CustomDataGrid()
CommandManager.RegisterClassCommandBinding(
typeof(CustomDataGrid),
new CommandBinding(ApplicationCommands.Paste,
new ExecutedRoutedEventHandler(OnExecutedPasteInternal),
new CanExecuteRoutedEventHandler(OnCanExecutePasteInternal)));
// ******************************************************************
#region Clipboard Paste
// ******************************************************************
private static void OnCanExecutePasteInternal(object target, CanExecuteRoutedEventArgs args)
((CustomDataGrid)target).OnCanExecutePaste(target, args);
// ******************************************************************
/// <summary>
/// This virtual method is called when ApplicationCommands.Paste command query its state.
/// </summary>
/// <param name="args"></param>
protected virtual void OnCanExecutePaste(object target, CanExecuteRoutedEventArgs args)
if (CanExecutePasteEvent != null)
CanExecutePasteEvent(target, args);
if (args.Handled)
return;
args.CanExecute = CurrentCell != null;
args.Handled = true;
// ******************************************************************
private static void OnExecutedPasteInternal(object target, ExecutedRoutedEventArgs args)
((CustomDataGrid)target).OnExecutedPaste(target, args);
// ******************************************************************
/// <summary>
/// This virtual method is called when ApplicationCommands.Paste command is executed.
/// </summary>
/// <param name="target"></param>
/// <param name="args"></param>
protected virtual void OnExecutedPaste(object target, ExecutedRoutedEventArgs args)
if (ExecutePasteEvent != null)
ExecutePasteEvent(target, args);
if (args.Handled)
return;
// parse the clipboard data [row][column]
List<string[]> clipboardData = HQ.Util.General.Clipboard.ClipboardHelper2.ParseClipboardData();
bool hasAddedNewRow = false;
Debug.Print(">>> DataGrid Paste: >>>");
#if DEBUG
StringBuilder sb = new StringBuilder();
#endif
int minRowIndex = Items.IndexOf(CurrentItem);
int maxRowIndex = Items.Count - 1;
int startIndexOfDisplayCol = (SelectionUnit != DataGridSelectionUnit.FullRow) ? CurrentColumn.DisplayIndex : 0;
int clipboardRowIndex = 0;
for (int i = minRowIndex; i <= maxRowIndex && clipboardRowIndex < clipboardData.Count; i++, clipboardRowIndex++)
if (i < this.Items.Count)
CurrentItem = Items[i];
BeginEditCommand.Execute(null, this);
int clipboardColumnIndex = 0;
for (int j = startIndexOfDisplayCol; clipboardColumnIndex < clipboardData[clipboardRowIndex].Length; j++, clipboardColumnIndex++)
// DataGridColumn column = ColumnFromDisplayIndex(j);
DataGridColumn column = null;
foreach (DataGridColumn columnIter in this.Columns)
if (columnIter.DisplayIndex == j)
column = columnIter;
break;
column?.OnPastingCellClipboardContent(Items[i], clipboardData[clipboardRowIndex][clipboardColumnIndex]);
#if DEBUG
sb.AppendFormat("0,-10", clipboardData[clipboardRowIndex][clipboardColumnIndex]);
sb.Append(" - ");
#endif
CommitEditCommand.Execute(this, this);
if (i == maxRowIndex)
maxRowIndex++;
hasAddedNewRow = true;
Debug.Print(sb.ToString());
#if DEBUG
sb.Clear();
#endif
// update selection
if (hasAddedNewRow)
UnselectAll();
UnselectAllCells();
CurrentItem = Items[minRowIndex];
if (SelectionUnit == DataGridSelectionUnit.FullRow)
SelectedItem = Items[minRowIndex];
else if (SelectionUnit == DataGridSelectionUnit.CellOrRowHeader ||
SelectionUnit == DataGridSelectionUnit.Cell)
SelectedCells.Add(new DataGridCellInfo(Items[minRowIndex], Columns[startIndexOfDisplayCol]));
// ******************************************************************
/// <summary>
/// Whether the end-user can add new rows to the ItemsSource.
/// </summary>
public bool CanUserPasteToNewRows
get return (bool)GetValue(CanUserPasteToNewRowsProperty);
set SetValue(CanUserPasteToNewRowsProperty, value);
// ******************************************************************
/// <summary>
/// DependencyProperty for CanUserAddRows.
/// </summary>
public static readonly DependencyProperty CanUserPasteToNewRowsProperty =
DependencyProperty.Register("CanUserPasteToNewRows",
typeof(bool), typeof(CustomDataGrid),
new FrameworkPropertyMetadata(true, null, null));
// ******************************************************************
#endregion Clipboard Paste
private void SetGridToSupportManyEditEitherWhenValidationErrorExists()
this.Items.CurrentChanged += Items_CurrentChanged;
//Type DatagridType = this.GetType().BaseType;
//PropertyInfo HasCellValidationProperty = DatagridType.GetProperty("HasCellValidationError", BindingFlags.NonPublic | BindingFlags.Instance);
//HasCellValidationProperty.
void Items_CurrentChanged(object sender, EventArgs e)
//this.Items[0].
//throw new NotImplementedException();
// ******************************************************************
private void SetGridWritable()
Type DatagridType = this.GetType().BaseType;
PropertyInfo HasCellValidationProperty = DatagridType.GetProperty("HasCellValidationError", BindingFlags.NonPublic | BindingFlags.Instance);
if (HasCellValidationProperty != null)
HasCellValidationProperty.SetValue(this, false, null);
// ******************************************************************
public void SetGridWritableEx()
BindingFlags bindingFlags = BindingFlags.FlattenHierarchy | BindingFlags.NonPublic | BindingFlags.Instance;
PropertyInfo cellErrorInfo = this.GetType().BaseType.GetProperty("HasCellValidationError", bindingFlags);
PropertyInfo rowErrorInfo = this.GetType().BaseType.GetProperty("HasRowValidationError", bindingFlags);
cellErrorInfo.SetValue(this, false, null);
rowErrorInfo.SetValue(this, false, null);
// ******************************************************************
还有:剪贴板助手:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Diagnostics;
using System.Windows;
using System.IO;
namespace HQ.Util.General.Clipboard
public static class ClipboardHelper
public delegate string[] ParseFormat(string value);
public static List<string[]> ParseClipboardData()
List<string[]> clipboardData = null;
object clipboardRawData = null;
ParseFormat parseFormat = null;
// get the data and set the parsing method based on the format
// currently works with CSV and Text DataFormats
IDataObject dataObj = System.Windows.Clipboard.GetDataObject();
if ((clipboardRawData = dataObj.GetData(DataFormats.CommaSeparatedValue)) != null)
parseFormat = ParseCsvFormat;
else if((clipboardRawData = dataObj.GetData(DataFormats.Text)) != null)
parseFormat = ParseTextFormat;
if (parseFormat != null)
string rawDataStr = clipboardRawData as string;
if (rawDataStr == null && clipboardRawData is MemoryStream)
// cannot convert to a string so try a MemoryStream
MemoryStream ms = clipboardRawData as MemoryStream;
StreamReader sr = new StreamReader(ms);
rawDataStr = sr.ReadToEnd();
Debug.Assert(rawDataStr != null, string.Format("clipboardRawData: 0, could not be converted to a string or memorystream.", clipboardRawData));
string[] rows = rawDataStr.Split(new string[] "\r\n" , StringSplitOptions.RemoveEmptyEntries);
if (rows != null && rows.Length > 0)
clipboardData = new List<string[]>();
foreach (string row in rows)
clipboardData.Add(parseFormat(row));
else
Debug.WriteLine("unable to parse row data. possibly null or contains zero rows.");
return clipboardData;
public static string[] ParseCsvFormat(string value)
return ParseCsvOrTextFormat(value, true);
public static string[] ParseTextFormat(string value)
return ParseCsvOrTextFormat(value, false);
private static string[] ParseCsvOrTextFormat(string value, bool isCSV)
List<string> outputList = new List<string>();
char separator = isCSV ? ',' : '\t';
int startIndex = 0;
int endIndex = 0;
for (int i = 0; i < value.Length; i++)
char ch = value[i];
if (ch == separator)
outputList.Add(value.Substring(startIndex, endIndex - startIndex));
startIndex = endIndex + 1;
endIndex = startIndex;
else if (ch == '\"' && isCSV)
// skip until the ending quotes
i++;
if (i >= value.Length)
throw new FormatException(string.Format("value: 0 had a format exception", value));
char tempCh = value[i];
while (tempCh != '\"' && i < value.Length)
i++;
endIndex = i;
else if (i + 1 == value.Length)
// add the last value
outputList.Add(value.Substring(startIndex));
break;
else
endIndex++;
return outputList.ToArray();
CsvHelper.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using MoreLinq; // http://***.com/questions/15265588/how-to-find-item-with-max-value-using-linq
namespace HQ.Util.General.CSV
public class CsvHelper
public static Dictionary<LineSeparator, Func<string, string[]>> DictionaryOfLineSeparatorAndItsFunc = new Dictionary<LineSeparator, Func<string, string[]>>();
static CsvHelper()
DictionaryOfLineSeparatorAndItsFunc[LineSeparator.Unknown] = ParseLineNotSeparated;
DictionaryOfLineSeparatorAndItsFunc[LineSeparator.Tab] = ParseLineTabSeparated;
DictionaryOfLineSeparatorAndItsFunc[LineSeparator.Semicolon] = ParseLineSemicolonSeparated;
DictionaryOfLineSeparatorAndItsFunc[LineSeparator.Comma] = ParseLineCommaSeparated;
// ******************************************************************
public enum LineSeparator
Unknown = 0,
Tab,
Semicolon,
Comma
// ******************************************************************
public static LineSeparator GuessCsvSeparator(string oneLine)
List<Tuple<LineSeparator, int>> listOfLineSeparatorAndThereFirstLineSeparatedValueCount = new List<Tuple<LineSeparator, int>>();
listOfLineSeparatorAndThereFirstLineSeparatedValueCount.Add(new Tuple<LineSeparator, int>(LineSeparator.Tab, CsvHelper.ParseLineTabSeparated(oneLine).Count()));
listOfLineSeparatorAndThereFirstLineSeparatedValueCount.Add(new Tuple<LineSeparator, int>(LineSeparator.Semicolon, CsvHelper.ParseLineSemicolonSeparated(oneLine).Count()));
listOfLineSeparatorAndThereFirstLineSeparatedValueCount.Add(new Tuple<LineSeparator, int>(LineSeparator.Comma, CsvHelper.ParseLineCommaSeparated(oneLine).Count()));
Tuple<LineSeparator, int> bestBet = listOfLineSeparatorAndThereFirstLineSeparatedValueCount.MaxBy((n)=>n.Item2);
if (bestBet != null && bestBet.Item2 > 1)
return bestBet.Item1;
return LineSeparator.Unknown;
// ******************************************************************
public static string[] ParseLineCommaSeparated(string line)
// CSV line parsing : From "jgr4" in http://www.kimgentes.com/worshiptech-web-tools-page/2008/10/14/regex-pattern-for-parsing-csv-files-with-embedded-commas-dou.html
var matches = Regex.Matches(line, @"\s?((?<x>(?=[,]+))|""(?<x>([^""]|"""")+)""|""(?<x>)""|(?<x>[^,]+)),?",
RegexOptions.ExplicitCapture);
string[] values = (from Match m in matches
select m.Groups["x"].Value.Trim().Replace("\"\"", "\"")).ToArray();
return values;
// ******************************************************************
public static string[] ParseLineTabSeparated(string line)
//var matchesTab = Regex.Matches(line, @"\s?((?<x>(?=[\t]+))|""(?<x>([^""]|"""")+)""|""(?<x>)""|(?<x>[^\t]+))\t?",
// RegexOptions.ExplicitCapture);
// Correction according to Cesar Morigaki from SO question: https://***.com/questions/4118617/wpf-datagrid-pasting/5436437?noredirect=1#comment115043404_5436437
var matchesTab = Regex.Matches(line, @"[\r\n\f\v ]*?((?<x>(?=[\t]+))|""(?<x>([^""]|"""")+)""|""(?<x>)""|(?<x>[^\t]+))\t?",
RegexOptions.ExplicitCapture);
string[] values = (from Match m in matchesTab
select m.Groups["x"].Value.Trim().Replace("\"\"", "\"")).ToArray();
return values;
// ******************************************************************
public static string[] ParseLineSemicolonSeparated(string line)
// CSV line parsing : From "jgr4" in http://www.kimgentes.com/worshiptech-web-tools-page/2008/10/14/regex-pattern-for-parsing-csv-files-with-embedded-commas-dou.html
var matches = Regex.Matches(line, @"\s?((?<x>(?=[;]+))|""(?<x>([^""]|"""")+)""|""(?<x>)""|(?<x>[^;]+));?",
RegexOptions.ExplicitCapture);
string[] values = (from Match m in matches
select m.Groups["x"].Value.Trim().Replace("\"\"", "\"")).ToArray();
return values;
// ******************************************************************
public static string[] ParseLineNotSeparated(string line)
string [] lineValues = new string[1];
lineValues[0] = line;
return lineValues;
// ******************************************************************
public static List<string[]> ParseText(string text)
string[] lines = text.Split(new string[] "\r\n" , StringSplitOptions.None);
return ParseString(lines);
// ******************************************************************
public static List<string[]> ParseString(string[] lines)
List<string[]> result = new List<string[]>();
LineSeparator lineSeparator = LineSeparator.Unknown;
if (lines.Any())
lineSeparator = GuessCsvSeparator(lines[0]);
Func<string, string[]> funcParse = DictionaryOfLineSeparatorAndItsFunc[lineSeparator];
foreach (string line in lines)
if (string.IsNullOrWhiteSpace(line))
continue;
result.Add(funcParse(line));
return result;
// ******************************************************************
ClipboardHelper2.cs
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Text.RegularExpressions;
using System.Windows;
using System.IO;
using System.Linq;
using System.Windows.Forms.VisualStyles;
using HQ.Util.General.CSV;
namespace HQ.Util.General.Clipboard
// Uses Clipboard in WPF (PresentationCore.dll in v4 of the framework)
public static class ClipboardHelper2
public delegate string[] ParseFormat(string value);
public static List<string[]> ParseClipboardData()
List<string[]> clipboardData = new List<string[]>();
// get the data and set the parsing method based on the format
// currently works with CSV and Text DataFormats
IDataObject dataObj = System.Windows.Clipboard.GetDataObject();
if (dataObj != null)
string[] formats = dataObj.GetFormats();
if (formats.Contains(DataFormats.CommaSeparatedValue))
string clipboardString = (string)dataObj.GetData(DataFormats.CommaSeparatedValue);
// EO: Subject to error when a CRLF is included as part of the data but it work for the moment and I will let it like it is
// WARNING ! Subject to errors
string[] lines = clipboardString.Split(new string[] "\r\n" , StringSplitOptions.None);
string[] lineValues;
foreach (string line in lines)
lineValues = CsvHelper.ParseLineCommaSeparated(line);
if (lineValues != null)
clipboardData.Add(lineValues);
else if (formats.Contains(DataFormats.Text))
string clipboardString = (string)dataObj.GetData(DataFormats.Text);
clipboardData = CsvHelper.ParseText(clipboardString);
return clipboardData;
【讨论】:
确实不错。大部分工作,但有时我在 CommitEditCommand.Execute(this,this);并且崩溃消息是“输入字符串的格式不正确。”。当我使用组合框并且没有选择任何元素时,我会得到这个。虽然我使用了 MVVM 模式,但它会在粘贴时期待一些东西。但是尝试一下会解决我的问题。 我添加了我当前的代码。我想我遇到了像你这样的问题,因为有很大的变化。我希望这个应该可以工作。请注意,CSV 格式有时会有不同的风格(分隔符可能会更改:“、”、“;”、制表符等)。实际上提供的新代码在我们的生产代码中使用并且似乎可以正常工作。祝你好运! 对我来说效果很好。我建议检查@Cesar Morigaki 解决方案中是否有 excel 中的空白单元格。【参考方案2】:对于那些感兴趣的人 - 列尝试更新可绑定对象的值似乎确实出了问题 - 可能的数据类型转换,所以我自己实现了这个,它现在就像一个魅力。
protected virtual void OnExecutedPaste(object sender, ExecutedRoutedEventArgs args)
// parse the clipboard data
List<string[]> rowData = ClipboardHelper.ParseClipboardData();
bool hasAddedNewRow = false;
// call OnPastingCellClipboardContent for each cell
int minRowIndex = Math.Max(Items.IndexOf(CurrentItem), 0);
int maxRowIndex = Items.Count - 1;
int minColumnDisplayIndex = (SelectionUnit != DataGridSelectionUnit.FullRow) ? Columns.IndexOf(CurrentColumn) : 0;
int maxColumnDisplayIndex = Columns.Count - 1;
int rowDataIndex = 0;
for (int i = minRowIndex; i <= maxRowIndex && rowDataIndex < rowData.Count; i++, rowDataIndex++)
if (CanUserAddRows && i == maxRowIndex)
// add a new row to be pasted to
ICollectionView cv = CollectionViewSource.GetDefaultView(Items);
IEditableCollectionView iecv = cv as IEditableCollectionView;
if (iecv != null)
hasAddedNewRow = true;
iecv.AddNew();
if (rowDataIndex + 1 < rowData.Count)
// still has more items to paste, update the maxRowIndex
maxRowIndex = Items.Count - 1;
else if (i == maxRowIndex)
continue;
int columnDataIndex = 0;
for (int j = minColumnDisplayIndex; j < maxColumnDisplayIndex && columnDataIndex < rowData[rowDataIndex].Length; j++, columnDataIndex++)
DataGridColumn column = ColumnFromDisplayIndex(j);
string propertyName = ((column as DataGridBoundColumn).Binding as Binding).Path.Path;
object item = Items[i];
object value = rowData[rowDataIndex][columnDataIndex];
PropertyInfo pi = item.GetType().GetProperty(propertyName);
if (pi != null)
object convertedValue = Convert.ChangeType(value, pi.PropertyType);
item.GetType().GetProperty(propertyName).SetValue(item, convertedValue, null);
//column.OnPastingCellClipboardContent(item, rowData[rowDataIndex][columnDataIndex]);
【讨论】:
【参考方案3】:来自@eric-ouellet 答案的 CsvHelper.cs 中的小修正。使用任何空白单元格从 Excel 复制时,数据映射不会正确应用。我修改了 ParseLineTabSeparated 方法的正则表达式。
public static string[] ParseLineTabSeparated(string line)
var matchesTab = Regex.Matches(line, @"[\r\n\f\v ]*?((?<x>(?=[\t]+))|""(?<x>([^""]|"""")+)""|""(?<x>)""|(?<x>[^\t]+))\t?",
RegexOptions.ExplicitCapture);
string[] values = (from Match m in matchesTab
select m.Groups["x"].Value.Trim().Replace("\"\"", "\"")).ToArray();
return values;
【讨论】:
【参考方案4】:如果我是对的,您还应该对变量 j 进行范围检查:
for (int j = minColumnDisplayIndex; j <= maxColumnDisplayIndex && columnDataIndex < rowData[rowDataIndex].Length; j++, columnDataIndex++)
if (j == maxColumnDisplayIndex)
continue;
DataGridColumn column = ColumnFromDisplayIndex(j);
column.OnPastingCellClipboardContent(Items[i], rowData[rowDataIndex][columnDataIndex]);
否则,在粘贴具有更多您的网格实际可以容纳的列的内容时,您会收到 ArgumentOutOfRangeException。
【讨论】:
【参考方案5】:https://www.codeproject.com/Articles/1172338/Easy-WPF-Copy-Paste-Excel
上面的链接通过 Nuget 包提供了“粘贴到 DataGrid”功能。
缺少布尔类型支持。我必须下载源代码并修改 convertes.cs。
case "System.Boolean": resultado = bool.Parse(valor); break;
if (tipoDelNulable.Contains("System.Boolean")) resultado = string.IsNullOrEmpty(valor) ? null : (bool?)bool.Parse(valor);
【讨论】:
【参考方案6】:我有一个类似的问题,我想将从 Excel(在剪贴板中)复制的列粘贴到 WPF 中的数据网格中。我的方法是构建一个数据表并将数据网格数据上下文设置为这个数据表。
string s = Clipboard.GetText();
string[] lines = s.Split('\r');
DataTable dt = new DataTable();
string[] fields;
int row = 0;
int col = 0;
foreach (string item in lines)
dt.Rows.Add();
fields = item.Split('\t');
foreach (string f in fields)
dt.Rows[row][col] = f;
col++;
row++;
col = 0;
【讨论】:
以上是关于WPF数据网格粘贴的主要内容,如果未能解决你的问题,请参考以下文章