Как создать GUI

Что такое GUI

GUI расшифровывается как graphical user interface, что по-русски переводится как графический интерфейс пользователя. Главное отличие GUI-приложения от консольного заключается в способе взаимодействия пользователя с приложением.

В консольном приложении общение с пользователем осуществляется в последовательной манере. То есть грубо говоря:

  1. Пользователь что-то ввел
  2. Приложение что-то ответило
  3. Пользователь снова что-то ввел
  4. Приложение снова что-то ответило
  5. и т.д.

И перескочить со 2-го пункта на 5-ый, если программой этого не было предусмотрено, при всем желании невозможно. Все очень строго.

В графическом же приложении появляются так называемые формы (или окна, кому как больше нравится), а с ними в придачу, всякие кнопки, поля для ввода, календари, таблицы и т.п.

И доступ ко всем эти объектам осуществляется в произвольном порядке. На программном уровне, достигается это за счет реализации так называемого цикла сообщений. Когда вы запускаете приложение (хотя бы тот же самый браузер), оно тут же начинает прослушивать какие сообщения ему посылает система.

Ясное дело, что сообщения эти не простые, а имеют строго установленную форму, например, когда вы двигаете мышкой, на каждый сдвиг отправляется сообщение WM_MOUSEMOVE, которое содержит информацию о позиции мыши, если вы куда-то кликаете отправляется сообщение WM_LBUTTONDOWN. Если вы нажимаете клавишу, то отправляется WM_KEYDOWN с кодом нажатой клавиши. И так далее.

Выглядит цикл сообщений на C примерно так:

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
    MSG msg;
    BOOL bRet;

    while(1) // цикл бесконечный
    {
        bRet = GetMessage(&msg, NULL, 0, 0);

        if (bRet > 0)  // (bRet > 0 пришло какое-то сообщение)
        {
            TranslateMessage(&msg);
            DispatchMessage(&msg);
        }
        else if (bRet < 0)  // (bRet == -1 произошла ошибка)
        {
            // Обработка ошибки ...
        }
        else  // (bRet == 0 пришол сигнал о завершении программы, например кто-то нажал Alt+F4)
        {
            break;
        }
    }
    return msg.wParam;
}

