Решаем типовые задачи на C# с помощью Windows Forms
В последнее время здесь был ряд заметок по работе с приложениями Windows Forms на C#, все вместе их можно рассматривать как набросок небольшого курса вроде "Технологии программирования, часть 1", изучаемого, когда "Языки программирования" в лице основ C++ и C# уже "пройдены":
1. Введение и основы Windows Forms
2. Взаимодействие форм. MDI-приложения
3. Работа со строками и списочными компонентами
4. Табличные компоненты и работа с ними
5. Графика (отрисовка, работа с изображениями)
6. Графика (работа с Chart-компонентами и анимация в .NET)
7. Интеграция с офисными приложениями
В этой заметке мы обобщаем, "чему должны были научиться" и приводим ряд типовых задач, которые имеет смысл ставить перед неофитами при контроле знаний.
В настоящее время нами освоены следующие возможности библиотеки .NET:
- управление приложением и компонентами с помощью событий и свойств;
- создание прикладных приложений и вычислительная обработка данных на основе базовых компонент (Panel, Button, Label, TextBox);
- списочные и табличные компоненты, обработка информации с их помощью (ListView, ListBox, ComboBox, DataGridView, DataGrid);
- работа с графической канвой и таймером, управление отображением и перемещением графических объектов (PictureBox, Graphics, Timer);
- работа с текстовыми и структурированными файлами средствами .NET;
- динамическое создание/удаление компонентов, управление дочерними объектами.
Как правило, в зачётной или экзаменационной работе по подобному курсу задач на "конкретные компоненты" нет (если тип компоненты отдельно не оговорен в условии), а задача состоит в том, чтобы написать работающее приложение согласно поставленному условию и с применением компонент, которые кажутся вам наиболее подходящими для этой цели.
Проект Ex8_1. Реализовать отсортированный по алфавиту список имён с поддержкой операций добавления и удаления элементов, сохранением списка строк в файл и загрузкой его из файла. Допустимые символы в именах – буквы и цифры.
Задачу можно решать как на основе списочных, так и табличных компонент. Поскольку требуется сортировка данных, удобнее решать на основе какого-либо списка, имеющего встроенное свойство Sorted. Форма будет иметь следующий вид:

