ПОЛНОЕ ЗАДАНИЕ В ДЕМО ФАЙЛЕ,
ЧАСТЬ ДЛЯ ПОИСКА ДУБЛИРУЮ НИЖЕ
Приложение должно включать следующие функции: открытие (создание в случае его отсутствия), сохранение файла, добавление, изменение полей записи, удаление записи, просмотр содержимого файла, обработка запросов. Выбор действия осуществляется посредством нажатия кнопки или выбора того или иного пункта меню. При выполнении данного задания следует показать умение пользоваться изученными компонентами[1].
Данные следует обрабатывать, используя переменную типа struct, структура которой соответствует варианту задания (использовать текстовый файл). Для отображения данных использовать компонент dataGridView.
Следует учитывать, что в dataGridView для доступа к конкретной ячейке сначала указывается столбец, а потом строка (счет с 0).
Задача:
Исходный файл содержит список пассажирских перевозок в формате «Транспорт; пункт отправки; пункт назначения; стоимость билета» (произвольное количество строк):
Необходимо вычислить:
количество автобусных рейсов;
среднюю стоимость билета по каждому виду транспорта;
перечислить все виды транспорта, следующие в заданный населенный пункт.
Пример выполнения задачи
Сведения о товарах на складе имеют следующую структуру:
-наименование товара;
-цена за единицу;
- cтрана- производитель
Определить, какой товар(ы) имеет наибольшую стоимость, и какая страна произвела больше всего товаров
Информационная модель задачи
Для долговременного хранения данных будем использовать текстовые файлы. В качестве входной информации будем использовать структуру данных:
Сведения о товарах будем хранить в структуре[2] с именем tovar. Эта структура будет содержать следующие поля:
nametov- наименование товара;
cena- цена за единицу;
strana- страна-производитель товара
Для записи элементов структуры в текстовый файл будем поля структуры разделять некоторым условным разделителем (например, ;). Для этого внутри структуры перегрузим метод ToString(). Для перегрузки используется ключевое слово override[3]. Тогда описание структуры будет выглядеть так:
Интерполированной строкой в C# называется строка, перед которой расположен символ $. Такая интерполированная строка может включать выражения, которые заключены в фигурные скобки:
Сведения о товарах будем хранить в файле, полный путь к которому указывается в глобальной переменной path. Пользователю не нужно получать непосредственный доступ к файлам, содержащим данные, поскольку для всех манипуляций с данными будет разработан соответствующий интерфейс.
Также для того, чтобы манипулировать данными, хранящимися в файле (а там хранятся преобразованные в строки структуры), данные из файла будут «выгружаться» в список List, который следует описать в разделе глобальных переменных:
List<tovar> a = new List<tovar>();
Для работы с файлами используются следующие стандартные классы и объекты:
Класс FileStream представляет возможности по считыванию из файла и записи в файл. Он позволяет работать как с текстовыми файлами, так и с бинарными.
Создание FileStream
Для создания объекта FileStream можно использовать как конструкторы этого класса, так и статические методы класса File. Конструктор FileStream имеет множество перегруженных версий, из которых будем использовать самую простую и используемую:
FileStream(string filename, FileMode mode)
Здесь в конструктор передается два параметра: путь к файлу и перечисление FileMode. Данное перечисление указывает на режим доступа к файлу и может принимать следующие значения:
Append: если файл существует, то текст добавляется в конец файл. Если файла нет, то он создается. Файл открывается только для записи.
Create: создается новый файл. Если такой файл уже существует, то он перезаписывается
CreateNew: создается новый файл. Если такой файл уже существует, то приложение выбрасывает ошибку
Open: открывает файл. Если файл не существует, выбрасывается исключение
OpenOrCreate: если файл существует, он открывается, если нет - создается новый
Truncate: если файл существует, то он перезаписывается. Файл открывается только для записи.
Для работы непосредственно с текстовыми файлами в пространстве System.IO определены специальные классы: StreamReader и StreamWriter.
Запись в файл и StreamWriter
Для записи в текстовый файл используется класс StreamWriter. Некоторые из его конструкторов, которые могут применяться для создания объекта StreamWriter:
StreamWriter(string path): через параметр path передается путь к файлу, который будет связан с потоком
StreamWriter(string path, bool append): параметр append указывает, надо ли добавлять в конец файла данные или же перезаписывать файл. Если равно true, то новые данные добавляются в конец файла. Если равно false, то файл перезаписывается заново
StreamWriter(string path, bool append, System.Text.Encoding encoding): параметр encoding указывает на кодировку, которая будет применяться при записи
Свою функциональность StreamWriter реализует через следующие методы:
int Close(): закрывает записываемый файл и освобождает все ресурсы
void Flush(): записывает в файл оставшиеся в буфере данные и очищает буфер.
void Write(string value): записывает в файл данные простейших типов, как int, double, char, string и т.д. Соответственно имеет ряд перегруженных версий для записи данных элементарных типов, например, Write(char value), Write(int value), Write(double value) и т.д.
void WriteLine(string value): также записывает данные, только после записи добавляет в файл символ окончания строки
Чтение из файла и StreamReader
Класс StreamReader позволяет легко считывать весь текст или отдельные строки из текстового файла.
Некоторые из конструкторов класса StreamReader:
StreamReader(string path): через параметр path передается путь к считываемому файлу
StreamReader(string path, System.Text.Encoding encoding): параметр encoding задает кодировку для чтения файла
Среди методов StreamReader можно выделить следующие:
void Close(): закрывает считываемый файл и освобождает все ресурсы
int Peek(): возвращает следующий доступный символ, если символов больше нет, то возвращает -1
int Read(): считывает и возвращает следующий символ в численном представлении. Имеет перегруженную версию: Read(char[] array, int index, int count), где array - массив, куда считываются символы, index - индекс в массиве array, начиная с которого записываются считываемые символы, и count - максимальное количество считываемых символов
string ReadLine(): считывает одну строку в файле
string ReadToEnd(): считывает весь текст из файла
Но для использования классов в тексте кода (в самом верху) каждого создаваемого модуля к формам вашего приложения ещё нужно будет добавить строчку:
using System.IO;
Структура программы, созданной с помощью C#, несколько отличается от традиционной, что, в свою очередь, обусловливает особенности создания подобных программ. Подобную программу можно представить себе, состоящей из двух частей: интерфейса – части программы, предназначенной для ввода в программу и вывода из нее информации, и собственно операторов, предназначенных для непосредственного решения задачи. В своей работе программа взаимодействует с операционной системой, различными внешними устройствами, базами данных, файлами, другими программами и т.п.
Интерфейс представляет собой совокупность объектов, с помощью которых осуществляется передача информации в соответствующем направлении, – формы, диалоговые окна, элементы управления и т.д. Во многом выбранный интерфейс определяет структуру всей программы.
Так как объекты в программе являются достаточно автономными, для передачи информации между ними, а также между программой и операционной системой, программой и внешними устройствами и т.д. используется система сообщений. Обычно программа после своего запуска находится в режиме ожидания очередного сообщения, при появлении которого программа анализирует его, определяет, какие действия следует выполнить, и выполняет их, после чего ожидает следующее сообщение. Так как при появлении различных сообщений программа, как правило, выполняет различные действия, все исполняемые операторы представляют собой совокупность подпрограмм, оформленных в виде тех или иных методов различных объектов.
Приложение – основной связующий объект проекта, организующий цикл по обработке поступающих сообщений.
Форма – интерфейсный элемент, посредством которого осуществляется обмен информацией между приложениями и внешними устройствами. Формы размещаются в приложении, а само приложение может содержать одну или несколько форм.
Компоненты – объекты, размещаемые на формах и предназначенные для выполнения тех или иных операций по отображению, получению и преобразованию информации.
Работа с формами
Как известно, существуют информационные окна (формы) и диалоговые. Информационное окно предназначено в основном для вывода информации пользователю. Диалоговые окна – специальные формы, предназначенные для организации диалога пользователя с программой посредством этого окна. Здесь возможна двусторонняя передача информации – как программе, так и пользователю.
Окна могут быть также модальными и немодальными. При открытии окна в немодальном режиме, возможен переход на окно, его вызвавшее, и на другие. При вызове окна в модальном режиме невозможен переход на другие окна до тех пор, пока не закроется модальное окно.
Вызов модального окна осуществляется с помощью метода ShowDialog():
Имя_формы. ShowDialog();
Немодальное окно вызывается методом Show.
С каждой формой связан модуль (*.cs). Определив глобальные переменные, например, в главной форме, их можно сделать доступными в дочерних окнах.
Для получения данных из созданного файла, следует открыть файл (если он существует!), прочитать данные из него, добавить их в список с идентификатором а и выгрузить их в таблицу dataGridView. Поскольку эти действия выполняются единственный раз, их следует выполнить при выгрузке формы.
Путь к файлу сохраним в глобальной переменной path
string path = "c:\\temp\\tt.dat";
Тогда обработчик события[4], возникающего при открытии формы, может выглядеть так:
Алгоритм решения задачи
Теперь пришло время наделить проектируемый интерфейс инструментарием добавления сведений о новых товарах, изменения сведений о товарах, уже введённых, и удаления сведений, ставших ненужными.
Реализация интерфейса может быть выполнена на новом, созданном с этой целью окне, но также можно разместить всю конструкцию и на том же окне, где происходило проектирование до сих пор. Предлагается реализовать вариант "на том же окне". Для этого надо принять решение, в какой области окна будут располагаться элементы управления и элементы ввода значений. Если ориентироваться на дизайн окна на иллюстрации, то:
§ кнопки инициации действий рекомендуется справа от таблицы в контейнер, в нём разместить весь спектр необходимых кнопок для управления (пользователем) процессом модификации данных
§ для элементов ввода значений предлагается место справа от dataGridView, куда помещается GroupBox1, а в нём - требуемые элементы: 3 textbox и необходимое количество label для указания названий полей ввода.
Скриншот того, как форма выглядит в проекте, представлен на иллюстрации ниже:
Представленный вид совершенно не означает, что форма так и будет выглядеть, когда приложение будет запущено: абсолютно не обязательно показывать GroupBox2 и его содержимое, пока пользователь не решит добавить или изменить данные
Далее выполняется последовательность действий по наполнению формы оговорёнными элементами:
· toolStrip1-контейнер с кнопками
Поместить на форму объект toolStrip1. Он автоматически расположится ("прилипнет") в верхней части формы. Это, вообще-то его "законное место", но не в этот раз. Чтобы этот контейнер можно было располагать в произвольном месте формы, надо изменить значение его свойства Dock на None, потом перетащить куда положено и подогнать размеры.
Наполнить контейнер кнопками (и разделителями). Выполняется выбором соответствующего элемента (Button <-> Separator) из выпадающего меню на поверхности toolStrip1.
Настоятельно рекомендуется присвоить вменяемые имена всем кнопкам (например, btnadd, btned, btndel, btnsave, btnundo). Кроме того, кнопки следует оснастить всплывающими подсказками, хранящимися в свойстве ToolTipText (например, ""Добавить товар" для btnadd).
Для указания картинок на кнопках понадобится изменить свойство Image кнопки. -
Нажать на кнопку Импорт, выбрать папку Basic - найти нужную картинку - нажать <Выбрать> - итерация.
По логике работы приложения не все компоненты должны быть видимыми или доступными в конкретный момент. Так, можно обозначить два основных режима отображения компонентов на экране (и один дополнительный - режим удаления, о котором речь позже):
♦ режим просмотра – GroupBox1 невидим, кнопки "Сохранить" и "Отменить" недоступны;
♦ режим редактирования – GroupBox1 видим, кнопки "Добавить" и "Изменить" "Удалить" недоступны;
Нетрудно обнаружить, что основные режимы инвертированы по отношению друг другу: там, где true для одного, там false для другого. Это позволяет использовать одну процедуру при "переключении" интерфейса по режимам, которой требуется передать как значение параметра информацию о том, под какой режим "выстроить" окно. Для определённости значение "да" (true) будет значить режим изменения; "нет" (false) - просмотра. Тип параметра - очевидно - следует объявить булевого типа. Итак, процедура[5] преобразования окна из одного вида в другой и обратно будет иметь схематично такой вид:
Следует обратить внимание, что в объявлении параметра процедуры fl ему устанавливается значение по умолчанию, то есть при вызове процедуры со значением параметра true, параметр вообще можно не указывать. Кстати, а в каком режиме построится окно, если процедуру вызвать без параметра: visy();?
Теперь:
- в обработчик события активации формы добавляется вызов созданной процедуры с тем значением параметра, который оформляет режим просмотра: таким способом приложение откроется, показывая адекватный режим;
visy(false);
- также в режим просмотра должна вернуться форма при (обработчик события) использовании кнопки "Отменить" ...
- и после сохранения "очередного изменения".
И обратно:
- в режим изменения интерфейс следует переключить при щелчке по кнопке (обработчик события) "Добавить" и ...
- по кнопке "Изменить"
Первая операция, которая теперь будет описана: добавление новой записи.
Некоторые пояснения перед описанием конкретных процедур корректировки данных созданного отношения.
Кнопки, которые обозначены в приложении как "Добавить" и "Изменить", в действительности ничего не добавляют и не изменяют. Они лишь подготавливают интерфейс, открывая и очищая (заполняя) поля для ввода данных. Собственно, исполнение команды добавления или изменения инициируется нажатием кнопки "Сохранить". Эта кнопка одна, а использовать её предполагается и после "Добавить" и после "Изменить". Возникает неопределённость: какую же именно операцию из двух должно выполнить приложение по кнопке "Сохранить"? Значит, при нажатии кнопок "Добавить" и "Изменить" нужно в некоторой переменной запомнить, какая именно кнопка была нажата.
Пусть это будет глобальная переменная
short rej;
В обработчики же кнопок "Добавить" и "Изменить" следует разместить присвоение переменной rej
rej= 1;
и
rej= 2;
соответственно. Теперь, в обработчике кнопки "Сохранить" значение переменной rej позволит[6] однозначно определить, какой кнопкой воспользовался пользователь, начиная корректировку.
А вот кнопка "Удалить" не требует от пользователя дополнительных манипуляций со значениями данных в полях ввода. Поэтому предъявлять группбокс смысла не имеет, но задать вопрос после нажатия этой кнопки для подтверждения такой серьёзной операции нужно: вдруг это нажатие случайно, а пользователь хотел "изменить" запись.
Следовательно, в обработчике нажатия кнопки "Удалить" надо:
§ показать окно с запросом о подтверждении команды и выйти из процедуры при отказе от намерений
§ присвоить: rej= 3;
Самое время оформить обработчик нажатия OnClick кнопки btnsave ("Сохранить"). Скелет процедуры можно представить следующим образом:
private void toolbtnsave_Click(object sender, EventArgs e)
{
if (rej == 1)
{// добавление записи
...
};
if (rej == 2) // изменение записи
{
}
}
Добавление записи
Начало программирования операций, естественно начать с операции добавления нового товара. Но сначала - несколько суждений и практических рекомендаций касательно последовательности действий, которые следует предпринять для изменения упомянутого значения:
· Поля ввода значений атрибутов при поступлении команды добавления, очевидно, необходимо очистить. Значит, в обработчик кнопки "Добавить" надо дописать очищаюший эти поля ввода[7] код следующего вида:
textBox1.Text = "";
...
А также добавить следующий фрагмент кода:
visy();
rej = 1;
Самое время для отладки кусочка программы, реализующего добавление новых товаров. Рекомендуется добавить не менее двух, отследив очистку полей после первого добавления.
Редактирование записи
Следующая операция, предлагаемая к реализации, - операция изменения существующей записи (то есть хранимых о товаре сведениях). И опять, сначала - несколько предварительных рекомендаций:
· Прежде чем предпринимать какие-либо действия, следует убедиться, а есть ли товар, который выбран для корректировки (в списке товары вообще есть)? Это позволит избежать неадекватной реакции приложения[8] при использовании кнопки "Обновить", в частности, на пустом списке в dataGridView или, когда "текущая позиция" ниже последней в списке. Итак, первыми командами обработчика btned должны быть, в простом варианте:
Visy();
rej = 2;
if (dataGridView1.Rows.Count == 0) return;
- в этом случае поведение приложения не будет "прозрачным" - нажатие btned не будет вызывать какой-либо зримой реакции интерфейса, что пользователя сподвигнет на "ласковые" слова в адрес "дактожэтосделал-то" [- try it! :) -].
Вариант другой, с реакцией:
if (dataGridView1.Rows.Count == 0){ MessageBox.Show(...);
// реакцию приложения на нелогичные действия пользователя предлагается сочинить самим
return;};
· Поля ввода значений при поступлении команды изменения, очевидно, необходимо заполнить значениями соответствующих атрибутов выбранной записи[9]. Значит, в обработчик кнопки "Изменить" надо дописать код, присваивающий значения этим полям ввода. А для этого нужно узнать, какую же запись мы выбрали? Очевидно, следует получить номер строки выделенной ячейки и запомнить его в глобальную целочисленную переменную index. Это можно сделать с помощью свойства CurrentRow.Index текущего dataGridView.
Отредактированные сведения нужно сохранить не только в списке, но и в файле. Поскольку это нужно будет делать дважды (еще при удалении записи), целесообразно перезапись файла оформить в виде метода SaveFile:
Всё готово, можно в место, определённое выше в "скелете" для изменения (в процедуре обработчика события нажатия на кнопку btnsave), вставить и отладить оператор "обновления" записи:
Удаление записи
Операция удаления записи структурно близка операции обновления, только не требует никаких значений для какого-либо присвоения.
· Обработчик же кнопки btndel принципиально отличается от обработчиков предыдущих кнопок добавления и обновления, для которых сам процесс сохранения инициируется дополнительной кнопкой "Сохранить", так как перед сохранением необходимы и ожидаются манипуляции пользователя по вводу (обновлению) значений. Здесь, в обработчике щелчка по кнопке BtnDel, достаточно:
· проверить, есть ли товар для удаления (см. выше обсуждение наличия записи для режима обновления);
· вызвать окно подтверждения намерений методом MessageBox.Show, который должен показывать сообщение и две кнопки:
Кстати, на этой иллюстрации "активной" является (она выделена и "сработает" на пробел) кнопка <Нет>, позволяющая ещё раз попытаться защитить пользователя от машинальной команды на исполнение серьёзной операции...
· проверить по закрытии окна сообщения, какая именно из двух была нажата
· при отказе от намерений выйти из процедуры
Для вывода сообщения в классе MessageBox предусмотрен метод Show, который имеет различные версии и может принимать ряд параметров. Рассмотрим одну из наиболее используемых версий:
public static DialogResult Show(
string text,
string caption,
MessageBoxButtons buttons,
MessageBoxIcon icon,
MessageBoxDefaultButton defaultButton,
MessageBoxOptions options
)
Здесь применяются следующие параметры:
text: текст сообщения
caption: текст заголовка окна сообщения
buttons: кнопки, используемые в окне сообщения. Принимает одно из значений перечисления MessageBoxButtons:
· AbortRetryIgnore: три кнопки Abort (Отмена), Retry (Повтор), Ignore (Пропустить)
· OK: одна кнопка OK
· OKCancel: две кнопки OK и Cancel (Отмена)
· RetryCancel: две кнопки Retry (Повтор) и Cancel (Отмена)
· YesNo: две кнопки Yes и No
· YesNoCancel: три кнопки Yes, No и Cancel (Отмена)
Таким образом, в зависимости от выбора окно сообщения может иметь от одной до трех кнопок.
icon: значок окна сообщения. Может принимать одно из следующих значений перечисления MessageBoxIcon:
· Asterisk, Information: значок, состоящий из буквы i в нижнем регистре, помещенной в кружок
· Error, Hand, Stop: значок, состоящий из белого знака "X" на круге красного цвета.
· Exclamation, Warning: значок, состоящий из восклицательного знака в желтом треугольнике
· Question: значок, состоящий из вопросительного знака на периметре круга
· None: значок у сообщения отсутствует
defaultButton: кнопка, на которую по умолчанию устанавливается фокус. Принимает одно из значений перечисления MessageBoxDefaultButton:
· Button1: первая кнопка из тех, которые задаются перечислением MessageBoxButtons
· Button2: вторая кнопка
· Button3: третья кнопка
options: параметры окна сообщения. Принимает одно из значений перечисления MessageBoxOptions:
· DefaultDesktopOnly: окно сообщения отображается на активном рабочем столе.
· RightAlign: текст окна сообщения выравнивается по правому краю
· RtlReading: все элементы окна располагаются в обратном порядке справа налево
· ServiceNotification: окно сообщения отображается на активном рабочем столе, даже если в системе не зарегистрирован ни один пользователь
Однако нам не просто дается возможность установки кнопок в окне сообщения. Метод MessageBox.Show() возвращает объект класса DialogResult, с помощью которого мы можем узнать, какую кнопку в окне сообщения нажал пользователь. DialogResult представляет перечисление, в котором определены следующие значения:
· Abort: нажата кнопка Abort
· Retry: нажата кнопка Retry
· Ignore: нажата кнопка Ignore
· OK: нажата кнопка OK
· Cancel: нажата кнопка Cancel
· None: отсутствие результата
· Yes: нажата кнопка Yes и No
· No: нажата кнопка No
На этом - после отладки операции удаления - функционал "ведения" данных в основном можно считать реализованным.
Для определения суммарной[10] (или средней) стоимости/количества по каждой группе целесообразно использовать структуру Dictionary (словарь), представляющую собой множество пар «ключ- значение». Очевидно, ключом пары будет наименование той или иной группы, а значением – сумма/количество для каждой группы.
В случае определения среднего значения какого-либо количественного признака для каждой группы необходимо суммировать не только значения признака, но и количество встреченных записей для каждой группы. Поэтому в этом случае значением элементов словаря будет структура, содержащая 2 поля: суммарное значение признака и его количество для каждой группы, по которой требуется подвести итог.
Приведем конкретный пример
Исходный файл содержит список книг в формате «Название книги, жанр, цена».
Необходимо вычислить:
среднюю стоимость книг в каждом жанре.
Текстовый файл может выглядеть, например, так:
Ещё многое имею сказать вам, но вы
теперь не можете вместить…
(Евангелие от Иоанна, 16:12)
.
[2] Или можно использовать класс
[3] override значит, что помеченный метод — новая версия родительского и должен использоваться вместо него
[4] Надеюсь, что все знают, что писать слова private void Form_Load не следует, а надо воспользоваться кнопкой «События» в окне свойств у Form1
[5] Правильнее ее называть методом класса Form1 (да-да, это ООП…)
[6] «Однозначность» обеспечивает логика механизма предоставления доступа к кнопкам.
[7] ...от старых значений, возможно оставшихся от предыдущего добавления или изменения.
[8] Извините за выражение, "защита от дурака", - защита от действий, которые не являются логичными и - в общем - не ожидаются. Качественно написанный программный код все подобные ситуации обрабатывать должен адекватно.
[9] Например, мышью эту запись выделили (а как ещё?)
[10] «Чему равно один плюс один плюс один плюс один плюс один плюс один плюс один плюс один плюс
один плюс один?» «Не знаю, – сказала Алиса. – Я сбилась со счёта!» «Да она не умеет складывать!!!» (Л. Кэр-
ролл «Алиса в Зазеркалье»)