Отображением ячеек занимаются классы наследники 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); } |
Итак, мы создали свой стиль колонки, но для его использования приходится набивать код вручную. Хотелось бы иметь возможность выбирать наши стили в дизайнере так же, как стандартные. К счастью, это несложно реализовать.
Первое, что необходимо сделать – это создать свой класс стиля таблицы - 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_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, нужно:
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); } } |
Приведу пример скрытия столбца по щелчку на заголовке. В событии 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); } } |
Довольно часто приходится предоставлять конкретный набор данных для изменения поля в 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); } } |