Повторите, какие свойства формы вы поменяли, чтобы получить такой вид окна?
Справа расположена panel1
со свойством Dock = Right
, на ней 4 кнопки button1
, ..., button4
для выполнения предусмотренных задачей действий. Слева – список comboBox1
со свойством Dock = Fill
.
По загрузке формы настроим список для нашей задачи:
private void Form1_Load (object sender, EventArgs e) { comboBox1.Sorted = true; //список будет сортироваться comboBox1.DropDownStyle = ComboBoxStyle.Simple; //развёрнутый вид списка }
Кнопка 1 будет добавлять запись, если такой же записи ещё нет в списке:
private void button1_Click (object sender, EventArgs e) { String r = comboBox1.Text; if (comboBox1.FindString (r) == -1) comboBox1.Items.Add (r); }
Кнопка 2 будет удалять выбранный в списке элемент, если таковой есть:
private void button2_Click (object sender, EventArgs e) { if (comboBox1.SelectedIndex != -1) comboBox1.Items.Remove (comboBox1.SelectedItem); }
Кнопка 3 выполнит работу по сохранению файла с использованием поточного класса StreamWriter
. Для простоты используем файл с фиксированным именем data.txt
, располагающийся в текущей папке.
private void button3_Click (object sender, EventArgs e) { try { System.IO.StreamWriter file = new System.IO.StreamWriter ("data.txt"); for (int i = 0; i < comboBox1.Items.Count; i++) file.WriteLine (comboBox1.Items [i].ToString ()); file.Close (); } catch (Exception) { MessageBox.Show ("Не могу записать data.txt"); } }
Кнопка 4 отвечает за загрузку элементов списка из файла data.txt
. Чтобы можно было закрыть дескриптор файла после чтения данных, применим поточный класс StreamReader
:
private void button4_Click (object sender, EventArgs e) { try { System.IO.StreamReader file = new System.IO.StreamReader ("data.txt"); String line; comboBox1.Items.Clear (); while (( line = file.ReadLine () ) != null) comboBox1.Items.Add (line); file.Close (); } catch (Exception) { MessageBox.Show ("Не могу открыть data.txt"); } }
Осталось обеспечить ввод только разрешённых символов, для этого достаточно добавить обработчик события KeyPress
(там доступно свойство KeyChar
, в отличие от KeyCode
в обработчике события KeyDown
) для списка comboBox1
:
private void comboBox1_KeyPress (object sender, KeyPressEventArgs e) { char c = e.KeyChar; if (Char.IsLetterOrDigit (c) || c == (char) Keys.Back || c == (char) Keys.Enter) return; else e.Handled = true; }
Задача решена полностью.
Скачать пример Ex8_1 в архиве .zip с проектом C# Visual Studio 2019 (11 Кб)
Проект Ex8_2. На графической канве отобразить работу светофора с задержкой между состояниями "красный-жёлтый-зелёный" 1 сек.
Форма – пустое окно Windows Forms.
Для решения задачи используем канву формы и программно созданный таймер.
Опишем таймер, счётчик состояний и размер элемента светофора в классе формы:
private Timer timer1; private int cnt; private const int size = 120; //ширина и высота окружностей
По событию загрузки формы (Load
) настроим размеры окна и инициализируем таймер:
private void Form1_Load (object sender, EventArgs e) { this.ClientSize = new System.Drawing.Size (size, size*3); this.DoubleBuffered = true; timer1 = new Timer (); timer1.Interval = 1000; timer1.Tick += (sendr, args) => { Invalidate (); cnt = ( cnt + 1 ) % 3; }; timer1.Enabled = true; cnt = 0; }
Обработчик события Tick
таймера мы встроили в оператор назначения как стрелочную функцию. Обратите внимание, что этот обработчик таймера только вызывает перерисовку формы (вызов Invalidate()
, отправляющий сообщение стандартному методу пререрисовки Paint()
) и меняет счётчик состояний cnt
, а саму отрисовку будет делать метод Paint
:
private void Form1_Paint (object sender, PaintEventArgs e) { Pen [] pens = new Pen [] { Pens.Red, Pens.Yellow, Pens.Green }; for (int i = 0; i < 3; i++) e.Graphics.DrawEllipse (pens [i], 0, i * size, size, size); //контуры 3 кружков Brush [] brushes = new Brush [] { Brushes.Red, Brushes.Yellow, Brushes.Green }; e.Graphics.FillEllipse (brushes [cnt], 0, cnt * size, size, size); //текущий закрасить }
Для простоты непосредственно в функции описаны массивы перьев Pen
и кистей Brush
нужных цветов. Но лучше вынести эти массивы тоже в свойства класса, а создать их один раз в конструкторе формы.
Альтернативный подход (без привязки к методу Paint
) – в обработчике события таймера программно создавать Bitmap
нужной размерности, выполнять отрисовку на нём, а затем назначать его компоненте PictureBox
.
Задача решена полностью.
Скачать пример Ex8_2 в архиве .zip с проектом C# Visual Studio 2019 (11 Кб)
Проект Ex8_3. Поддержка динамического списка компонент TextBox произвольной размерности. Реализовать добавление и удаление компонент.
Форма – пустая Windows Forms. В качестве примера будем создавать поля ввода TextBox
в месте щелчка мышью по форме и удалять их при щелчке мышью на самих полях.
Нам не понадобится отдельно сохранять поля ввода в каком-либо списке, хотя мы могли бы сделать это, например, так:
using System.Collections.Generic; //... List <TextBox> F; //динамический список System.Collections.Generic.List объектов типа TextBox //... F = new List <TextBox>(); //конструктор списка //... F.Add (T); //где T - созданный TextBox
Дело в том, что у формы уже есть контейнер Controls
с методами Add
и Remove
.
В классе формы дополнительно опишем только счётчик контролов:
int cnt; //счётчик объектов
Инициализируем счётчик в конструкторе формы:
public Form1 () { InitializeComponent (); cnt = 0; }
Щелчку по форме мышью соответствует событие MouseClick
. Достаточно в обработчике этого события формы создать программно новый TextBox
и добавить его в список компонент формы:
private void Form1_MouseClick (object sender, MouseEventArgs e) { TextBox T = new TextBox (); T.Text = "Text" + ( cnt++ ); T.Location = new Point (e.X, e.Y); T.Parent = this; this.Controls.Add (T); }
Длина списка не ограничена. Однако если мы хотим удалять компоненты TextBox
по какому-то событию, например, по щелчку на них, придётся всем создаваемым TextBox
программно назначать обработчик этого события, так что вставим соответствующий код в метод Form1_MouseClick
(перед добавлением поля ввода в список контролов):
T.Click += (sendr, args) => { this.Controls.Remove ((TextBox)sendr); };
Задача решена полностью.
Альтернативным, но избыточным решением, как сказано выше, были бы статические или динамические массивы компонент.
Скачать пример Ex8_3 в архиве .zip с проектом C# Visual Studio 2019 (10 Кб)
Проект Ex8_4. Реализовать простейшее движение объекта ("героя") по форме (двумерному "лабиринту"). Лабиринт состоит из отдельных полей, среди которых есть проходимые и не проходимые.
Сделаем всё максимально просто согласно условию задачи.
Нам понадобится растянутый на всю клиентскую часть формы PictureBox
(свойство Dock = Fill
) и таймер, у которого установлены свойства Enabled = true
и Interval = 200
.
В классе формы опишем нужные данные:
public const int width = 10, height = 10, k = 16; //размеры игровой доски и одного поля public int [,] field = new int [width, height]; //сама игровая доска public int [] hero = new int [] { 1, 1}; //координаты героя public Bitmap bitfield = new Bitmap (k * width, k * height ); //битмап для отрисовки public Graphics gr; //графический контекст
Массив в C# автоматически инициализируется нулями, поэтому достаточно выставить в значение "1" те элементы массива поля, которые соответствуют "стенам". Сделаем это в конструкторе формы, там же установим размер окна и получим графический контекст:
public Form1 () { InitializeComponent (); //установим размер окна и получим графический контекст: this.ClientSize = new Size (k * width, k * height); gr = Graphics.FromImage (bitfield); //выставим все крайние поля в единицы: for (int i = 0; i < width; i++) { field [i, 0] = 1; field [i, height - 1] = 1; } for (int i = 0; i < height; i++) { field [0, i] = 1; field [width - 1, i] = 1; } //выставьте и некоторые другие поля в единицы... }
Обработчик нажатия клавиши формы будет получать текущее положение героя и менять его, если этому не мешают препятствия (в нашем случае - "стены"):
private void Form1_KeyDown (object sender, KeyEventArgs e) { //обрабатываем нажатия клавиш со стрелками int x = hero [0], y = hero [1]; switch (e.KeyCode) { case Keys.Left: if (x > 0 && field [x - 1, y] == 0) x--; break; case Keys.Right: if (x < width - 1 && field [x + 1, y] == 0) x++; break; case Keys.Up: if (y > 0 && field [x, y-1] == 0) y--; break; case Keys.Down: if (x < height - 1 && field [x , y+1] == 0) y++; break; } hero[0] = x; hero [1] = y; }
Обработчик события таймера будет просто запрашивать отрисовку, которую мы, на случай развития приложения, вынесем в отдельный метод DrawMe
:
private void TickTimer_Tick (object sender, EventArgs e) { //по таймеру просто запрашиваем отрисовку DrawMe (); } public void DrawMe () { //метод для отрисовки поля и героя gr.Clear (Color.Black); for (int i = 0; i < width; i++) for (int j = 0; j < height; j++) if (field [i, j] == 1) { gr.FillRectangle (Brushes.Green, i * k, j * k, k, k); gr.DrawRectangle (Pens.Black, i * k, j * k, k, k); } gr.FillEllipse (Brushes.Red, hero[0] * k, hero [1] * k, k, k); pictureBox1.Image = bitfield; }
Задача решена полностью, вот что вышло:

вид приложения, стены - только по краям поля
Скачать пример Ex8_4 в архиве .zip с проектом C# Visual Studio 2019 (11 Кб)
13.05.2023, 10:04 [483 просмотра]