Как работать с массивами структурированных данных, часть II
- Добавляем меню
- Добавляем возможность редактировать записи
- Добавляем возможность удалять записи
- Добавляем отчеты
Добавляем меню
Надо признать, что кнопки занимают сильно много места. Так что давайте удалим все кнопки, а вместо них добавим стандартное меню.
теперь добавим пункты меню как на картинке:
теперь кликнем дважды на пункт меню Файл/Загрузить…, должен добавится обработчик клика на этот пункт меню, вставим в него код из button3_Click
private void загрузитьToolStripMenuItem_Click(object sender, EventArgs e)
{
var openFileDialog = new OpenFileDialog();
if (openFileDialog.ShowDialog() != DialogResult.OK)
return;
var serializer = new XmlSerializer(typeof(SolarSystemObject[]));
using (var fs = new FileStream(openFileDialog.FileName, FileMode.Open))
{
var arrayOfSolarObjects = (SolarSystemObject[])serializer.Deserialize(fs);
this.solarSystemObjects.Clear();
foreach (var solarObject in arrayOfSolarObjects)
{
this.solarSystemObjects.Add(solarObject);
}
}
}
вернемся на форму, кликнем дважды на пункт меню Файл/Сохранить…, вставим в обработчик код из button2_Click
private void сохранитьToolStripMenuItem_Click(object sender, EventArgs e)
{
var saveFileDialog = new SaveFileDialog();
if (saveFileDialog.ShowDialog() != DialogResult.OK)
return;
var serializer = new XmlSerializer(typeof(SolarSystemObject[]));
using (var fs = new FileStream(saveFileDialog.FileName, FileMode.OpenOrCreate))
{
serializer.Serialize(fs, this.solarSystemObjects.ToArray());
}
}
и снова вернемся на форму и кликнем дважды на пункт меню Данные/Добавить, вставим в обработчик код из button1_Click
private void добавитьToolStripMenuItem_Click(object sender, EventArgs e)
{
var solarObjectForm = new SolarObjectForm();
var solarObject = new SolarSystemObject();
solarObjectForm.SolarObjectField = solarObject;
if (solarObjectForm.ShowDialog() == DialogResult.OK)
{
this.solarSystemObjects.Add(solarObject);
}
}
ура теперь можно запустить и проверить, что менюшка работает корректно.
Добавляем возможность редактировать записи
Добавляем контекстное меню
Я хочу, чтобы была возможность кликнуть на запись в таблице правой кнопкой мыши чтобы открылось контекстное меню и предложило отредактировать ее.
Для этого нам надо добавить элемент контекстного меню, добавляем:
контекстное меню на самом деле тоже меню что и добавленное выше, но только оно вызывается по правому клику мыши. Добавим в него пункт Редактировать:
Если вы случайно закрыли меню, то чтобы его открыть, посмотрите внимательнее: на экране внизу под формой есть невидимые компоненты, просто кликнете на contextMenuStrip1
и меню снова станет доступным для редактирования.
Однако, добавить контекстное меню недостаточно, его еще надо привязать к компоненте. Выделяем dataGridView находим у него свойство ContextMenuStrip и в выпадаюшем списке выбираем contextMenuStrip1:
Давайте запустим программу и проверим что по клику правой кнопкой мыши на табличку, появляется менюшка:
Отлично!
Добавляем обработчик действия Редактировать
Выделим под формой contextMenuStrip1, и в появившемся меню дважды кликнем на Редактировать
private void редактироватьToolStripMenuItem_Click(object sender, EventArgs e)
{
// если в таблице нет активных строк, просто выходим
if (this.dataGridView.CurrentRow == null)
return;
// по номеру активной строки выбираем в списке solarSystemObjects (где содержатся данные таблицы) активный объект
var solarSystemObject = this.solarSystemObjects[this.dataGridView.CurrentRow.Index];
// создаем форму для редактирования
var solarObjectForm = new SolarObjectForm();
// привязываем к ней активный объект
solarObjectForm.SolarObjectField = solarSystemObject;
// открываем новую форму в виде модального окна
solarObjectForm.ShowDialog();
}
Проверяем:
неплохо конечно, но хотелось бы чтобы данные активного объекта заполняли пустые поля новой формы.
Для этого переключимся на новую форму SolarObjectForm.cs дважды щелкнув ее в обозревателе решений, и переключимся на ее код (нажав F7).
Теперь найдем строчку, в которой объявлено поле для хранения объекта
public SolarSystemObject SolarObjectField;
и превратим его в свойство, но не так как мы сделали ли бы это в классе SolarSystemObject:
public SolarSystemObject SolarObjectField {get; set;}
а немного более хитро. Надо понимать, что свойство отличается от поля тем что к нему можно привязать обработчики момента присвоения и чтения значения. Для этого как раз и используются эти непонятые get и set.
И так, я хочу, чтобы когда в главной форме, в методе редактировать пишут:
var solarObjectForm = new SolarObjectForm();
var solarObject = new SolarSystemObject();
solarObjectForm.SolarObjectField = solarObject; // <<< ВОТ В ЭТОТ МОМЕНТ ПРИСВОЕНИЯ ЗНАЧЕНИЯ СВОЙСТВУ
то значение не просто присваивалось полю, но и вызывался метод который заполнит поля новой формы соответствующими значениями.
Возвращаемся с чего начали. В коде SolarObjectForm.cs найдем строчку, в которой объявлено поле для хранения объекта:
public SolarSystemObject SolarObjectField;
и заменим ее следующей конструкцией:
private SolarSystemObject _solarObjectField; // это скрытое поле, в котором будет хранится непосредственно объект
// а это определенно свойство, которое представляет собой своего рода посредника между формой и скрытым полем
// так как этот посредник ничего хранить сам не может он использует для этого скрытое поле
public SolarSystemObject SolarObjectField {
get // когда мы обращаемся к solarObjectForm.SolarObjectField вызывается этот метод
{
return this._solarObjectField; // который просто возвращает скрытое поле
}
set // а этот метод вызывается, когда мы присваиваем значения, например, solarObjectForm.SolarObjectField = solarObject
{
// value -- это ключевое слово, которое хранит переданное свойству значение
// мы присваиваем скрытому полю значение value
this._solarObjectField = value;
// а дальше заполняем поля на форме
txtDayLengthInDays.Text = this._solarObjectField.DayLengthInDays.ToString();
txtYearLengthInDays.Text = this._solarObjectField.YearLengthInDays.ToString();
txtRadius.Text = this._solarObjectField.Radius.ToString();
txtDistanceToTheSun.Text = this._solarObjectField.DistanceToTheSun.ToString();
txtName.Text = this._solarObjectField.Name;
}
}
Я понимаю, что это выглядит как страшное колдунство. И если вам не понятно, не постесняйтесь и спросите меня на паре, я попробую объяснить, могу даже картинку нарисовать… спрашивать можно неограниченное количество раз.
И так, тестируем:
посмотрите внимательнее на гифку, и возможно вы заметите что значение радиуса в табличке изменяется только когда я тыкаю на него мышкой
чтобы строка сразу менялась целиком, надо сообщить таблице что мы редактировали строку. Делается это так.
Переключимся на старую форму и поправим метод редактироватьToolStripMenuItem_Click:
private void редактироватьToolStripMenuItem_Click(object sender, EventArgs e)
{
// ...
solarObjectForm.ShowDialog();
dataGridView.InvalidateRow(this.dataGridView.CurrentRow.Index); // даем команду перерисовать строчку
}
теперь все должно быть ок.
Добавляем возможность удалять записи
Ломать – не строить, но даже чтобы удалить запись придется написать немного кода. Кликаем на наше контекстное меню
добавляем пункт Удалить запись
кликаем на новый пункт меню дважды и пишем:
private void удалитьЗаписьToolStripMenuItem_Click(object sender, EventArgs e)
{
// мы люди вежливые, поэтому сначала спросим, а не ошибся ли пользователь
// тут нам на помощь придет наш старый знакомый MessageBox, с функций Show,
// которой можно передать какой набор кнопок мы хотим видеть на форме, я хочу кнопки Yes и No
var confirmResult = MessageBox.Show("Вы уверены, что хотите удалить запись", "Подтверждение", MessageBoxButtons.YesNo);
if (confirmResult != DialogResult.Yes) // проверяем нажали ли Yes
return;
// удаляем запись
this.solarSystemObjects.RemoveAt(this.dataGridView.CurrentRow.Index);
}
проверяем:
Прекрасно!
Добавляем отчеты
Я буду писать несколько вариантов отчетов. Создадим элемент главного меню “Отчеты” и добавим туда следующие пункты меню
- Планета с самыми длинными сутками
- Суммарный радиус планет
- Поиск планет по названию
- Планеты расстояние до солнца у которых попадает в промежуток
- Поиск ближайшей планеты к указанной
Планета с самыми длинными сутками
Это совсем простой отчет тут просто надо найти взять все данные по планетам упорядочить по удалённости от солнца в порядке убывания и взять первую планету. Кликаем два раза на пункт меню Планета с самыми длинными сутками и пишем:
Способ 1
стандартный подход, по аналоги с поиском максимального значения, только тут фиксируем не просто число, а целый объект если какое-то его число превысило максимальной значение
private void планетаССамымДлиннымДнемToolStripMenuItem_Click(object sender, EventArgs e)
{
if (this.solarSystemObjects.Count == 0) // чтобы не падала программа если табличка пуста
return;
// предполагаем, что планет с наибольшей продолжительностью суток, это первая планета в списке
var planet = this.solarSystemObjects[0];
// в цикле по всем объектам
foreach(var p in this.solarSystemObjects)
{
// если у очередной планеты в списке продолжительность больше чем у той которую считаем за максимальную
if (planet.DayLengthInDays > p.DayLengthInDays)
{
// то меняем максимальную планету на другую
planet = p;
}
}
MessageBox.Show("Планета с наибольшей продолжительностью суток это " + planet.Name);
}
Способ 2 (функциональный подход)
private void планетаССамымДлиннымДнемToolStripMenuItem_Click(object sender, EventArgs e)
{
if (this.solarSystemObjects.Count == 0)
return;
var planet = this.solarSystemObjects
.OrderBy(x => -x.DayLengthInDays) // упорядочиваем по продолжительности дня в порядке убывания
.First(); // берем первый элемент в упорядоченном списке
MessageBox.Show("Планета с наибольшей продолжительностью суток это " + planet.Name);
}
Способ 3 (LINQ подход)
private void планетаССамымДлиннымДнемToolStripMenuItem_Click(object sender, EventArgs e)
{
if (this.solarSystemObjects.Count == 0)
return;
var planet = ( // типа SQL запроса
from p in this.solarSystemObjects // откуда берем
orderby p.DayLengthInDays descending // как упорядочиваема
select p // в конце надо обязательно чтобы select был
).First(); // берем только первый элемент
MessageBox.Show("Планета с наибольшей продолжительностью суток это " + planet.Name);
}
Суммарный радиус планет
Способ 1
простой расчёт суммы
private void суммарныйРадиусПланетToolStripMenuItem_Click(object sender, EventArgs e)
{
float sumRadius = 0;
foreach (var planet in this.solarSystemObjects)
{
sumRadius += planet.Radius;
}
MessageBox.Show("Суммарный радиус планет " + sumRadius);
}
Способ 2 (функциональный)
private void суммарныйРадиусПланетToolStripMenuItem_Click(object sender, EventArgs e)
{
var sumRadius = this.solarSystemObjects
.Select(x => x.Radius) // этой строчкой список планет превращается в список радиусов
.Sum(); // суммируем список радиусов
MessageBox.Show("Суммарный радиус планет " + sumRadius);
}
Способ 3 (LINQ подоход)
private void суммарныйРадиусПланетToolStripMenuItem_Click(object sender, EventArgs e)
{
var sumRadius = (
from p in this.solarSystemObjects // запрос, откуда берем
select p.Radius // и чего берем
).Sum(); // суммируем
MessageBox.Show("Суммарный радиус планет " + sumRadius);
}
Поиск планет по названию
Тут я хочу организовать поиск не просто по названию, а именно по части названия, и чтобы в результате мне выдавался список названий планет, которые удовлетворяют поисковой строке. Для этого, в отличии, от предыдущих запросов мне понадобится запросить чего-нибудь у пользователя. Для этого придется добавить новую форму. Добавляем
называем ее SearchByTestForm.cs
оформляем как на картинке:
- у TextBox, свойство (Name): txtSearch
- у кнопки Найти свойство DialogResult: Ok
- у кнопки Отмена свойство DialogResult: Cancel
переключимся на код этой формы (нажмем F7), и добавил прокси-свойство, через которое можно будет узнать введенное пользователем значение в txtSearch. Сделать это необходимо так как доступ к компонентам формы ограничен из вне.
Пишем:
namespace WindowsFormsApp8
{
public partial class SearchByTextForm : Form
{
public string SearchText // добавили свойство
{
get // при обращении к этому свойству получим содержимое текста в txtSearch
{
return txtSearch.Text;
}
}
public SearchByTextForm()
{
InitializeComponent();
}
}
}
Вернемся к нашей старой форме и кликаем дважды на пункт меню Поиск планеты по названию. И теперь уже напишем отчет
Способ 1
private void поискПланетыПоНазваниюToolStripMenuItem_Click(object sender, EventArgs e)
{
// так как мы не просто закрываем форму, а на самом деле просто прячем ее, когда нажимаем OK
// чтобы она в памяти не висела, используем конструкцию using,
// которая гарантирует удаление формы из памяти по завершению функции
using(var form = new SearchByTextForm())
{
if (form.ShowDialog() != DialogResult.OK) // проверяем что нажали OK
return;
var planets = new List<string>();
foreach(var planet in this.solarSystemObjects)
{
if (planet.Name.Contains(form.SearchText)) // если название содержит введенный текст
{
planets.Add(planet.Name); // добавляем название в список
}
}
// выводим список названий планет
// тут я использую функцию String.Join, у которой два параметра разделитель
// я указал "\n" -- это перенос на новую строку
// а второй параметр — это список значений, я передаю
// список планет удовлетворяющих поисковой строке
// ну а дальше просто вывожу все на экран
MessageBox.Show(String.Join("\n", planets));
}
}
Способ 2 (функциональный подход)
private void поискПланетыПоНазваниюToolStripMenuItem_Click(object sender, EventArgs e)
{
using(var form = new SearchByTextForm())
{
if (form.ShowDialog() != DialogResult.OK)
return;
var planets = this.solarSystemObjects
.Select(x => x.Name)
.Where(x => x.Contains(form.SearchText));
MessageBox.Show(String.Join("\n", planets));
}
}
Способ 3 (LINQ подоход)
private void поискПланетыПоНазваниюToolStripMenuItem_Click(object sender, EventArgs e)
{
using(var form = new SearchByTextForm())
{
if (form.ShowDialog() != DialogResult.OK)
return;
var planets = from p in this.solarSystemObjects
where p.Name.Contains(form.SearchText)
select p.Name;
MessageBox.Show(String.Join("\n", planets));
}
}
можно запустить и проверить (например, поиск планет содержащих букву “а”)
Планеты расстояние до солнца у которых попадает в промежуток
Тут нам опять понадобится новая форма, на которой можно будет ввести целых два значения, и так добавляем:
назовем ее RangeForm.cs
добавляем элементы на форму:
- у первого TextBox, свойство (Name): txtFrom
- у второго TextBox, свойство (Name): txtTo
- у кнопки Ok свойство DialogResult: Ok
- у кнопки Отмена свойство DialogResult: Cancel
переключимся на код формы и добавим два прокси-свойства, одно проксирует значение От другое значение До
//...
namespace WindowsFormsApp8
{
public partial class RangeForm : Form
{
public float FromValue // свойство преобразует значение в txtForm в число и возвращает результат
{
get
{
float result;
if (float.TryParse(txtFrom.Text, out result))
return result;
return 0; // если преобразовать не удалось, возвращаем 0
}
}
public float ToValue // свойство преобразует значение в txtTo в число и возвращает результат
{
get
{
float result;
if (float.TryParse(txtTo.Text, out result))
return result;
return 0; // если преобразовать не удалось, возвращаем 0
}
}
public RangeForm()
{
InitializeComponent();
}
}
}
теперь снова вернемся на главную форму Form.cs, найдем в меню пункт Планеты расстояние до солнца у которых попадает в промежуток и дважды кликнем на него дважды чтобы добавить обработчик. И напишем наш отчет.
Способ 1
Тут по сути тот же код что и в запросе выше, только тут я уже использую два прокси-свойства формы (form.FromValue и form.ToValue), ну и условие немного посложнее:
private void планетыРасстояниеДоСолнцаУКоторыхПопадаетВПромежутокToolStripMenuItem1_Click(object sender, EventArgs e)
{
using (var form = new RangeForm())
{
if (form.ShowDialog() != DialogResult.OK)
return;
var planets = new List<string>();
foreach(var p in this.solarSystemObjects)
{
if (form.FromValue < p.DistanceToTheSun && p.DistanceToTheSun <= form.ToValue)
{
planets.Add(p.Name);
}
}
MessageBox.Show(String.Join("\n", planets));
}
}
Способ 2 (функциональный подход)
private void планетыРасстояниеДоСолнцаУКоторыхПопадаетВПромежутокToolStripMenuItem1_Click(object sender, EventArgs e)
{
using (var form = new RangeForm())
{
if (form.ShowDialog() != DialogResult.OK)
return;
var planets = this.solarSystemObjects
.Where(p => form.FromValue < p.DistanceToTheSun && p.DistanceToTheSun <= form.ToValue)
.Select(p => p.Name);
MessageBox.Show(String.Join("\n", planets));
}
}
Способ 3 (LINQ подход)
private void планетыРасстояниеДоСолнцаУКоторыхПопадаетВПромежутокToolStripMenuItem1_Click(object sender, EventArgs e)
{
using (var form = new RangeForm())
{
if (form.ShowDialog() != DialogResult.OK)
return;
var planets = from p in this.solarSystemObjects
where form.FromValue < p.DistanceToTheSun && p.DistanceToTheSun <= form.ToValue
select p.Name;
MessageBox.Show(String.Join("\n", planets));
}
}
проверяем:
Поиск ближайшей планеты к указанной
Тут снова понадобится новая форма. Я хочу, чтобы на новой форме присутствовал выпадающий список с названиями планет. Чтобы можно было выбрать планету, нажать OK и получить в ответ название ближайшей планеты.
Добавляем форму:
назовем ее ComboBoxForm.cs
добавляем элементы на форму:
- элемент типа ComboBox, свойство (Name): cmbPlanets
- у кнопки Ok свойство DialogResult: Ok
- у кнопки Отмена свойство DialogResult: Cancel
Давайте добавим прокси-свойство, которое будет возвращать выбранное в cmbPlanets значение:
namespace WindowsFormsApp8
{
public partial class ComboBoxForm : Form
{
public string SelectedPlanet
{
get
{
return cmbPlanets.SelectedValue.ToString();
}
}
public ComboBoxForm()
{
InitializeComponent();
}
}
}
теперь переключимся на главную форму, найдем пункт меню Поиск ближайшей планеты к указанной и кликнем на него дважды, чтобы добавить обработчик события, пропишем туда следующий код:
private void поискБлижайщейПланетыКУказанойToolStripMenuItem_Click(object sender, EventArgs e)
{
using(var form = new ComboBoxForm())
{
if (form.ShowDialog() != DialogResult.OK)
return;
// найдем планету в списке у которой название совпадает с выбранным
var planet = this.solarSystemObjects
.Where(p => p.Name == form.SelectedPlanet)
.FirstOrDefault(); // этот метод в отличии от First() не будет выдавать ошибки если планеты не найдется, а просто вернет null
// если такая нашлась
if (planet != null)
{
var nearestPlanet = this.solarSystemObjects
.Where(p => p.Name != planet.Name) // чтоб планету с самой собой не сравнивать, а то так получается что ближайшая планета к Земле это Земля
.OrderBy(p => Math.Abs(planet.DistanceToTheSun - p.DistanceToTheSun)) // упорядочить в порядке удаления
.First(); // выбрать первое значение
MessageBox.Show("Ближайшая планет: " + nearestPlanet.Name);
}
}
}
пробуем запустить:
кто бы мог подумать, у нас пустой список…
В общем чтобы список заполнить надо форме как-нибудь передать названия планет. Для этого воспользуемся конструктором.
Переключимся на форму ComboBoxForm, перейдем в ее код и заменим
namespace WindowsFormsApp8
{
public partial class ComboBoxForm : Form
{
// ...
public ComboBoxForm() // <<< ВОТ ЭТОТ КОНСТРУКТОР
{
InitializeComponent();
}
}
}
на
namespace WindowsFormsApp8
{
public partial class ComboBoxForm : Form
{
// ...
public ComboBoxForm(List<string> planetNames) // <<< добавили список названий в качестве аргумента функции
{
InitializeComponent();
this.cmbPlanets.DataSource = planetNames; // привязываем переданный список к выпадающему списку
}
}
}
снова возвращаемся на главную форму в обработчик поискБлижайщейПланетыКУказанойToolStripMenuItem_Click. Правим его следующим образом:
private void поискБлижайщейПланетыКУказанойToolStripMenuItem_Click(object sender, EventArgs e)
{
// формирую список названий планет
var planetNames = this.solarSystemObjects
.Select(p => p.Name)
.OrderBy(p => p) // по алфавиту упорядочиваем
.Distinct() // только уникальные значения, ну вдруг у вас будет две планеты с одинаковым названием
.ToList();
using(var form = new ComboBoxForm(planetNames)) // передаю список планет в конструктор new ComboBoxForm(planetNames)
{
// остальное не трогаем
// ...
}
}
запускаем и проверяем
Тут уж не буду все три варианта расписывать, думаю и все так понятно.
Собственно, и все. Этого должно хватить чтобы написать код по курсовой.