БлогNot. C#: Табличные компоненты Windows Forms и работа с ними

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 [3825 просмотров]


теги: программирование c# учебное xml

К этой статье пока нет комментариев, Ваш будет первым