【问题标题】:DataGridView Copy Paste differentiate between blank cells and skipped cells?DataGridView复制粘贴区分空白单元格和跳过的单元格?
【发布时间】:2020-07-15 21:29:46
【问题描述】:

我已经实现了一个 C# DataGridView 并构建了复制/粘贴功能,如其他帖子中所述:

        // Copy Code
        DataObject d = ws.GetClipboardContent();
        if (d != null)
        {
            Clipboard.SetDataObject(d);
        }

//

        // Paste Code
        string s = Clipboard.GetText();
        string sWithoutSlashR = s.Replace("\r", "");    // get rid of \r if it exists
        string[] lines = sWithoutSlashR.Split('\n');
        int row = ws.CurrentCell.RowIndex;
        Boolean allGood = true;
        foreach (string line in lines)
        {
            int col = ws.CurrentCell.ColumnIndex;
            string[] cells = line.Split('\t');
            int cellsSelected = cells.Length;
            for (int i = 0; i < cellsSelected; i++)
            {
                if (col < ws.ColumnCount && row < ws.RowCount)
                {
                    ws.Rows[row].Cells[col].Value = cells[i].ToString();
                }
                col++;
            }
            row++;
        }

如果复制源单元格中有空白且粘贴目标单元格不为空,则会出现问题。想象三个单元格被复制,内容为“T”、“”、“T”(即引号只是为了您的可读性 - 中间单元格是空白的)。如果用户选择所有三个单元格,则复制的是:“T\r\n\r\nT”。粘贴这个会用空白覆盖中间单元格,这很好。

但是,如果用户使用控制键只选择了第一个和第三个单元格,那么复制的内容是相同的:“T\r\n\r\nT”。由于用户没有选择空的中间单元格,所以不应该粘贴空的中间单元格,但是可以!

由于复制内容相同,如何区分选定的空单元格和跳过的单元格?

迈克

【问题讨论】:

  • 我不得不问你为什么要使用操作系统剪贴板?据我所知,OS COPY 将为两个单元格之间的每个未选定单元格添加“空”值。我想知道你为什么不使用“网格”SelectedCells 集合来复制和粘贴。 SelectedCells 集合将只包含“选定”的单元格,每个单元格都有一个行/列属性。是否有某些原因您必须为此使用 OS COPY?
  • 一旦用户完成复制操作并移动到新单元格进行粘贴操作,SelectedCells 不再代表复制源。其他帖子建议使用 OS 剪贴板,以便用户灵活地粘贴到 DataGridView 中的其他位置或粘贴到其他应用程序(如 Excel)。
  • 您的评论... “一旦用户完成复制操作并移动到新单元格进行粘贴操作,SelectedCells 不再代表复制源。” ... 创建一个global var LastSelectedCells... 然后,当用户单击 COPY 按钮时,只需将 LastSelectedCells 设置为网格当前选定单元格集合的新副本。那么当用户点击粘贴时……就可以使用LastSelectedCells集合了。
  • 我以为 DataGridViewCellCollection 只支持一维数组。 DataGridViewCellCollection 能否支持选定单元格的二维数组(即按行和列排列的单元格)?
  • 定义...DataGridViewSelectedCellCollection nonSortedSelectedCells;...在复制代码中:...nonSortedSelectedCells = ws.SelectedCells;。您需要记住,网格SelectedCells 集合考虑了“第一个”选择的哪个单元格(最后一个集合中的项目)和“最后一个”(集合中的第一个项目)。这意味着用户“选择”单元格的顺序将决定数组中的顺序。

标签: c# datagridview copy copy-paste paste


【解决方案1】:

感谢 JohnG 让我朝着正确的方向前进!这是我的解决方案:

全局变量:

DataGridViewSelectedCellCollection copySource;

我的 DataGridView 在下面的示例中被命名为 ws。在 CTRL+C 或上下文菜单访问的复制命令中,我填充了全局变量:

copySource = ws.SelectedCells;

粘贴命令如下:

        if (copySource == null) return;     // nothing to paste, so leave 
        if (copySource.Count == 0) return;  // nothing to paste, so leave

        // find top left Source cell
        int topSourceRow = copySource[0].RowIndex;
        int leftSourceCol = copySource[0].ColumnIndex;
        foreach(DataGridViewCell cell in copySource)
        {
            if (cell.RowIndex < topSourceRow) topSourceRow = cell.RowIndex;
            if (cell.ColumnIndex < leftSourceCol) leftSourceCol = cell.ColumnIndex;
        }

        // find top left destination cell
        int topDestRow = ws.SelectedCells[0].RowIndex;
        int leftDestCol = ws.SelectedCells[0].ColumnIndex;

        // paste cells
        Boolean allGood = true;
        foreach (DataGridViewCell cell in copySource)
        {
            // check if we have run off the edge of the grid
            // todo: expand if statement to add type checks if needed
            if (topDestRow + cell.RowIndex - topSourceRow >= ws.Rows.Count || leftDestCol + cell.ColumnIndex - leftSourceCol >= ws.Columns.Count)
            {
                // do not paste, set flag to show warning
                allGood = false;
            }
            else
            {
                // perform paste
                ws.Rows[topDestRow + cell.RowIndex - topSourceRow].Cells[leftDestCol + cell.ColumnIndex - leftSourceCol].Value = cell.Value.ToString();
            }
        }
        if (!allGood)
        {
            // todo: display warning to user that not all cells were pasted
        }

