Сообщений 26    Оценка 534 [+2/-0]         Оценить  
Система Orphus

DataGrid FAQ

Авторы: SiAVoL
Beker

Источник: RSDN Magazine #2-2004
Опубликовано: 31.10.2004
Исправлено: 10.12.2016
Версия текста: 1.0
Раскраска ячеек в зависимости от значения
Регистрация в дизайнере стилей колонок (DataGridColumnStyle)
Изменение порядка колонок (визуальное и программное)
Выделение всей строки в Datagrid (Многотабличные Datagrid).
Получение выделенной (текущей) строки в Datagrid.
Скрытие столбца в Datagrid.
Редактирование ячеек с помощью ComboBox

Код к статье

Раскраска ячеек в зависимости от значения

Отображением ячеек занимаются классы наследники System.Windows.Forms.DataGridColumnStyle. В WinForms уже существует два таких наследника DataGridBoolColumn и DataGridTextBoxColumn, вы с ними наверняка знакомы. Можно создавать наследников как непосредственно от DataGridColumnStyle, так и унаследоваться от уже существующего стиля. Для нашей задачи проще унаследоваться от DataGridTextBoxColumn. Для настройки отображения ячеек надо переопределить метод Paint:

      public
      class DataGridColorTextBoxColumnStyle : DataGridTextBoxColumn
{
  public DataGridColorTextBoxColumnStyle()
  {}

  protected override void Paint(
    Graphics g, 
    Rectangle bounds, 
    CurrencyManager source, 
    int rowNum, 
    Brush backBrush, 
    Brush foreBrush, 
    bool alignToRight)
  {
    try
    {
      string value = GetColumnValueAtRow(source, rowNum) as string;
      if (value == "Bill")
         backBrush = new SolidBrush(Color.AliceBlue);
    }
    finally
    {
      // Обязательно вызываем отрисовку базового класса
      base.Paint (g, bounds, source, rowNum, backBrush, 
        foreBrush, alignToRight);
    }
  }
}
ПРИМЕЧАНИЕ

В примере не производится проверка значения value на null, т.к. в данном случае проверки на равенство конкретному значению вполне достаточно и null не пройдет :). Но в общем случае надо обязательно проверять тип отображаемого значения.

Наш стиль подкрашивает ячейку голубым цветом, если в ней содержится строка «Bill».


Недостатком такого подхода является то, что логика раскраски жестко зашита в класс. И в таком случае на каждый чих нам будет необходимо создавать отдельный класс, что плохо сочетается с принципами объектно-ориентированного проектирования. Решением является использование событий. Наш стиль колонки будет возбуждать событие, а подписчику будет предоставлена возможность выбора параметров отрисовки. Первым делом мы создадим класс аргументов нашего события:

      public
      class DataGridColorlEventArgs: System.EventArgs
{
  /// <summary>/// номер столбца рисуемой ячейки/// </summary>publicreadonlyint Column;
  /// <summary>///  номер строки рисуемой ячейки/// </summary>publicreadonlyint Row;
  /// <summary>/// текущее значение в ячейке/// </summary>publicreadonlyobject CurrentCellValue;

  /// <summary>///  шрифт для вывода текста в ячейке/// </summary>public Font TextFont;

  /// <summary>/// кисть, используемая для рисования фона ячейки/// </summary>public Brush BackBrush;

  /// <summary>///  кисть для рисования текста в ячейке/// </summary>public Brush ForeBrush;

  /// <summary>/// Аргумент события грида/// </summary>/// <param name="column">Номер колонки</param>/// <param name="row">Номер строки</param>/// <param name="currentCellValue">Значение текущей ячейки</param>public DataGridColorlEventArgs(
    int column,
    int row,
    object currentCellValue)
  {
    Column = column;
    Row = row;
    CurrentCellValue = currentCellValue;
  }
}

Также нам понадобится делегат обратного вызова для функции-обработчика события

      public
      delegate
      void DataGridColorEventHandler(
  object sender, 
  DataGridColorlEventArgs e);

Тогда наш новый стиль будет выглядеть так:

      public
      class DataGridColorTextBoxColumnStyle2 : DataGridTextBoxColumn
{
  public DataGridColorTextBoxColumnStyle2()
  {}

