C#: Табличные компоненты Windows Forms и работа с ними
Табличное представление данных повсеместно используется в приложениях. В этой лекции рассмотрены основные приёмы работы со следующими компонентами:
- DataGridView – табличный редактор для отображения данных из файла XML или из БД (доступен в группе "Данные" Панели Элементов);
- DataGrid – базовая табличная компонента для отображения связанных таблиц (щёлкнуть правой кнопкой мыши в Панели Элементов, команда "Выбрать элементы", дождаться загрузки списка, на вкладке "Компоненты .NET Framework" включить DataGrid из пространства имён System.Windows.Forms. После этого DataGrid можно добавить на форму).
Проект Lab4_1. Напишем простейший проект для редактирования таблицы и сохранения её в формате XML.
На форму добавим dataGridView1, установив ему свойство Dock = Fill, а объекты DataTable и DataSet создадим программно.
Для этого опишем глобально в классе формы следующие величины:
private String BaseName; private DataTable Table; private DataSet Set;
На загрузку формы реализуем такой код (обработчик события Load):
BaseName = "table.xml"; Table = new DataTable (); Set = new DataSet (); if (System.IO.File.Exists (BaseName) == false) { //Если файл не существует - создать таблицу и DataSet dataGridView1.DataSource = Table; Table.Columns.Add ("Имена"); Table.Columns.Add ("Номера телефонов"); Set.Tables.Add (Table); } else { //Если файл существует - загрузить и показать данные Set.ReadXml (BaseName); String StringXML = Set.GetXml (); dataGridView1.DataMember = "Название таблицы"; dataGridView1.DataSource = Set; }
Перед закрытием формы выполним следующий код (обработчик события FormClosing):
Table.TableName = "Название таблицы"; Set.WriteXml (BaseName);
Данные сохраняются в формате XML, после выполнения приложения найдите файл данных в папке с исполняемым файлом проекта.
Объект DataSet представляет собой кэш данных, расположенный в оперативной памяти. DataSet состоит из коллекции объектов класса DataTable.
Доступ к ячейкам таблицы можно получить, используя свойства класса DataTable (Rows, Cols, Item) - но "прямая" запись поля таблицы в файл может быть некорректной из-за того, что технология ADO.NET предусматривает кэширование данных (особенно если данные сохраняются посредством SQL-транзакций). Пример такого кода:
System.Data.DataRow newRow = Table.NewRow (); Table.Rows.Add (newRow);
Поэтому следует пользоваться методами объекта DataSet.
Скачать пример Lab4_1 в архиве .zip с проектом C# Visual Studio 2019 (11 Кб)
Проект Lab4_2. Напишем простой проект для редактирования связанных отношением "один ко многим" таблиц.
Компонента DataGrid - решение для показа связанных таблиц в одной компоненте, в DataGridView такой возможности нет. Разместим компонент на форме, можно установить свойство Dock = Fill. Также предусмотрим пункты или кнопки меню "Переключить вид", "Сохранить", "Загрузить".
Эти данные описаны глобально в классе формы:
private Boolean ShowClients; //Флажок-переключатель таблиц private System.Data.DataSet dataSet1; //Кэш данных private System.Data.DataTable Table, Table2; //Родительская и дочерняя таблицы
На загрузку формы (в обработчике её события Load) будем выполнять следующий код:
ShowClients = true; if (System.IO.File.Exists ("data.xml") == true) { //Если файл существует - загрузить и показать данные загрузитьToolStripMenuItem_Click (this, e); //Обработчик команды "Загрузить"! } else { //Иначе создать предустановленные данные //Создадим таблицу: Table = new DataTable ("Клиенты"); //Создадим и настроим столбец программно: DataColumn Column = new DataColumn ("Название организации"); Column.ReadOnly = true; Column.Unique = true; Table.Columns.Add (Column); //Добавим столбцы с настройками по умолчанию, указав только названия: Table.Columns.Add ("Контактное лицо"); Table.Columns.Add ("Телефон"); //Создадим DataSet и добавим туда таблицу: dataSet1 = new DataSet (); dataSet1.Tables.Add (Table); //Добавим в таблицу предустановленные записи об организациях-заказчиках Table.Rows.Add ("НГАСУ", "Иванов Максим", "3234566"); Table.Rows.Add ("НГТУ", "Сидорова Ксения", "3630313"); //Создадим вторую таблицу - "Заказы" Table2 = new DataTable ("Заказы"); DataColumn Column2 = new DataColumn ("Номер заказа"); Column2.DataType = System.Type.GetType ("System.Int32"); Column2.AutoIncrement = true; //Автоматический счётчик заказов Column2.ReadOnly = true; Column2.Unique = true; //Название организации - уникально! Table2.Columns.Add (Column2); Table2.Columns.Add ("Объем заказа"); Table2.Columns.Add ("Организация-заказчик"); //Добавим в DataSet вторую таблицу: dataSet1.Tables.Add (Table2); Table2.Rows.Add (1, "100000", "НГАСУ"); Table2.Rows.Add (2, "200000", "НГАСУ"); //Обеспечим отношение 1:N между первой и второй таблицами: DataColumn Parent = dataSet1.Tables ["Клиенты"].Columns ["Название организации"]; DataColumn Child = dataSet1.Tables ["Заказы"].Columns ["Организация-заказчик"]; DataRelation Link1 = new DataRelation ("Ссылка на заказы клиента", Parent, Child); // В Parent значения в связываемом столбце должны быть уникальными, в Child - нет dataSet1.Tables ["Заказы"].ParentRelations.Add (Link1); } dataGrid1.SetDataBinding (dataSet1, "Клиенты"); dataGrid1.CaptionText = "Родительская таблица \"Клиенты\""; dataGrid1.CaptionFont = new System.Drawing.Font ("Consolas", 11);
На нажатие кнопки или выбор пункта меню "Переключить вид" будем переключаться между родительской и дочерней таблицами:
if (ShowClients == true) { dataGrid1.SetDataBinding (dataSet1, "Клиенты"); dataGrid1.CaptionText = "Родительская таблица \"Клиенты\""; } else { dataGrid1.SetDataBinding (dataSet1, "Заказы"); dataGrid1.CaptionText = "Дочерняя таблица \"Заказы\""; } dataGrid1.Collapse (-1); //Свернуть все ветви ShowClients = !ShowClients;
На выбор команды "Сохранить" будем сохранять все данные в файле типа .xml текущей папки:
dataSet1.WriteXml ("data.xml", XmlWriteMode.WriteSchema);
На выбор команды "Загрузить" будем обновлять все данные из файла, сбросив несохранённые изменения, если таковые есть:
dataSet1 = new DataSet (); dataSet1.ReadXml ("data.xml"); ShowClients = true; переключитьВидToolStripMenuItem_Click (this, e);
Приложение запускается и редактирует связанные таблицы.
Скачать пример Lab4_2 в архиве .zip с проектом C# Visual Studio 2019 (12 Кб)
Проект Lab4_3. Реализуем больше возможностей компоненты DataGridView. Форма приложения будет такой же, как в проекте 4.1, а действия можно запрограммировать как реакцию на выбор пунктов верхнего меню.
Она представляет собой прямоугольный массив ячеек, который можно рассматривать как коллекцию строк или столбцов.
- Rows - это коллекция строк, имеет тип DataGridRowCollection.
- Columns - это коллекция столбцов типа DataGridColumnCollection. Оба свойства индексируются как массивы для доступа к конкретной строке/столбцу, нумерация производится с нуля.
- Cells - это коллекция ячеек из объекта DataGridRowCollection, приведём пример доступа к конкретной ячейке:
try { MessageBox.Show (dataGridView1.Rows [1].Cells [1].Value.ToString ()); } catch (Exception) { MessageBox.Show ("Нет такой ячейки"); }
- RowCount, ColumnCount - количество строк и столбцов.
В несвязанном режиме компонента может отображать любые табличные данные.
Методы для добавления/удаления/редактирования строк и столбцов относятся к коллекциям Rows и Columns и имеют типовые имена: Add, Insert, Clear, AddCopy, InsertCopy, Remove, RemoveAt, а также могут иметь по несколько перегрузок каждая, например, для метода Add добавления строки есть версии Add(), Add(int count), Add (DataGridViewRow row), Add (object []values).
private static int Cnt; //Счётчик столбцов в классе формы //... if (dataGridView1.ColumnCount < 1) { //Сначала нужно создать столбец dataGridView1.Columns.Add ("Столбец " + Cnt, "Заголовок " + Cnt); Cnt++; } dataGridView1.Rows.Add ();
Настройка внешнего вида компонента также типовая: такие свойства, как BackColor, Alignment, Font и т.д. находятся в объекте типа DataGridViewCellStyle.
Каждая ячейка представлена объектом System.Windows.Forms.DataViewCell, за "личный" внешний вид ячейки отвечает свойство InheritedStyle, а за вид по умолчанию - DefaultCellStyle.
Очередной командой перекрасим фон таблицы в розовый цвет:
dataGridView1.DefaultCellStyle.BackColor = Color.Pink;
А теперь поменяем фон только выбранной ячейки:
if (cell_y > -1 && cell_x > -1) dataGridView1.Rows[cell_y].Cells[cell_x].Style.BackColor = Color.Green;
Предполагается, что значения cell_y, cell_x описаны глобально в классе формы:
private int cell_y, cell_x;
и инициализируются в обработчике её события Load:
cell_y = cell_x = -1;
а затем получают значения в обработчиках событий KeyUp и MouseUp компоненты dataGridView1 (одинаковым кодом):
cell_y = dataGridView1.CurrentCell.RowIndex; cell_x = dataGridView1.CurrentCell.ColumnIndex;
Когда требуется форматирование содержимого ячейки DataGridView для отображения, возникает событие CellFormatting, вот пример его обработчика:
e.CellStyle.SelectionBackColor = Color.Yellow; e.CellStyle.SelectionForeColor = Color.Black;
Сделаем в dataGridView1 таблицу со значениями функции. Вот код соответствующей команды:
dataGridView1.Columns.Clear (); dataGridView1.ColumnCount = 2; dataGridView1.Rows.Add (10); //Добавили 10 строк dataGridView1.Columns [0].Name = "X"; dataGridView1.Columns [1].Name = "Y(X)"; double x; int i; for (x = 1.5, i = 0; i < 10; x += 0.1, i++) { dataGridView1.Rows [i].Cells [0].Value = Convert.ToString (x); dataGridView1.Rows [i].Cells [1].Value = Math.Round (x * x, 2).ToString (); //или dataGridView1.Rows[i].Cells[1].Value = (x*x).ToString("f"); }
Существует также множество событий, связанных с редактированием ячейки: CellBeginEdit, CellEndEdit, CellParsing, CellValidating, CellValidated и т.д.
Например, по умолчанию наша таблица редактируется. Чтобы разрешить в первом столбце (Y(X)) ввод только числовых значений, напишем следующий код, выполняемый по событию CellValueChanged компоненты DataGridView:
String Val = dataGridView1.Rows[e.RowIndex].Cells[e.ColumnIndex].Value.ToString (); if (e.ColumnIndex == 1) { double val; bool A = Double.TryParse (Val, System.Globalization.NumberStyles.Number, System.Globalization.NumberFormatInfo.CurrentInfo, out val); if (A == false) { dataGridView1.Rows[e.RowIndex].Cells [e.ColumnIndex].Value = lastValue; MessageBox.Show ("Неверное число: " + Val, "Ошибка"); } }
Здесь предполагается, что величина lastValue описана в классе формы:
private double lastValue;
и по событию CellBeginEdit, сохраняет предыдущее значение, хранимое в ячейке:
if (e.ColumnIndex == 1) lastValue = Convert.ToDouble (dataGridView1.Rows[e.RowIndex].Cells[e.ColumnIndex].Value);
Запретить редактирование данных можно стандартно – с помощью свойства ReadOnly. Чтобы запретить редактирование конкретной ячейки, строки или столбца, также воспользуйтесь этим свойством:
if (dataGridView1.ColumnCount > 0) { dataGridView1.Rows [0].Cells [0].ReadOnly = true; dataGridView1.Columns [0].ReadOnly = true; }
Скачать пример Lab4_3 в архиве .zip с проектом C# Visual Studio 2019 (12 Кб)
Проект Lab4_4. Пример выполнения варианта задания. Написать табличный редактор ведомости студенческой группы со столбцами: Фамилия, 1, 2, ..., 17 (номера недель), итого (отметка о зачете)
Данные автоматически загружаются из файла и сохраняются в файле формата xml.
Поддерживаются редактирование, в том числе, произвольное добавление и удаление строк, проверка правильности ввода данных.
Первый вариант решения - создать таблицу dataGridView, не связанную с DataSet и работать непосредственно с ячейками через методы класса dataGridView. Сложности такого подхода – придётся «вручную» писать поддержку сохранения и загрузки таблицы. Второй вариант – связать таблицу с DataSet, чтобы легко загружать и сохранять данные XML, но тогда работа с добавлением/удалением ячеек делается через методы кэша DataSet, иначе компилятор и не разрешит выполнять этого.
Создав новый проект Windows Forms с главной формой Form1, добавим на неё компоненту DataGridView и растянем на всю форму (свойство Dock = Fill). Также добавим к проекту контекстное меню contextMenuStrip с пунктами "Добавить", "Удалить", "Вычислить" и укажем его в свойстве ContextMenuStrip компоненты dataGridView1.
Пропишем в классе формы глобальные величины:
private String BaseName; private DataTable Table; private DataSet Set; private String Val; //предыдущее значение из текущей ячейки private int Column; //текущий столбец private bool Changed; //признак изменения текущей ячейки
Столбцы таблицы (фамилия, 17 граф для оценок или иных отметок, графа "зачёт") создадим программно по событию Load формы:
BaseName = "table.xml"; Table = new DataTable (); Set = new DataSet (); Table.Columns.Add ("ФИО"); for (int i = 1; i <= 17; i++) Table.Columns.Add ("" + i); Table.Columns.Add ("Итого"); Set.Tables.Add (Table); dataGridView1.DataSource = Set; Table.TableName = "Успеваемость"; if (System.IO.File.Exists (BaseName) == true) { Set.ReadXml (BaseName); } dataGridView1.DataMember = "Успеваемость"; dataGridView1.Columns [0].Width = 100; for (int i = 1; i <= 17; i++) dataGridView1.Columns [i].Width = 25;
"Подогнать" ширину столбцов под ширину формы можно, например, в обработчике события SizeChanged формы (при старте приложения ширина "подогнана" не будет):
Rectangle Rect = this.ClientRectangle; int w = Rect.Width; //клиентская ширина формы if (w < 400) w = Rect.Width = 400; int border = dataGridView1.Columns [0].DividerWidth, left = dataGridView1.Rows [0].HeaderCell.Size.Width; //ширина разделителя столбцов и закрепленного столбца слева int w1 = 100, w2 = (int) Math.Floor (( w - 2 * w1 - 19 * border - left ) / 17.0); //под 1-й и последний столбец по 100 пикселей, остальное место делим поровну dataGridView1.Columns [0].Width = dataGridView1.Columns [18].Width = w1; for (int i = 1; i <= 17; i++) dataGridView1.Columns [i].Width = w2;
Также будем автоматически сохранять данные при выходе из программы (событие формы FormClosing):
Table.TableName = "Успеваемость"; Set.WriteXml (BaseName);
По выбору пункта меню "Добавить" выполняется следующее:
DataRow newR = Set.Tables ["Успеваемость"].NewRow (); newR [0] = "Студент"; try { int i = dataGridView1.CurrentCell.RowIndex; Set.Tables ["Успеваемость"].Rows.InsertAt (newR, i); Set.Tables ["Успеваемость"].AcceptChanges (); } catch (Exception) { }
А пункт "Удалить" проще всего запрограммировать так:
try { int i = dataGridView1.CurrentCell.RowIndex; Set.Tables ["Успеваемость"].Rows [i].Delete (); Set.Tables ["Успеваемость"].AcceptChanges (); } catch (Exception) { }
Применение метода AcceptChanges нужно, чтобы изменение данных немедленно отобразилось в таблице.
Контроль правильности ввода (не более 1 символа в графы отметок, не более 20 символов в графы "ФИО" и "Итого") сделаем следующими обработчиками событий компоненты dataGridView1:
CellBeginEdit(на начало редактирования):
Column = e.ColumnIndex; //запоминаем столбец Val = dataGridView1.Rows[e.RowIndex].Cells[e.ColumnIndex].Value.ToString (); //что было в ячейке Changed = false; //ячейка не изменена
CellValueChanged (по изменению ячейки):
String newVal = dataGridView1.Rows[e.RowIndex].Cells[e.ColumnIndex].Value.ToString (); if (e.ColumnIndex > 0 && e.ColumnIndex < 18) { if (newVal.Length > 1) newVal = Val; } else if (newVal.Length > 20) newVal = newVal.Substring(0,20); dataGridView1.Rows[e.RowIndex].Cells [e.ColumnIndex].Value = newVal;
EditingControlShowing (для установки своих обработчиков в дополнение к стандартным обработчиками событий компоненты):
if (Column > 0 && Column < 18) { TextBox tb = (TextBox)e.Control; tb.MaxLength = 1; tb.KeyPress += new KeyPressEventHandler (tb_KeyPress); }
В проект добавлен метод tb_KeyPress – дополнение к обработке KeyPress, разрешающее вводить буквы, цифры, Backspace и пробел:
void tb_KeyPress (object sender, KeyPressEventArgs e) { char c = e.KeyChar; if (!( Char.IsLetterOrDigit (c) || c == (char) Keys.Back || c == (char) Keys.Space )) e.Handled = true; //а вот обработчик KeyDown так не сделать }
CellLeave (покидая ячейку, изменим правила перехода к следующей ячейке, по умолчанию это вниз, а мы хотим вправо):
//конечно, "костыль", по идее, надо писать класс-наследник //DataGridView и там управлять клавиатурой if (!Changed) { Changed = true; int c = dataGridView1.CurrentCell.ColumnIndex; if (c == dataGridView1.Columns.Count - 1) { SendKeys.Send ("{Home}"); } else { SendKeys.Send ("{Up}"); SendKeys.Send ("{Right}"); } }
Проблема состоит в том, что DataGridView обрабатывает многие клавиши своими собственными событиями и "не пускает" коды до уровня KeyPress или KeyDown (KeyUp выполняется).
Как пример расчёта добавим вычисление среднего балла по выставленным отметкам, код можно выполнить по соответствующему пункту меню:
for (int i = 0; i < dataGridView1.RowCount - 1; i++) { int sum = 0, cnt = 0, val = 0; for (int j = 1; j < dataGridView1.ColumnCount - 1; j++) { String str = dataGridView1.Rows [i].Cells [j].Value.ToString ().Trim(); try { if (str.Length > 0) val = Int32.Parse (str); else continue; } catch (Exception) { continue; } sum += val; cnt++; } if (cnt > 0) { double avg = (sum + 0.0) / cnt; //чтобы результат был double dataGridView1.Rows [i].Cells [dataGridView1.ColumnCount - 1].Value = String.Format ("{0:f2}", avg); } }
Мы выбираем для обработки только оценки (не проверяя их корректность), так как в ведомости могли быть другие отметки, например "б" (болен), "н" (отсутствует) и т.п.
Границей внешнего цикла, равной dataGridView1.RowCount - 1, мы исключаем при обработке пустую строку в конце таблицы, не содержащую данных.
Скачать пример Lab4_4 в архиве .zip с проектом C# Visual Studio 2019 (13 Кб)
Задание: реализовать табличный редактор в соответствии с вариантом задания. Предусмотреть в приложении следующие возможности:
- загрузка табличных данных из файла и их сохранение в файл;
- редактирование, добавление, удаление записей;
- при необходимости – поиск и выделение (или отображение в новом окне) записей, отвечающих заданным условиям;
- реализация расчётов, указанных в варианте задания.
Разработка базового редактора несвязанной таблицы на основе DataGridView (файл .pdf) (483 Кб)
Проект C# (Visual Studio 2019) из этой статьи, развернуть архив .zip в новую папку (12 Кб)
09.03.2023, 10:49 [2828 просмотров]