初始冒烟测试显示空白单元格被复制并正确覆盖目标单元格。复制不连续单元格的行为符合预期 - 复制源中间跳过的单元格不包含在粘贴中。

这种方法的假设是用户选择了目标单元格的左上角进行粘贴,我认为这是合理的。

如果粘贴超出右侧或底部边缘,则会粘贴可以粘贴的单元格。超出边缘的单元格将被跳过,并向用户显示警告消息。您的错误检查要求可能会有所不同。

在我的应用程序中,我将保留将源单元格复制到操作系统剪贴板的原始代码,以便用户灵活地从我的应用程序复制到 Excel 或其他目标。

【讨论】:

    【解决方案2】:

    经过长时间的讨论,我想澄清一些事情。我在下面的解决方案与 Mike Paisner 的回答非常相似,我对他的解决方案表示赞赏。干得好……给我点赞。

    关于使用操作系统的复制命令,它会根据您的需要而工作。在 Mike Paisner 的原始问题中,代码使用操作系统的复制命令来复制网格中的“选定”单元格。使用操作系统的复制命令时,粘贴会覆盖未选择“空”值的单元格。

    考虑到操作系统如何使用在DataGridView 中选择的单元格构建复制命令,这是正确的。使用原始帖子中发布的代码,它如上所述工作,用空值覆盖未选择的单元格。如果您想“忽略”空值而不覆盖这些单元格,那么简单检查原始代码即可解决此问题。像……

    if (col < ws.ColumnCount && row < ws.RowCount) {
      if (cells[i].ToString() != "") {   // <-- added check for empty (non-selected) cells
        ws.Rows[row].Cells[col].Value = cells[i].ToString();
      }
      else {
         // empty cell value
       }
    }
    

    这有效并且将停止覆盖未选中的单元格。但是,如前所述,如果用户选择了一个“空”单元格然后单击复制按钮,选择另一个单元格,然后单击粘贴按钮,则要粘贴到的单元格中的任何值都将保持不变。如果这是所需的行为,那么代码就完成了,但是,恕我直言,如果我选择了一个空单元格并复制粘贴它,那么它不应该仅仅因为它是一个空值而忽略粘贴的值。

    如果您想在选中空单元格时复制它们,那么使用操作系统的复制命令将不起作用。很难区分空的“选中”单元格和“未选中”单元格。请理解,我不想劝阻任何人使用操作系统的复制命令,在许多情况下这是最好的方法。但是,在 OP 的情况下,这将不起作用,因为他们希望能够粘贴“空”单元格。

    鉴于此,我的建议是使用DataGridViewSelectedCells 集合来管理粘贴过程。使用这个集合需要不同的方法和更多的工作。对于初学者,如 cmets 中所述,当用户选择要粘贴到的单元格时,SelectedCells 集合将使用我们要粘贴到的选定单元格重新开始。因此,删除我们需要的先前选择的单元格。我建议在单击复制按钮时使用全局变量来存储选定的单元格。然后当点击粘贴按钮时,我们可以使用保存先前选择的单元格的全局变量。

    The SelectedCells collection is a single dimension “dynamic” array and obviously grows/shrinks when cells are selected/deselected.从技术上讲,集合是一个“堆栈”或 FIFO(先进先出)……集合中的“第一个”单元格是选择的“最后一个”单元格,集合中的“最后一个”单元格是选择的“第一个”单元格.这显然使您能够知道选择单元格的顺序。这是设计使然并且有意义,但是对于这种情况,选择单元格的顺序无关紧要。与操作系统副本相同,我们只关心由选定单元格创建的矩形的“左上”单元格的位置。

    因为单元格的排序顺序与用户选择单元格的顺序相同,所以我们需要找到矩形选择的“左上角”单元格(浅绿色单元格 R1C1)。例子;使用下图,使用 Ctrl 键以 col2、col1、col4 和 col3 的顺序多选单​​元格进行选择。

    选定的单元格数组看起来像……

    Index   cellValue
      0       R3C3
      1       R1C4
      2       R3C1
      3       R5C2
    

    在这个例子中,我们想要找到集合中编号“最低”的列和行。我们可以遍历集合,或者,由于集合是可枚举的,我们可以简单地根据列索引对集合进行排序,然后获取第一项列索引。然后按行索引对其进行排序并获取第一项行索引。它可能看起来像……

    int colIndex = copyCollection.Cast<DataGridViewCell>().OrderBy(c => c.ColumnIndex).First().ColumnIndex;
    int rowIndex = copyCollection.Cast<DataGridViewCell>().OrderBy(c => c.RowIndex).First().RowIndex;
    

    这将为我们提供选择矩形(图片中的粉红色矩形)中左上角的单元格(浅绿色单元格)。在此示例中,第一列将是第 1 列,顶行将是第 1 行。假设用户单击“R5C0”单元格(深绿色)进行粘贴。

    现在我们想要获得需要添加到每个选定单元格的行和列索引的“神奇”数字。看图片,这种差异将是两个绿色单元格之间的差异。这可能看起来像……

    int colDif = ws.CurrentCell.ColumnIndex - colIndex
    int rowDif = ws.CurrentCell.RowIndex - rowIndex;
    

    那么colDif 将是 0 – 1 = -1 ...rowDif 将是 5 – 1 = 4。这些将是我们需要“添加”到所选单元格集合中的每个单元格的“神奇”数字映射到要粘贴到的正确单元格。

    示例:在选定单元格循环中,我们需要每个选定单元格的新列和行索引,在这种情况下(从上面)集合中的第一个单元格是“R3C3”,因此它的新列索引将是它的当前索引加上difCol 值。 ... 3 + (-1) = 2,与行相同... 3 + 4 = 7。这会将单元格“R3C3”粘贴到单元格“R7C2”中。一个简单的检查是查看“R1C1”并检查与“R3C3”的差异……右侧两列,向下两列。所以,如果我们从粘贴单元格“R5C0”开始,那么 5 + 2 = 7 和 0 + 2 = 2……我希望这是有道理的。

    左上角选定单元格矩形与粘贴单元格位置的差值可用于所有选定单元格,并且它应该正确映射而无需大量索引杂耍。

    最后,下面的代码。对于粘贴过程,会检查空/空选择并返回。然后我们得到“神奇的”差异值以添加到每个选定的单元格。循环遍历创建映射行/列索引的选定单元格。进行边界检查以忽略网格边界之外的单元格,并检查新行。如果存在,这将防止粘贴到新行中。如果所有条件都成功,则粘贴单元格。

    我为代码中的“选定”和“粘贴”单元格添加了单元格颜色,以直观地帮助测试这一点。我希望我做对了。

    DataGridViewSelectedCellCollection copyCollection;
    
    public Form1() {
      InitializeComponent();
    }
    
    private void Form1_Load(object sender, EventArgs e) {
      FillGrid();
    }
    
    private void btnCopy_Click(object sender, EventArgs e) {
      ClearCellColors();
      copyCollection = ws.SelectedCells;
      foreach (DataGridViewCell cell in copyCollection) {
        cell.Style.BackColor = Color.LightCoral;
      }
    }
    
    private void btn_Paste_Click(object sender, EventArgs e) {
      if (copyCollection == null || copyCollection.Count == 0) {
        Debug.WriteLine("Copy button was not clicked - empty collection");
        return;
      }
      // get the difference between the top-left columns and rows
      int colDif = ws.CurrentCell.ColumnIndex - copyCollection.Cast<DataGridViewCell>().OrderBy(c => c.ColumnIndex).First().ColumnIndex;
      int rowDif = ws.CurrentCell.RowIndex - copyCollection.Cast<DataGridViewCell>().OrderBy(c => c.RowIndex).First().RowIndex;
      int rowIndex;
      int colIndex;
      foreach (DataGridViewCell cell in copyCollection) {
        rowIndex = cell.RowIndex + rowDif;
        colIndex = cell.ColumnIndex + colDif;
        if (rowIndex >= 0 && colIndex >= 0 &&
            rowIndex < ws.RowCount && colIndex < ws.ColumnCount &&
            !ws.Rows[rowIndex].IsNewRow) {
          ws.Rows[rowIndex].Cells[colIndex].Value = cell.Value;
          ws.Rows[rowIndex].Cells[colIndex].Style.BackColor = Color.LightGreen;
        }
        else {
          Debug.WriteLine("Out of bounds: RowIndex:" + rowIndex + " ColIndex: " + colIndex);
        }
      }      
    }
    
    private void FillGrid() {
      DataGridViewTextBoxColumn newCol;
      for (int i = 0; i < 5; i++) {
        newCol = new DataGridViewTextBoxColumn();
        newCol.Name = "Col " + i;
        ws.Columns.Add(newCol);
      }
      for (int row = 0; row < 20; row++) {
        int curRowIndex = ws.Rows.Add();
        for (int col = 0; col < ws.Columns.Count; col++) {
          ws.Rows[curRowIndex].Cells[col].Value = "R" + curRowIndex + "C" + col;
        }
      }
    }
    
    private void ClearCellColors() {
      foreach (DataGridViewRow row in ws.Rows) {
        foreach (DataGridViewCell cell in row.Cells) {
          cell.Style.BackColor = Color.White;
        }
      }
    }
    

    我希望这是有道理的并有所帮助。 ?

    【讨论】:

    • 写得真好!只是好奇,因为我们只在复制源中寻找一个最小值,OrderBy 函数是比 foreach 循环更有效还是本质上只是一种替代语法?
    • @Mike Paisner ...我不确定您所说的“只在复制源中寻找一个最小值”是什么意思?我们需要两个最小值……最上面的行和最左边的列,除非我遗漏了什么。在这种情况下,OrderBy 只是一种替代语法,但是,它是一种更有效的方法,具体取决于集合。然而,在这种情况下,它是无关紧要的。