  protectedoverridevoid Paint(
    Graphics g, 
    Rectangle bounds, 
    CurrencyManager source, 
    int rowNum, 
    Brush backBrush, 
    Brush foreBrush, 
    bool alignToRight)
  {
    try
    {
      int col = DataGridTableStyle.GridColumnStyles.IndexOf(this);
      DataGridColorlEventArgs e = 
        new DataGridColorlEventArgs(col, rowNum,
        this.GetColumnValueAtRow(source, rowNum));
      OnCellPaint(e);

      if (e.BackBrush != null)
        backBrush = e.BackBrush;
      if (e.ForeBrush != null)
        foreBrush = e.ForeBrush;
    }
    finally
    {
      base.Paint (g, bounds, source, rowNum, backBrush, 
        foreBrush, alignToRight);
    }
  }

  /// <summary>/// Событие, которое передается управляющему объекту/// для решения о цвете и шрифте отображения данных/// </summary>publicevent DataGridColorEventHandler CellPaint;
  /// <summary>/// Рассылка события SetCellFormat/// </summary>/// <param name="e"></param>protectedvirtualvoid OnCellPaint(
    DataGridColorlEventArgs e)
  {
    if (CellPaint != null)
      CellPaint(this, e);
  }
}

Для использования данного стиля необходимо подписаться на событие OnCellPaint:

dataGridTextBoxColumn1.CellPaint += new DataGridColorEventHandler(dataGridTextBoxColumn1_CellPaint);

и в функции-обработчике события определять, как будет отображаться колонка:

      private
      void dataGridTextBoxColumn1_CellPaint(
  object sender, DataGridColorlEventArgs e)
{
  int value = (int)dataView1[e.Row]["id"];
  if (value > 2)
    e.BackBrush = new SolidBrush(Color.AliceBlue); 
}


Регистрация в дизайнере стилей колонок (DataGridColumnStyle)

Итак, мы создали свой стиль колонки, но для его использования приходится набивать код вручную. Хотелось бы иметь возможность выбирать наши стили в дизайнере так же, как стандартные. К счастью, это несложно реализовать.

Первое, что необходимо сделать – это создать свой класс стиля таблицы - DataGridTableStyle

      public
      class MyDataGridTableStyle : DataGridTableStyle
{
  [Editor(typeof(MyGridColumnStylesCollectionEditor), typeof(UITypeEditor))]
  publicnew GridColumnStylesCollection GridColumnStyles
  {
    get {returnbase.GridColumnStyles;}
  }

  privateclass MyGridColumnStylesCollectionEditor : CollectionEditor
  {
    public MyGridColumnStylesCollectionEditor(Type type) : base(type)
    {
    }

    protectedoverride System.Type[] CreateNewItemTypes()
    {
      returnnew Type[] {
              typeof(DataGridNoActiveCellColumn),
              typeof(DataGridTextBoxColumn),
              typeof(DataGridBoolColumn)
              }; 
    } 
  } 
}

