C# DataGridView: объединить несколько строк в одну, но сохранить оригиналы

Прежде всего: я собираюсь дать ответ на этот вопрос сам, как только я закончу.
Но вы можете помочь мне на этом пути, я буду признателен за все ваши советы.

У меня есть DataGridView с различными строками, которые принадлежат друг другу. Моя задача - найти подходящий способ отображения и работы с этими связанными строками.

Моей первой идеей было сохранить индивидуальность каждого ряда, но у этого есть некоторые недостатки:

  • как четко показать, что ряды принадлежат друг другу?
    Я мог бы добавить еще один столбец, ячейки которого показывают одинаковое число для связанных строк, но это не так легко увидеть, и для него требуется другой столбец.
    Мое решение здесь состояло бы в том, что все связанные строки имеют один и тот же цвет фона, и цвет меняется, например, между белым и светло-серым для каждого набора связанных строк.

  • как работать со связанными рядами? Как только я выберу одну строку набора, мне придется проанализировать эту строку, извлекая информацию (сохраненную в теге ячейки или в скрытой ячейке), какие строки принадлежат друг другу, и также выбирать их. Еще большая работа по перемещению строк вверх / вниз в DataGridView: мне нужно было бы также проанализировать соседние наборы строк, чтобы увидеть, как далеко я должен двигаться.

Поэтому я решил создать DataGridViewMultiRow,
Я опубликую полный код этого класса здесь как ответ, когда закончите.

Это унаследует от DataGridViewRow ("DGVR") и сохраните список из одного DGVR или других многорядных строк и отобразите их как единое целое, нарисовав ячейки строки собственным кодом. Однако мне все еще нужно выяснить, какие события использовать для этой цели. MSDN предлагает использовать DataGridView.RowPrePaint, но я скорее хочу использовать событие, которое связано с самим DGVR. Может быть, я проанализирую исходный код DataGridViewRow.Paint() и напишу свой собственный метод...

Отдельные строки будут сделаны невидимыми при добавлении в многорядную строку (их можно переключить на видимую снова, злоупотребив концепцией, но в самой.net есть много вещей, которые не защищены от злоупотреблений; возможно, я даже не переключаюсь к невидимым, так что это в ответственности пользователя).
Рекурсии в многорядных строках просто избежать, заставив каждый DGVR быть частью того же самого DGV, что и многорядный, и поскольку каждую строку можно добавить только в один DGV и только один раз, мне не нужно проверять наличие рекурсия больше

Сейчас я изо всех сил пытаюсь реализовать внутренний список строк. Я думал об использовании.net DataGridViewRowCollection, но я обнаружил, что это операция тесно связана с DataGridView сам по себе: DGV может иметь только один DGVRColl, но каждый DGVRColl ссылается на DGV. Так что в каждом моем DGVMultiRow будет полусвязанный DGVRColl.
Я собирался спросить, не вызовет ли это проблемы, но я уже обнаружил, что должен предоставить DGV при создании экземпляра DGVRColl, которого у меня нет в тот момент, когда вызывается ctor DGVMultiRow. Кроме того, при использовании DGVRColl и предоставлении ему публичного свойства get я могу только подключиться к событию CollectionChanged и не иметь никакого контроля над отдельными операциями, такими как Add() а также Remove(), Поэтому я буду использовать простой приватный список.

Продолжение № 1
Я сделал основную работу, она выглядит довольно хорошо:

Мне все еще нужно исправить детали, такие как правильное размещение текста при перемещении полосы прокрутки и другие мелочи.

Я решил не отменять DataGridViewRow.Paint() потому что у этого слишком много внутренних связей. Итак, я сначала поиграл с использованием CellPainting Событие DGV, которое было хорошим началом. Но мне нужно было получать информацию обо всех ячейках подряд одновременно, поэтому я решил переопределить DataGridView.RowPrePaint() как предложено MSDN, см. ссылку выше. Это работает очень хорошо.

Продолжение следует.

1 ответ

Решение

После различных недостатков я наконец-то создал решение.
Он написан на C++/CLI, поэтому большинству из вас придется адаптировать его для использования в C#.
Это решение содержит некоторые пользовательские функции, которые не являются частью решения, но назначение которых должно быть легко угадано по их именам.

Вот предварительный просмотр:

#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;
}
Другие вопросы по тегам