C# DataGridView: combine multiple rows to one but

2019-01-29 05:47发布

问题:

First of all: I am going to provide an answer to this question myself, as soon as I'm done.
But you may help me on my way to it, I will appreciate all of your advices.

I have a DataGridView with different rows that belong together. My problem is to find a suitable way for displaying and working with these connected rows.

My first idea was keeping each row individual, but that has some disadvantages:

  • how to clearly show that rows belong together?
    I could add another column whose cells show the same number for connected rows, but that's not easily visible and requires another column.
    My solution here would have been that all connected rows have the same background colour, and the colour changes e.g. between white and light grey for each set of connected rows.

  • how to work with connected rows? As soon as I select one row of a set, I would have to analyse this row by extracting information (saved in a cell's tag or in a hidden cell) which rows belong together and select them as well. Even bigger work for moving rows up/down in a DataGridView: I would have to analyse the neighbouring row sets as well to see how far I have to move.

Therefore I decided to create a DataGridViewMultiRow.
I will post the full code of that class here as an answer when finished.

It will inherit from DataGridViewRow ("DGVR") and store a list of single DGVR or of other multi-rows and display them as one by drawing the cells of the row by own code. However, I still need to find out which events to use for that purpose. MSDN suggests to use DataGridView.RowPrePaint, but I rather want to use an event that is bound to the DGVR itself. Maybe I will analyse the source code of DataGridViewRow.Paint() and write my own method...

The single rows will be made invisible when being added to a multi-row (they could be switched to visible again by abusing the concept, but there's a lot in .net itself that is not protected against abuse; maybe I don't even switch to invisible, so it's in the user's responsibility).
Recursion in the multi-rows will simply be avoided by forcing each DGVR to be part of the same DGV as the multi-row, and because each row can be added to only one DGV and only once, I don't have to check for recursion any more.

Now I am struggling how to implement the internal list of rows. I was thinking about using the .net DataGridViewRowCollection, but I found that it's operation is tightly bound to the DataGridView itself: a DGV can have only one DGVRColl, but each DGVRColl refers to a DGV. So there would be half-connected DGVRColl in each of my DGVMultiRow.
I was going to ask whether this will this cause issues, but I already found that I must provide a DGV when instantiating the DGVRColl, which I don't have at that moment when the DGVMultiRow ctor is called. Furthermore, when using a DGVRColl and providing a public get property to it, I can only hook to the 'CollectionChanged' event and have no control over the individual operations like Add() and Remove(). So I will use a simple private list.

Continuation #1
I got the main work done, it looks pretty good already:

I still need to get details fixed, like placing the text correctly when the scrollbar is moved and other small things.

I decided not to override the DataGridViewRow.Paint() because that one has too many internal connections. So I first played with using the CellPainting event of the DGV, which was a good start. But I needed to have infos of all cells of the row at the same time, so I went forward to overriding the DataGridView.RowPrePaint() as suggested by MSDN, see link above. This works really well.

To be continued.

回答1:

After various drawbacks, I have finally created a solution.
It is written in C++/CLI, so most of you will have to adapt it for usage in C#.
This solution contains some user functions that are not part of the solution, but whose purpose should be easy to guess by their names.

Here's a preview:

#pragma once

using namespace System;
using namespace System::Collections::Generic;
using namespace System::Drawing;
using namespace System::Windows::Forms;
using namespace System::ComponentModel;

public ref class CDataGridViewMultiRow : DataGridViewRow
{
public:
  //----------------------------------------------------------------------------
  // constructor
  //----------------------------------------------------------------------------
  CDataGridViewMultiRow ();
  CDataGridViewMultiRow (bool i_bHideRows);
  CDataGridViewMultiRow (bool i_bHideRows, ::DataGridView^ i_dgv);

  //----------------------------------------------------------------------------
  // Clone
  //----------------------------------------------------------------------------
  virtual Object^ Clone () override;

  //----------------------------------------------------------------------------
  // Clear
  //----------------------------------------------------------------------------
  void Clear ();

  //----------------------------------------------------------------------------
  // Add, Insert
  //----------------------------------------------------------------------------
  bool Add (DataGridViewRow^ i_dgvr);
  bool Insert (int i_ixRow, DataGridViewRow^ i_dgvr);

  //----------------------------------------------------------------------------
  // Remove
  //----------------------------------------------------------------------------
  bool Remove (int i_ixRow);
  bool Remove (DataGridViewRow^ i_dgvr);

  //----------------------------------------------------------------------------
  // Update
  //----------------------------------------------------------------------------
  void Update ();

  //----------------------------------------------------------------------------
  // PaintRow
  //
  // description: manually paints the row.
  //
  // !!! IMPORTANT NOTICE: !!!
  // This method must be attached to the DataGridView's RowPrePaint event.
  //----------------------------------------------------------------------------
  static void PaintRow (Object^ sender, DataGridViewRowPrePaintEventArgs^ e);

  //----------------------------------------------------------------------------
  // properties
  //----------------------------------------------------------------------------
  property DataGridViewRow^   Rows[int] { DataGridViewRow^  get (int i_ixRow);
                                          void              set (int i_ixRow, DataGridViewRow^ i_dgvr); }
  property int                RowCount  { int   get() { return m_listdgvr->Count;} }
  property bool               HideRows  { bool  get() { return m_bHideRows;}
                                          void  set(bool i_bHideRows); }

public:

protected:
  List<DataGridViewRow^>^     m_listdgvr;
  bool                        m_bHideRows;

private:

protected:
  virtual void OnDataGridViewChanged () override;

private:
  void CommonConstructor (bool            i_bHideRows,
                          ::DataGridView^ i_dgv);
};

#include "CDataGridViewMultiRow.h"

using namespace Schmoll_SwCore;

//----------------------------------------------------------------------------
// constructor
//----------------------------------------------------------------------------
CDataGridViewMultiRow::CDataGridViewMultiRow () : DataGridViewRow ()
{
  CommonConstructor (false, nullptr);
}

//----------------------------------------------------------------------------
CDataGridViewMultiRow::CDataGridViewMultiRow (bool i_bHideRows) : DataGridViewRow ()
{
  CommonConstructor (i_bHideRows, nullptr);
}

//----------------------------------------------------------------------------
CDataGridViewMultiRow::CDataGridViewMultiRow (bool i_bHideRows, ::DataGridView^ i_dgv) : DataGridViewRow ()
{
  CommonConstructor (i_bHideRows, i_dgv);
}

//----------------------------------------------------------------------------
// property: Rows
//----------------------------------------------------------------------------
DataGridViewRow^ CDataGridViewMultiRow::Rows::get (int i_ixRow)
{
  if (i_ixRow < 0 || i_ixRow >= m_listdgvr->Count)
    return nullptr;

  return m_listdgvr[i_ixRow];
}

//----------------------------------------------------------------------------
void CDataGridViewMultiRow::Rows::set (int i_ixRow, DataGridViewRow^ i_dgvr)
{
  if (!i_dgvr)
    return;
  if (i_ixRow < 0 || i_ixRow >= m_listdgvr->Count)
    return;

  int ixDgvr = -1;
  DataGridViewRow^ dgvr = m_listdgvr[i_ixRow];
  if (dgvr->DataGridView
  &&  dgvr->DataGridView == this->DataGridView)
  {
    ixDgvr = dgvr->Index;
    dgvr->DataGridView->Rows->Remove (dgvr);
  }

  m_listdgvr[i_ixRow] = i_dgvr;
  if (this->DataGridView)
  {
    if (ixDgvr < 0)
      ixDgvr = this->DataGridView->Rows->IndexOf (this) + 1 + i_ixRow;
    this->DataGridView->Rows->Insert (ixDgvr, i_dgvr);
    i_dgvr->Visible = !m_bHideRows;
  }

  Update();
}

//----------------------------------------------------------------------------
// property: HideRows
//----------------------------------------------------------------------------
void CDataGridViewMultiRow::HideRows::set (bool i_bHideRows)
{
  m_bHideRows = i_bHideRows;
  for (int ixRow = 0; ixRow < m_listdgvr->Count; ixRow++)
    m_listdgvr[ixRow]->Visible = !m_bHideRows;
}

//----------------------------------------------------------------------------
// Clone
//----------------------------------------------------------------------------
Object^ CDataGridViewMultiRow::Clone ()
{
  CDataGridViewMultiRow^ dgvr = (CDataGridViewMultiRow^)DataGridViewRow::Clone();
  if (dgvr)
  {
    dgvr->m_bHideRows = this->m_bHideRows;
    dgvr->m_listdgvr->Clear();
    dgvr->m_listdgvr->AddRange (this->m_listdgvr);
  }
  return dgvr;
}

//----------------------------------------------------------------------------
// Clear
//----------------------------------------------------------------------------
void CDataGridViewMultiRow::Clear ()
{
  for (int ixRow = 0; ixRow < m_listdgvr->Count; ixRow++)
  {
    if (m_listdgvr[ixRow]->DataGridView
    &&  m_listdgvr[ixRow]->DataGridView == this->DataGridView)
      m_listdgvr[ixRow]->DataGridView->Rows->Remove (m_listdgvr[ixRow]);
    m_listdgvr[ixRow]->Visible = true;
  }
  m_listdgvr->Clear();

  Update();
}

//----------------------------------------------------------------------------
// Add
//----------------------------------------------------------------------------
bool CDataGridViewMultiRow::Add (DataGridViewRow^ i_dgvr)
{
  return Insert (m_listdgvr->Count, i_dgvr);
}

//----------------------------------------------------------------------------
// Insert
//----------------------------------------------------------------------------
bool CDataGridViewMultiRow::Insert (int i_ixRow, DataGridViewRow^ i_dgvr)
{
  if (!i_dgvr)
    return false;
  if (i_dgvr->Index < 0)
    return false;  // block shared rows and rows that are not part of a DGV
  if (i_ixRow < 0)
    return false;
  else if (i_ixRow > m_listdgvr->Count)
    i_ixRow = m_listdgvr->Count;

  m_listdgvr->Insert (i_ixRow, i_dgvr);

  if (i_dgvr->DataGridView
  &&  i_dgvr->DataGridView != this->DataGridView)
    i_dgvr->DataGridView->Rows->Remove (i_dgvr);
  if (this->DataGridView)
  {
    int ixDgvr = this->DataGridView->Rows->IndexOf (this) + 1 + i_ixRow;
    if (i_dgvr->DataGridView == this->DataGridView
    &&  i_dgvr->Index != ixDgvr)
      i_dgvr->DataGridView->Rows->Remove (i_dgvr);

    ixDgvr = this->DataGridView->Rows->IndexOf (this) + 1 + i_ixRow;
    if (i_dgvr->DataGridView != this->DataGridView)
      this->DataGridView->Rows->Insert (ixDgvr, i_dgvr);
  }
  i_dgvr->Visible = !m_bHideRows;

  Update();

  return true;
}

//----------------------------------------------------------------------------
// Remove
//----------------------------------------------------------------------------
bool CDataGridViewMultiRow::Remove (int i_ixRow)
{
  return Remove (Rows[i_ixRow]);
}

//----------------------------------------------------------------------------
// Remove
//----------------------------------------------------------------------------
bool CDataGridViewMultiRow::Remove (DataGridViewRow^ i_dgvr)
{
  bool bResult = m_listdgvr->Remove (i_dgvr);

  if (i_dgvr)
  {
    if (i_dgvr->DataGridView
    &&  i_dgvr->DataGridView == this->DataGridView)
      i_dgvr->DataGridView->Rows->Remove (i_dgvr);
    i_dgvr->Visible = true;
  }

  Update();

  return bResult;
}

//----------------------------------------------------------------------------
// Update
//----------------------------------------------------------------------------
void CDataGridViewMultiRow::Update ()
{
  if (!this->DataGridView)
    return;
  if (this->Index < 0)
    throw gcnew InvalidOperationException ("Index is < 0. This may happen if the row was created by CreateCells(), then added to a DGV, which made a previously shared row become unshared, and then being accessed by the same invalidated object. Get the updated row object from the DGV.");

  array<int>^ aiNewLines = gcnew array<int>(m_listdgvr->Count);
  array<String^, 2>^ a2sValue = gcnew array<String^, 2>(this->Cells->Count, m_listdgvr->Count);

  for (int ixCell = 0; ixCell < Cells->Count; ixCell++)
  {
    for (int ixRow = 0; ixRow < m_listdgvr->Count; ixRow++)
    {
      if (m_listdgvr[ixRow]->Index < 0)
        continue;
      Object^ oValue = m_listdgvr[ixRow]->Cells[ixCell]->Value;
      if (oValue)
      {
        a2sValue[ixCell, ixRow] = oValue->ToString();
        int iNewLines = CString::Count (a2sValue[ixCell, ixRow], CONSTS::CRLF, StringComparison::InvariantCultureIgnoreCase);
        aiNewLines[ixRow] = Math::Max (aiNewLines[ixRow], iNewLines);
      }
    }
  }

  for (int ixCell = 0; ixCell < Cells->Count; ixCell++)
  {
    String^ sText = nullptr;
    for (int ixRow = 0; ixRow < m_listdgvr->Count; ixRow++)
    {
      if (ixRow > 0)
        sText += CONSTS::CRLF;
      sText += a2sValue[ixCell, ixRow];
      int iNewLines = CString::Count (a2sValue[ixCell, ixRow], CONSTS::CRLF, StringComparison::InvariantCultureIgnoreCase);
      sText += CString::Repeat (CONSTS::CRLF, aiNewLines[ixRow] - iNewLines);
    }
    this->Cells[ixCell]->Value = sText;
  }
}

//----------------------------------------------------------------------------
// OnDataGridViewChanged
//----------------------------------------------------------------------------
void CDataGridViewMultiRow::OnDataGridViewChanged ()
{
  try
  {
    if (this->DataGridView)
    {
      int ixDgvr = this->DataGridView->Rows->IndexOf (this) + 1;
      for (int ixCnt = 0; ixCnt < m_listdgvr->Count; ixCnt++)
        DataGridView->Rows->Insert (ixDgvr + ixCnt, m_listdgvr[ixCnt]);
    }
    else
    {
      for (int ixCnt = 0; ixCnt < m_listdgvr->Count; ixCnt++)
        m_listdgvr[ixCnt]->DataGridView->Rows->Remove (m_listdgvr[ixCnt]);
    }
  }
  finally
  {
    DataGridViewRow::OnDataGridViewChanged();
  }
}

//----------------------------------------------------------------------------
// PaintRow
//----------------------------------------------------------------------------
void CDataGridViewMultiRow::PaintRow (Object^ sender, DataGridViewRowPrePaintEventArgs^ e)
{
  ::DataGridView^ dgv = dynamic_cast<::DataGridView^>(sender);
  if (!dgv)
    return;
  if (e->RowIndex < 0 || e->RowIndex >= dgv->RowCount)
    return;
  CDataGridViewMultiRow^ dgvmr = dynamic_cast<CDataGridViewMultiRow^>(dgv->Rows->SharedRow(e->RowIndex));
  if (!dgvmr)
    return;
  if (dgvmr->DataGridView != dgv)
    return;

  bool bAutoHeight = dgv->AutoSizeRowsMode == DataGridViewAutoSizeRowsMode::AllCells
                  || dgv->AutoSizeRowsMode == DataGridViewAutoSizeRowsMode::AllCellsExceptHeaders
                  || dgv->AutoSizeRowsMode == DataGridViewAutoSizeRowsMode::DisplayedCells
                  || dgv->AutoSizeRowsMode == DataGridViewAutoSizeRowsMode::DisplayedCellsExceptHeaders;
  Graphics^ g = e->Graphics;
  StringFormatFlags enFlags = (StringFormatFlags)0;
  if (dgvmr->InheritedStyle->WrapMode != DataGridViewTriState::True)
    enFlags = enFlags | StringFormatFlags::NoWrap;
  StringFormat^ oStringFormat = gcnew StringFormat(enFlags);

  array<float>^       afRowHeight = gcnew array<float>(dgvmr->RowCount);
  array<int>^         aiLines     = gcnew array<int>  (dgvmr->RowCount);
  array<int,     2>^  a2iLines    = gcnew array<int,     2>(dgvmr->Cells->Count, dgvmr->RowCount);
  array<String^, 2>^  a2sValue    = gcnew array<String^, 2>(dgvmr->Cells->Count, dgvmr->RowCount);
  for (int ixRow = 0; ixRow < dgvmr->RowCount; ixRow++)
  {
    DataGridViewRow^ dgvr = dgvmr->Rows[ixRow];
    for (int ixCell = 0; ixCell < dgvmr->Cells->Count; ixCell++)
    {
      if (dgvr->Index < 0)
        continue;
      Object^ oValue = dgvr->Cells[ixCell]->Value;
      if (!oValue)
        continue;
      a2sValue[ixCell, ixRow] = oValue->ToString();
      int iCharacters = 0, iLines = 0;
      SizeF oLayoutArea ((float)dgvmr->Cells[ixCell]->Size.Width, 0);
      SizeF oTextSize = g->MeasureString (a2sValue[ixCell, ixRow],
                                          dgvmr->Cells[ixCell]->InheritedStyle->Font,
                                          oLayoutArea,
                                          oStringFormat,
                                          iCharacters,
                                          iLines);
      float fHeight = oTextSize.Height;
      if (!bAutoHeight)
        fHeight += 4;
      afRowHeight[ixRow] = Math::Max (afRowHeight[ixRow], fHeight);
      a2iLines[ixCell, ixRow] = iLines;
      aiLines[ixRow] = Math::Max (aiLines[ixRow], iLines);
    }
  }
  int iLength = dgv->Columns->GetColumnsWidth(DataGridViewElementStates::Visible);
  int iHeight = (int)Math::Ceiling(CMath::Sum (afRowHeight));
  dgvmr->Height = iHeight;

  e->PaintCellsBackground (e->ClipBounds, true);

  int iPositionX = e->RowBounds.X + dgvmr->HeaderCell->Size.Width - dgv->HorizontalScrollingOffset;
  int iPositionY = 0;
  for (int ixCell = 0; ixCell < dgvmr->Cells->Count; ixCell++)
  {
    String^ sText = nullptr;
    DataGridViewCell^ oCell = dgvmr->Cells[ixCell];
    Color oTextColor = oCell->Selected ? oCell->InheritedStyle->SelectionForeColor : oCell->InheritedStyle->ForeColor;
    Drawing::Brush^ oBrush = gcnew Drawing::SolidBrush (oTextColor);
    iPositionY = e->RowBounds.Y;
    if (!bAutoHeight)
      iPositionY += 2;
    for (int ixRow = 0; ixRow < dgvmr->RowCount; ixRow++)
    {
      if (ixRow > 0)
        sText += CONSTS::CRLF;
      sText += a2sValue[ixCell, ixRow];
      sText += CString::Repeat (CONSTS::CRLF, aiLines[ixRow] - a2iLines[ixCell, ixRow]);

      Rectangle oRectText (iPositionX, iPositionY, oCell->Size.Width, oCell->Size.Height);
      g->DrawString (a2sValue[ixCell, ixRow], oCell->InheritedStyle->Font, oBrush, oRectText, oStringFormat);
      iPositionY += (int)afRowHeight[ixRow];
    }
    dgvmr->Cells[ixCell]->Value = sText;
    iPositionX += oCell->Size.Width;
  }

  Color oLineColor = dgvmr->Selected ? dgvmr->InheritedStyle->SelectionForeColor : dgvmr->InheritedStyle->ForeColor;
  Pen^ oPen = gcnew Pen(oLineColor , 1);
  oPen->DashPattern = gcnew array<float>{5, 15};
  iPositionX = e->RowBounds.X + dgvmr->HeaderCell->Size.Width - dgv->HorizontalScrollingOffset;
  iPositionY = e->RowBounds.Y;
  for (int ixRow = 0; ixRow < dgvmr->RowCount - 1; ixRow++)
  {
    iPositionY += (int)afRowHeight[ixRow];
    g->DrawLine (oPen, iPositionX,           iPositionY,
                       iPositionX + iLength, iPositionY);
  }

  e->PaintHeader (true);

  e->Handled = true;
}

//----------------------------------------------------------------------------
// CommonConstructor
//----------------------------------------------------------------------------
void CDataGridViewMultiRow::CommonConstructor ( bool            i_bHideRows,
                                                ::DataGridView^ i_dgv)
{
  m_bHideRows = i_bHideRows;

  if (i_dgv)
    this->CreateCells(i_dgv);

  m_listdgvr = gcnew List<DataGridViewRow^>;
  this->ReadOnly = true;
}