Единственной целью данного класса является указание для свойства GridColumnStyles редактора, «знающего» о наших стилях колонок. У нас таким редактором является класс MyGridColumnStylesCollectionEditor. Теперь аналогичным образом надо заместить свойство TableStyles у DataGrid`а:

      public
      class DataGridEx : DataGrid
{
  public DataGridEx()
  {
    this.TableStyles.CollectionChanged +=
      new CollectionChangeEventHandler(TableStyles_CollectionChanged);
  }

  [Editor(typeof(MyDataGridTableStylesCollectionEditor), typeof(UITypeEditor))]
  publicnew GridTableStylesCollection TableStyles
  {
    get{returnbase.TableStyles;}
  }
  
  privateclass MyDataGridTableStylesCollectionEditor : CollectionEditor
  {
    public MyDataGridTableStylesCollectionEditor(Type type):base(type)
    {
    }

    protectedoverride System.Type[] CreateNewItemTypes()
    {
      returnnew Type[] {typeof(MyDataGridTableStyle)};
    } 
  } 
}

Изменение порядка колонок (визуальное и программное)

Практически во всех коммерческих Grid`ах есть очень удобная возможность мышкой изменять порядок колонок, просто перетаскивая их с одного места на другое. Почему бы нам не сделать такую же «фичу» в стандартном DataGrid`е?

Первое, что потребуется – функция, перемещающая колонку на другую позицию. Колонки хранятся в свойстве GridColumnStyles класса DataGridTableStyle. На экране они отображаются в порядке их следования в коллекции GridColumnStylesCollection. К сожалению, класс GridColumnStylesCollection не позволяет вставлять элементы в произвольную позицию, поэтому придется вооружиться автогеном :)

      public
      void MoveColumn(int fromCol, int toCol) 
{
  if(fromCol == toCol) return; 
  if (toCol == ColumnShift.Length-1)
    toCol--;

  CurrencyManager mgr = (CurrencyManager)BindingContext[
    this.DataSource, this.DataMember];
  DataRowView row = mgr.Current as DataRowView;
  string mappingName = row.Row.Table.TableName;

  DataGridTableStyle oldTS = CurrentTableStyle; 
  DataGridTableStyle newTS = new DataGridTableStyle(); 
  newTS.MappingName = mappingName; 

  for(int i = 0; i < oldTS.GridColumnStyles.Count; ++i) 
  { 
    if(i != fromCol && fromCol < toCol) 
      newTS.GridColumnStyles.Add(oldTS.GridColumnStyles[i]); 
    if(i == toCol) 
      newTS.GridColumnStyles.Add(oldTS.GridColumnStyles[fromCol]); 
    if(i != fromCol && fromCol > toCol) 
      newTS.GridColumnStyles.Add(oldTS.GridColumnStyles[i]);      
  } 

  this.TableStyles.Remove(oldTS); 
  this.TableStyles.Add(newTS); 
}

Ну а реализация перетаскивания колонок мышью уже дело техники. Мы будем отслеживать номер колонки, над которой произошло нажатие кнопки мыши. Инициировать процесс перетаскивания колонки будем не сразу, а по прошествии небольшого промежутка времени. Это исключит изменения порядка колонок при быстром щелчке мышью по колонке для выполнения сортировки. Для этого мы будем использовать класс Timer.

      protected
      override
      void OnMouseDown(MouseEventArgs e)
{
  DataGrid.HitTestInfo hti = this.HitTest(e.X, e.Y);
  if (hti.Type == DataGrid.HitTestType.ColumnHeader)
  {
    // Таймер для включения drag`n`drop колонок только // после определенного времени удержания кнопки
    timer = new System.Threading.Timer(
      new System.Threading.TimerCallback(OnMouseDownTimer),
      null, 200, -1);
    mouseDownColumn = currentMouseColumn =  hti.Column;
    GenerateColumnShift();
    movingMouseDown = false;
  }

  base.OnMouseDown (e);
}

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

      protected
      override
      void OnMouseMove(MouseEventArgs e)
{
  if(movingColumn && e.Button == MouseButtons.Left)
  {
    DataGrid.HitTestInfo hti = this.HitTest(e.X, e.Y);

    int column = hti.Column;
    if (column < 0)
      column = 0;
    if ( (ColumnShift[column+1] - e.X) < (e.X - ColumnShift[column]) )
      column++;

    if(column != currentMouseColumn)
    {
      if(movingMouseDown)
        EraseLine();
      currentMouseColumn = column;

      DrawLine(currentMouseColumn);
      movingMouseDown = true;
    }
    return; // не вызываем базовый метод, т.к. не нужны ресайзы и пр.
  }

  base.OnMouseMove(e);
}

А при отпускании кнопки мыши произведем перемещение колонки и уничтожение таймера:

      protected
      override
      void OnMouseUp(MouseEventArgs e)
{
  if(movingMouseDown && movingColumn)
  {
    EraseLine();
    MoveColumn(mouseDownColumn, currentMouseColumn);
    movingColumn = false;
    ColumnShift = null;
  }
  elsebase.OnMouseUp (e);

  if (timer != null)
    timer.Dispose();
}
ПРЕДУПРЕЖДЕНИЕ

У предложенного подхода есть один недостаток. Дело в том, что данные о колонках берутся из стилей колонок, поэтому они должны быть обязательно созданы. А если, например, подключить DataGrid к DataSet`у и не настраивать TableStyles, то перемещение колонок работать не будет.