Все эти события отправляются главному окну приложения, которое в свою очередь распределяет эти сообщения между своими кнопками, полями для ввода и т.д. (именуемые в простонародье контр`олами). По приходу сообщения тому или иному контролу, можно выполнить какой-нибудь код. Например, который отобразит какое-нибудь сообщение.

К счастью, в наши развитые времена, заботиться об обработки сообщений не надо. И чтобы добавить реакцию на какое-нибудь сообщение, надо просто добавить соответствующий метод и особым образом привязать его к контролу.

Создаем интерфейс для задачи

Необходимо: разработать интерфейс для задачи: “Студенты Иванов и Петров за время практики заработали определенную сумму. Кто из них заработал большую сумму? Определить средний заработок”.

Создаем проект

Выбираем Файл/Создать/Проект, затем Приложение Windows Forms, жмем Ok

Откроется редактор формы:

Добавляем поля для ввода

На форму надо чего-то добавить. По задаче у нас два параметра, значит придется добавить два поля для ввода. Откроем панель с элементами и для удобства зафиксируем ее с помощью пипки в верхнем правом углу.

Если у вас вдруг не видно панели с элементами, включите ее через пункт меню Вид/Панель элементов

Теперь добавим элементы на форму, нам потребуется два элемента вида TextBox (поля для ввода текста)

Чтобы было понятно чего в них вводить добавим подписи (элементы типа Label)

Но у этих подписей вместо текста написано label1 и label2, поменяем их свойства. У всех элементов типа Label есть свойство Text, которое определяет чего в них писать. Выделим label1 кликнув на него, он обведется пунктирной рамкой

в правом нижнем углу найдем панель свойств (если ее не видно нажмите F4). Среди множества свойств найдем то что называется Text и введем в него корректный текст.

Закончив вводить переключимся на форму, кликнув на нее, если все было сделано корректно текст label1 заменится на руб. заработал Петров

Повторим ту же процедуру для label2

  • выберем на форме
  • в окне свойств в поле Text введем руб. заработал Иванов
  • снова переключимся на форму

получим:

Работаем с кнопкой

Теперь давайте добавим кнопку, и изменим размеры формы, а то что-то сильно много пустого места:

кнопка уже штука поинтереснее, давайте добавим ее какую-нибудь реакцию на щелчок. Выделим кнопку и щелкнем по ней два раза.

Нас перекинет в редактор кода, который будет выглядеть как-то так:

Мы только что создали обработчик события щелчка мыши по кнопке. В который можно добавить какую-нибудь реакцию на щелчок.

[Как работает обработчик]

Если вам не интересно как, смело пропускайте данный раздел =)

Что же тут произошло? А произошло тут создание функции (то есть Visual Studio за нас написала код, нам никто не мешает его ручками писать) с сигнатурой обработчика системного события. У функции два аргумента:

  • object sender – объект от которого пришло событие, в нашем случае это будет кнопка button1
  • EventArgs e – специфически свойства события, в клика нет особых свойства, а вот всякие события типа клика мыши или нажатия кнопки могут содержать дополнительную информацию (см. выше про системные события)

Далее студия привязала данную функцию к кнопке. Если смотреть через интерфейс (переключимся на форму нажав Shift+F7), то эта функция будет указана в качестве значения свойства Click в разделе Событий.

Если же смотреть еще глубже можно открыть, автогенерируемый файлик для формы Form1.Designer.cs

Там мы увидим код:

И если раскрыть узел Код автоматически созданный конструктором, то увидим сгенерированный код, который в явном виде описывает положение всех объектов на форме, а также привязанные к ним свойства. Приведу часть кода:

private void InitializeComponent()
{
    // ...
    // label1, это создание подписи
    // 
    this.label1.AutoSize = true;
    this.label1.Location = new System.Drawing.Point(118, 15); // вот ее точная координата верхнего левого угла
    this.label1.Name = "label1";
    this.label1.Size = new System.Drawing.Size(123, 13); // а вот размеры
    this.label1.TabIndex = 2;
    this.label1.Text = "руб. заработал Петров"; // а вот и свойство Text
    // 
    //...
    // 
    // button1, а это кнопку добавляем
    // 
    this.button1.Location = new System.Drawing.Point(12, 64);
    this.button1.Name = "button1";
    this.button1.Size = new System.Drawing.Size(229, 23);
    this.button1.TabIndex = 4;
    this.button1.Text = "button1";
    this.button1.UseVisualStyleBackColor = true;
    this.button1.Click += new System.EventHandler(this.button1_Click); // вот подключение функции к событию щелчка
    // 
    // ...
}

но ладно, вернемся к нашему обработчику.

Пишем обработчик

Если в какой-то момент времени вы потеряетесь среди файлов, то вы всегда можете дважды кликнуть на файл Form1.cs в обозревателе решений

а затем нажать F7 чтобы переключится непосредственно к коду формы.

И так, у нас там имеется код:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace WindowsFormsApp4
{
    /**
     * Тут сразу два интересных момента при создании класса
     * 1. Есть слово partial, которое говорит, что код класса разбит на несколько файлов, 
     *    если вы читали предыдущий раздел, то может заметили, что в Form1.Designer.cs 
     *    находится вторая часть этого класса.
     * 2. Через двоеточие справа от названия класса (т.е. Form1) стоит слово Form,
     *    Form -- это класс которые написали за нас разработчики .Net, он реализуют
     *    всю ту вакханалию с обработкой системных событий, про которую я говорил в самом начале 
     *    статьи и много еще чего полезного. 
     *    Так вот написав справа от названия своего класса, через двоеточие название 
     *    другого класса мы все свойства и методы последнего перенимаем в свой класс.
     *    Это называется наследованием.
     */
    public partial class Form1 : Form 
    {
        public Form1()
        {
            /**
             * это вызов метода который формирует поля на форме, добавляет свойства, 
             * короче все то что находится в Form1.Designer.cs 
            */
            InitializeComponent();  
        }

        private void button1_Click(object sender, EventArgs e)
        {
            // а вот реакция на клик, сейчас чего-нибудь сюда напишем
        }
    }
}

Нам по заданию надо будет вывести сообщение с решением задачи. Пока мы еще решатель не реализовали (точнее реализовывали, но еще сюда к новому коду не подцепили). А вот что-нибудь вывести уже можем. Правим обработчик:

namespace WindowsFormsApp4
{
    public partial class Form1 : Form 
    {
        // ...

        private void button1_Click(object sender, EventArgs e)
        {
            // Это должно показать сообщение с текстом
            MessageBox.Show("Ура работает! (╯°□°)╯︵ ┻━┻");
        }
    }
}

Запустим приложение и проверим кнопку

Читаем значения из TextBox

Давайте теперь попробуем немного расширить функциональность, и будем выводить по нажатию на кнопки не простое сообщение а содержимое которое ввели в качестве зарплаты Петрова.

Переключимся на форму, нажав Shift+F7. Выберем первое поле для ввода.

Чтобы получить содержимое TextBox надо сначала узнать имя элемента. Заглянем в панель Свойств, и найдем там свойство (Name). Это и есть его имя. По умолчанию там стот textBox1, поменяем его на что-то более осознанное (txtPetrovSum):

Теперь мы сможем обратиться к элементу по этому имени. Давайте теперь еще и поменяем свойство Name у второго textBox2. Поменяем его на txtIvanovSum. По итогу будем иметь следующие названия у элементов:

переключимся обратно на код, нажмем F7, либо два раза щелкнем на кнопку.

public partial class Form1 : Form
{
    public Form1()
    {
        InitializeComponent();
    }

    private void button1_Click(object sender, EventArgs e)
    {
        MessageBox.Show(this.txtPetrovSum.Text); // выведем содержимое поля txtPetrovSum
    }
} 

Запускаем и проверяем:

Можно собрать какую-нибудь фразу:

public partial class Form1 : Form
{
    public Form1()
    {
        InitializeComponent();
    }

    private void button1_Click(object sender, EventArgs e)
    {
        MessageBox.Show("Петров заработал: " + this.txtPetrovSum.Text + "\n"
                            + "Иванов заработал: " + this.txtIvanovSum.Text);
    }
} 

Добавив “\n” мы сможем вывести текст в две строки. Получится:

Но это мы все в игрушки играемся, давайте все таки уже задачу решим

Подключаем старый код

Для лучше переносимости, рекомендую уже реализованное решение исходной задачи сначала декомпозировать, и тогда вам будет достаточно скопировать класс с логикой. Я это уже сделал с задачей про студентов еще пару статей назад, поэтому я возьму код класса Logic

public class Logic
{
    /**
     * функция Compare нужна нам, чтобы сформировать сообщение о сравнимости полученных денег 
     */
    public static string Compare(int ivanovSum, int petrovSum)
    {
        string outMessage = "";
        if (ivanovSum > petrovSum)
        {
            outMessage = "Иванов заработал больше";
        }
        else if (petrovSum > ivanovSum)
        {
            outMessage = "Петров заработал больше";
        }
        else
        {
            outMessage = "Студенты заработали одинаковое количество денег";
        }
        return outMessage;
    }
    
    /**
     * А эта функция нам нужна, чтобы получить среднее значение двух заработков
     */
    public static int GetAverage(int ivanovSum, int petrovSum)
    {
        var averageSum = (petrovSum + ivanovSum) / 2;
        return averageSum;
    }
}

и вставлю этот класс вместе со всем его содержимым после класса Form1 в файле Form1.cs. Вот что у меня получится:

namespace WindowsFormsApp4 // у вас будет другой namespace, скорее всего 
{
    // этот класс пока не трогаем
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private void button1_Click(object sender, EventArgs e)
        {
            MessageBox.Show("Петров заработал: " + this.txtPetrovSum.Text + "\n"
                            + "Иванов заработал: " + this.txtIvanovSum.Text);
        }
    }

    public class Logic // класс где будем хранить логику
    {
        public static string Compare(int ivanovSum, int petrovSum)
        {
            string outMessage = "";
            if (ivanovSum > petrovSum)
            {
                outMessage = "Иванов заработал больше";
            }
            else if (petrovSum > ivanovSum)
            {
                outMessage = "Петров заработал больше";
            }
            else
            {
                outMessage = "Студенты заработали одинаковое количество денег";
            }
            return outMessage;
        }

        public static int GetAverage(int ivanovSum, int petrovSum)
        {
            var averageSum = (petrovSum + ivanovSum) / 2;
            return averageSum;
        }
    }
}

Очень важно вставить код класса ПОСЛЕ класса Form1, иначе получите страшную ошибку:

Внедряем логику

правим наш обработчик клика на кнопку:

namespace WindowsFormsApp4 
{
    public partial class Form1 : Form
    {
        // ...
        
        private void button1_Click(object sender, EventArgs e)
        {
            /**
             * Можно заметить, как это код похож на считывание ввода пользователя из консоли:
             * 
             * var ivanovSum = int.Parse(Console.ReadLine());
             * var petrovSum = int.Parse(Console.ReadLine());
             * 
             * мы все также преобразуем ввод в число, и так же сохраняем результат в переменные.
             */ 
            var petrovSum = int.Parse(this.txtPetrovSum.Text);
            var ivanovSum = int.Parse(this.txtIvanovSum.Text);
            
            // выведем сообщение о сравнимости заработка
            MessageBox.Show(Logic.Compare(ivanovSum, petrovSum));
        }
    }

    public class Logic // класс где будем хранить логику
    {
        //...
    }
}

Запускаем и проверяем:

А! Нам же еще среднее арифметическое надо вывести:

namespace WindowsFormsApp4 
{
    public partial class Form1 : Form
    {
        // ...
        
        private void button1_Click(object sender, EventArgs e)
        {
            var petrovSum = int.Parse(this.txtPetrovSum.Text);
            var ivanovSum = int.Parse(this.txtIvanovSum.Text);

            var average = Logic.GetAverage(ivanovSum, petrovSum);
            MessageBox.Show(Logic.Compare(ivanovSum, petrovSum) + "\n"
                            + "Среднее арифметическое: " + average.ToString());
        }
    }

    public class Logic // класс где будем хранить логику
    {
        //...
    }
}

еще один запуск:

Обработка ошибок

Может вы уже столкнулись с этим, но если запустить приложение, ничего не ввести и просто нажать кнопку, программа выдаст ошибку:

появление ее закономерно, программа пытается с помощью метода int.Parse преобразовать строку в число, но в строке пусто и преобразовывать нечего. Аналогичная ошибка появится если ввести буквы вместо цифр.

Наверное было бы здорово, просто проигнорировать нажатие кнопки с некорректными данными, для этого нам надо заставить программу не падать. Делается это не сильно сложно, путем добавления конструкции для перехвата ошибок, Именуется она try-catch, и выглядит так:

namespace WindowsFormsApp4
{
    public partial class Form1 : Form
    {
        // ...
        
        private void button1_Click(object sender, EventArgs e)
        {
            try // оборачиваем кусок кода склонный к падению
            {
                var petrovSum = int.Parse(this.txtPetrovSum.Text);
                var ivanovSum = int.Parse(this.txtIvanovSum.Text);
            }
            catch(FormatException) // тип ошибки, которую перехватываем
            {
                return; // прерываем обработчик клика, если ввели какую-то ерунду
            }

            var average = Logic.GetAverage(ivanovSum, petrovSum);
            MessageBox.Show(Logic.Compare(ivanovSum, petrovSum) + "\n"
                            + "Среднее арифметическое: " + average.ToString());
        }
    }

    public class Logic // класс где будем хранить логику
    {
        //...
    }
}

правда если просто вставить код в таком виде то он будет ругаться на переменные ivanovSum и petrovSum, после блока try/catch. Это происходит потому что переменные инициализируются внутри блока try, надо их вынести вовне. Придется указать тип явно.

namespace WindowsFormsApp4
{
    public partial class Form1 : Form
    {
        // ...
        
        private void button1_Click(object sender, EventArgs e)
        {
            int petrovSum, ivanovSum; // вынес инициализацию переменных за блок try-catch
            try
            {
                // тут теперь просто присваиваем значения
                petrovSum = int.Parse(this.txtPetrovSum.Text);
                ivanovSum = int.Parse(this.txtIvanovSum.Text);
            }
            catch(FormatException)
            {
                return; // прерываем обработчик если ввели какую-то ерунду
            }

            var average = Logic.GetAverage(ivanovSum, petrovSum);
            MessageBox.Show(Logic.Compare(ivanovSum, petrovSum) + "\n"
                            + "Среднее арифметическое: " + average.ToString());
        }
    }

    public class Logic // класс где будем хранить логику
    {
        //...
    }
}

проверяем:

Красота! Можно сообщение выдавать об ошибке (но лучше не надо):

namespace WindowsFormsApp4
{
    public partial class Form1 : Form
    {
        // ...
        
        private void button1_Click(object sender, EventArgs e)
        {
            int petrovSum, ivanovSum;
            try
            {
                petrovSum = int.Parse(this.txtPetrovSum.Text);
                ivanovSum = int.Parse(this.txtIvanovSum.Text);
            }
            catch(FormatException)
            {
                // сообщение об ошибке
                MessageBox.Show("Некорректный ввод", "Ошибка", MessageBoxButtons.OK, MessageBoxIcon.Error);
                return; // а затем прерываем обработчик
            }

            var average = Logic.GetAverage(ivanovSum, petrovSum);
            MessageBox.Show(Logic.Compare(ivanovSum, petrovSum) + "\n"
                            + "Среднее арифметическое: " + average.ToString());
        }
    }

    public class Logic // класс где будем хранить логику
    {
        //...
    }
}

получится:

Это в принципе должно хватить для выполнения первого задания в лабе 4.