Полный код с примером использования можно посмотреть в прилагающемся архиве.

Выделение всей строки в Datagrid (Многотабличные Datagrid).

Добавляем DataGrid событие dataGrid_MouseUp и помещаем следующий код:

      private
      void dataGrid_MouseUp(object sender, System.Windows.Forms.MouseEventArgs e)
{
// dataGrid.CurrentRowIndex определяем номер строки, на которой установлен курсор
  dataGrid.Select( dataGrid.CurrentRowIndex );
}
ПРИМЕЧАНИЕ

К сожалению, данный способ подходит только для однотабличных DataGrid`ов. В многотабличных DataGrid`ах (Master-Detail) для определения строки, на которую был произведен клик мыши лучше использовать метод HitTestInfo:

      private
      void dataGrid_MouseUp(object sender, System.Windows.Forms.MouseEventArgs e)
{
  System.Drawing.Point point = new Point(e.X,e.Y );
  DataGrid.HitTestInfo info = dataGrid.HitTest(point);
  if (info.Type == DataGrid.HitTestType.Cell)
  {
    dataGrid.CurrentCell = new DataGridCell(info.Row,info.Column);
    dataGrid.Select(info.Row);
  }
}

Получение выделенной (текущей) строки в Datagrid.

Чтобы получить выделенную строку в DataGrid, нужно:

  1. Определить с помощью HitTestInfo текущую строку.
  2. Получить данные строки с помощью BindingManagerBase:
      private
      void dataGrid_MouseUp(object sender, System.Windows.Forms.MouseEventArgs e)
{
  DataGrid.HitTestInfo info = dataGrid.HitTest(e.X,e.Y);
  if (info.Type == DataGrid.HitTestType.Cell)
  {
    BindingManagerBase bmb = this.BindingContext[dataGrid.DataSource,dataGrid.DataMember];
    bmb.Position = info.Row;
    DataRowView drv = (DataRowView) bmb.Current;  
    MessageBox.Show(@"Строка:" + Environment.NewLine + drv[0].ToString() + Environment.NewLine 
+ drv[1].ToString() + Environment.NewLine);
  }
}

Скрытие столбца в Datagrid.

Приведу пример скрытия столбца по щелчку на заголовке. В событии MouseUp отлавливаем позицию курсора, c помощью HitTestInfo получаем номер столбца, который нужно скрыть, и у текущего DataGridTableStyle удаляем по номеру GridColumnStyles.

      private
      void dataGrid_MouseUp(object sender, System.Windows.Forms.MouseEventArgs e)
{
  DataGrid.HitTestInfo info = dataGrid.HitTest(e.X,e.Y);
  if (info.Type == DataGrid.HitTestType.ColumnHeader )
  {
    DataGridTableStyle tableStyle = dataGrid.TableStyles[dataGrid.DataMember]; 
    tableStyle.GridColumnStyles.RemoveAt(info.Column);
  }
}

Редактирование ячеек с помощью ComboBox

Довольно часто приходится предоставлять конкретный набор данных для изменения поля в DataGrid. Наиболее удобно это делается с помощью элемента управления ComboBox. Создадим свой класс DataGridComboBoxColumn, унаследованный от DataGridTextBoxColumn, и переопределим в нем следующие методы:

При изменении ячейки в DataGrid подменяем TextBox на ComboBox:

      protected
      override
      void Edit(System.Windows.Forms.CurrencyManager source, int rowNum, System.Drawing.Rectangle bounds, bool readOnly, string instantText, bool cellIsVisible)
{
  base.Edit(source,rowNum, bounds, readOnly, instantText , cellIsVisible);

  _rowNum = rowNum;
  _source = source;
  
  ColumnComboBox.Parent = this.TextBox.Parent;
  ColumnComboBox.Location = this.TextBox.Location;
  ColumnComboBox.Size = new System.Drawing.Size(this.TextBox.Size.Width, ColumnComboBox.Size.Height);
  ColumnComboBox.SelectedIndex = ColumnComboBox.FindStringExact(this.TextBox.Text);
  ColumnComboBox.Text =  this.TextBox.Text;
  this.TextBox.Visible = false;
  ColumnComboBox.Visible = true;
  
  ColumnComboBox.BringToFront();
  ColumnComboBox.Focus();  
}

Здесь _rowNum и _source – это глобальные объявления в DataGridComboBoxColumn, они нам понадобятся позже, Далее вся работа выполняется comboBox-ом. Для него нужно создать событие:

ColumnComboBox.SelectionChangeCommitted += new EventHandler(ComboStartEditing);

тело события:

      private
      void ComboStartEditing(object sender, EventArgs e)
{
  // разрешаем правку данных
  _isEditing = true;  
  base.ColumnStartedEditing((Control) sender);
}

Оно вызывается при подтверждении изменения выбранного элемента comboBox, и производит только одно действие - устанавливает флаг _isEditing (глобальное объявление DataGridComboBoxColumn) в значение true, то есть разрешает правку значения.

После работы с comboBox нужно вернуть выбранное значение в источники данных. Этим займется событие:

ColumnComboBox.Leave += new EventHandler(LeaveComboBox);

которое скрывает comboBox, и, если его значение было изменено (_isEditing==true), вызывает метод SetColumnValueAtRow:

      private
      void LeaveComboBox(object sender, EventArgs e)
{
  if(_isEditing)
  {
    SetColumnValueAtRow(_source, _rowNum, ColumnComboBox.Text);
    _isEditing = false;
    Invalidate();
  }
  ColumnComboBox.Hide();
}

Здесь в качестве параметров в метод SetColumnValueAtRow передается _rowNum, _source (были заданы в методе Edit) и само выбранное значение:

      protected
      override
      void SetColumnValueAtRow(System.Windows.Forms.CurrencyManager source, int rowNum, object value)
{
  object s = value;
  DataTable  dt = (DataTable )this.ColumnComboBox.DataSource;
  int rowCount = dt.Rows.Count;
  int i = 0;
  while (i < rowCount)
  {
    if( s.Equals( dt.Rows [i][this.ColumnComboBox.DisplayMember]))
      break;
    ++i;
  }
  if(i < rowCount)
    s =  dt.Rows [i][this.ColumnComboBox.ValueMember];
  else
    s = DBNull.Value;
  base.SetColumnValueAtRow(source, rowNum, s);
}

В DataGrid отображаемые данные в ячейки - получены с помощью GetColumnValueAtRow :

      protected
      override
      object GetColumnValueAtRow(System.Windows.Forms.CurrencyManager source, int rowNum)
{
  object s =  base.GetColumnValueAtRow(source, rowNum);
  DataTable  dt = (DataTable )this.ColumnComboBox.DataSource;
  int rowCount = dt.Rows.Count; 
  int i = 0;
  while (i < rowCount)
  {
    if( s.Equals( dt.Rows [i][this.ColumnComboBox.ValueMember]))
      break;
    ++i;
  }
  if(i < rowCount)
    return dt.Rows [i][this.ColumnComboBox.DisplayMember];
  return DBNull.Value;
}

И наконец-то переопределить метод Commit:

      protected
      override
      bool Commit(System.Windows.Forms.CurrencyManager dataSource, int rowNum)
{
  if(_isEditing)
  {
    _isEditing = false;
    SetColumnValueAtRow(dataSource, rowNum, ColumnComboBox.Text);
  }
  returntrue;
}
ПРЕДУПРЕЖДЕНИЕ

В приведенном примере есть один недостаток. Если делать навигацию по строке с помощью клавиши Tab, то при нажатии на эту клавишу comboBox получит фокус, но если отпустить клавишу, фокус перейдет к следующему элементу. Таким образом, невозможно с помощью клавиши Tab передать фокус колонке, содержащей comboBox.

Избежать данной ситуации можно следующим способом – создать свой comboBox, унаследованный от стандартного, в котором поставить ловушку для сообщения WM_KEYUP:

      public
      class NoKeyUpCombo : ComboBox
{
  privateconstint WM_KEYUP = 0x101;
  protectedoverridevoid WndProc(ref System.Windows.Forms.Message m)
  {
    if(m.Msg == WM_KEYUP)
    {
      return;
    }
    base.WndProc(ref m);
  }
}


Эта статья опубликована в журнале RSDN Magazine #2-2004. Информацию о журнале можно найти здесь
    Сообщений 26    Оценка 534 [+2/-0]         Оценить