GUI и визуальные компоненты

Разработка оконных приложений на C#

Обновлено: · 8 мин чтения

Оконное приложение на C# - это программа с графическим интерфейсом: окна, кнопки, поля ввода и таблицы. Для Windows чаще всего выбирают Windows Forms ради простоты и WPF ради гибкого оформления. Здесь собрано всё, что нужно для старта: выбор технологии, установка, первый проект на .NET 8, события, асинхронность, привязка данных и публикация готовой программы.

Разработка оконных приложений на C#
Содержание
  1. Какую технологию выбрать
  2. Что нужно установить
  3. Первое приложение на Windows Forms
    1. Основные элементы управления
    2. Расположение элементов и адаптивность
  4. Долгие операции и асинхронность
  5. Несколько окон и передача данных
  6. Приложение на WPF
    1. Привязка данных и MVVM
    2. Стили и ресурсы
  7. Работа с данными в окне
  8. Публикация готовой программы
  9. Типичные ошибки новичков
  10. Частые вопросы

Какую технологию выбрать

Под Windows на C# существует несколько UI-платформ, и от выбора зависит, сколько времени вы потратите на интерфейс. Все они работают на современном .NET, а классический .NET Framework 4.8 остаётся только для поддержки старых проектов. Новичку лучше сразу создавать проекты на .NET 8, там тот же язык, те же компоненты и актуальные обновления.

ТехнологияДля чего подходитСложностьОсобенности
Windows Formsучебные программы, внутренние утилиты, быстрые формы ввода данныхнизкаявизуальный конструктор, готовые элементы, только Windows
WPFбизнес-приложения со сложным интерфейсом, таблицы, графика, темысредняяразметка XAML, привязка данных, шаблоны, паттерн MVVM
WinUI 3современные приложения в стиле Windows 11выше среднейновые элементы Fluent, упаковка в MSIX
Avalonia UIпрограммы для Windows, Linux и macOSсредняяXAML-подобная разметка, один код на все системы
.NET MAUIприложения для Android, iOS, Windows и macOSвыше среднейориентирован на мобильные устройства

Если вы только учитесь, начинайте с Windows Forms: интерфейс собирается перетаскиванием, а логика пишется в обработчиках событий, поэтому быстро видно результат. Когда понадобятся красивые окна, гибкая вёрстка и разделение кода и оформления, переходите на WPF. Приложение для нескольких операционных систем разумнее делать на Avalonia.

Что нужно установить

Основной инструмент - Visual Studio 2022 Community, она бесплатна для обучения, open source и небольших команд. При установке отметьте рабочую нагрузку "Разработка классических приложений .NET" (.NET desktop development), в неё входят шаблоны Windows Forms и WPF, конструктор форм и .NET SDK. Альтернативы: JetBrains Rider, который работает с WPF, и Visual Studio Code с расширением C# Dev Kit, но визуального конструктора форм в Code нет.

Проверить, что пакет разработки установился, можно в командной строке командой dotnet --list-sdks. Должна появиться версия 8.0 или новее. Эта же утилита создаёт проекты без Visual Studio, что удобно для автоматизации и проверки.

dotnet new winforms -n MyFirstApp
cd MyFirstApp
dotnet run

Первое приложение на Windows Forms

В Visual Studio выберите "Создать проект", найдите шаблон "Приложение Windows Forms" для C# (не путайте с вариантом ".NET Framework"), задайте имя и целевую платформу .NET 8. Откроется конструктор с пустой формой Form1. В панели элементов перетащите на неё Button, Label и TextBox, а в окне "Свойства" измените им имена и тексты.

Файл Program.cs в новых шаблонах очень короткий: настройки высокого разрешения и шрифта выполняет один вызов. Именно отсюда запускается главное окно.

namespace MyFirstApp;

static class Program
{
    [STAThread]
    static void Main()
    {
        ApplicationConfiguration.Initialize();
        Application.Run(new Form1());
    }
}

Код формы разделён на два файла: Form1.Designer.cs создаётся конструктором автоматически и не редактируется вручную, а Form1.cs принадлежит вам. Двойной щелчок по кнопке создаёт в нём обработчик события Click.

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

    private void buttonGreet_Click(object sender, EventArgs e)
    {
        string name = textBoxName.Text.Trim();
        labelResult.Text = string.IsNullOrEmpty(name)
            ? "Введите имя"
            : $"Привет, {name}!";
    }
}

Запуск выполняется клавишей F5 или зелёной кнопкой на панели. Пользователь вводит имя, нажимает кнопку, и метка показывает приветствие. Всё взаимодействие построено на событиях: нажатие, изменение текста, закрытие окна. Подписку на событие можно сделать и в коде: buttonGreet.Click += buttonGreet_Click;.

Основные элементы управления

Набор стандартных компонентов закрывает большинство задач. Чтобы не путаться в названиях, держите перед глазами эту краткую шпаргалку.

ЭлементНазначениеКлючевое свойство или событие
Labelподпись, вывод текстаText
TextBoxввод строкиText, TextChanged
Buttonзапуск действияClick
CheckBox, RadioButtonфлажок и выбор одного из вариантовChecked, CheckedChanged
ComboBox, ListBoxвыбор из спискаItems, SelectedItem
DataGridViewтаблица данныхDataSource, CellValueChanged
NumericUpDownчисловой ввод с ограничениемValue, Minimum, Maximum
Timerдействие по таймеруInterval, Tick

Про отдельные компоненты у нас есть подробные материалы: как вывести число в TextBox и как добавить строку в DataGridView.

Расположение элементов и адаптивность

Чтобы окно нормально выглядело при изменении размера, не расставляйте элементы вслепую. Свойство Anchor привязывает элемент к краям формы, а Dock растягивает его на всю сторону контейнера. Для сложных макетов используйте TableLayoutPanel и FlowLayoutPanel: они сами распределяют место между вложенными элементами. Отдельно проверьте работу на экране с масштабом 125 или 150 процентов, типичная проблема - обрезанный текст кнопок.

Долгие операции и асинхронность

Главная ошибка начинающих - тяжёлая работа прямо в обработчике кнопки. Пока код выполняется в потоке интерфейса, окно не перерисовывается и выглядит зависшим. Решение - вынести расчёт или запрос в фоновую задачу через async и await. После await выполнение возвращается в поток интерфейса, поэтому элементы можно менять напрямую.

private async void buttonLoad_Click(object sender, EventArgs e)
{
    buttonLoad.Enabled = false;
    labelResult.Text = "Загрузка...";
    try
    {
        using var client = new HttpClient();
        string html = await client.GetStringAsync("https://example.com");
        labelResult.Text = $"Получено символов: {html.Length}";
    }
    catch (HttpRequestException ex)
    {
        labelResult.Text = "Ошибка: " + ex.Message;
    }
    finally
    {
        buttonLoad.Enabled = true;
    }
}

Если расчёт загружает процессор, а не сеть, оберните его в await Task.Run(() => ...). Обращаться к элементам формы изнутри такого лямбда-блока нельзя, иначе получите исключение о доступе из другого потока. Как правильно обновлять интерфейс из фона, мы разобрали отдельно: обновление GUI из другого потока. И не используйте Application.DoEvents() как костыль, о рисках написано в статье про DoEvents.

Несколько окон и передача данных

Реальные программы редко состоят из одной формы. Для диалогов используется метод ShowDialog(), он блокирует родительское окно, пока диалог не закрыт, и возвращает результат кнопки. Данные в дочернюю форму удобно передавать через конструктор, а забирать обратно через свойство.

public partial class SettingsForm : Form
{
    public string UserName => textBoxName.Text;

    public SettingsForm(string currentName)
    {
        InitializeComponent();
        textBoxName.Text = currentName;
        buttonOk.DialogResult = DialogResult.OK;
        buttonCancel.DialogResult = DialogResult.Cancel;
    }
}

// в главной форме
using var dialog = new SettingsForm(currentName);
if (dialog.ShowDialog(this) == DialogResult.OK)
{
    currentName = dialog.UserName;
}

Другие способы - события, делегаты и общий объект данных - описаны в руководстве про передачу данных между формами и в статье о взаимодействии форм Windows Forms.

Приложение на WPF

WPF отделяет внешний вид от логики. Интерфейс описывается в XAML, а поведение пишется в C#. Проект создаётся шаблоном "Приложение WPF" на .NET 8. Главное окно находится в MainWindow.xaml, а код окна - в MainWindow.xaml.cs. Разметка Grid делит окно на строки и столбцы, вложенные панели StackPanel выстраивают элементы друг за другом.

<Window x:Class="WpfApp.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Приветствие" Height="220" Width="360">
    <StackPanel Margin="16">
        <TextBox x:Name="NameBox" Margin="0,0,0,8" />
        <Button Content="Поздороваться" Click="Greet_Click" />
        <TextBlock x:Name="ResultText" Margin="0,12,0,0" FontSize="16" />
    </StackPanel>
</Window>
private void Greet_Click(object sender, RoutedEventArgs e)
{
    ResultText.Text = string.IsNullOrWhiteSpace(NameBox.Text)
        ? "Введите имя"
        : $"Привет, {NameBox.Text}!";
}

Такой подход работает, но настоящая сила WPF проявляется в привязке данных. Вместо ручного копирования значений между полями и кодом элемент связывается со свойством объекта, и интерфейс обновляется сам при его изменении.

Привязка данных и MVVM

Для привязки класс модели представления реализует интерфейс INotifyPropertyChanged и сообщает об изменении свойств. Разметка указывает путь к свойству в выражении Binding. Так формируется паттерн MVVM: модель хранит данные, представление отвечает за вид, а модель представления связывает их. Подробный разбор паттерна есть в статье MVVM в C#.

using System.ComponentModel;
using System.Runtime.CompilerServices;

public class MainViewModel : INotifyPropertyChanged
{
    private string _name = "";
    public string Name
    {
        get => _name;
        set { _name = value; OnPropertyChanged(); OnPropertyChanged(nameof(Greeting)); }
    }

    public string Greeting => string.IsNullOrWhiteSpace(Name) ? "" : $"Привет, {Name}!";

    public event PropertyChangedEventHandler? PropertyChanged;
    private void OnPropertyChanged([CallerMemberName] string? prop = null)
        => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(prop));
}
<StackPanel Margin="16">
    <TextBox Text="{Binding Name, UpdateSourceTrigger=PropertyChanged}" />
    <TextBlock Text="{Binding Greeting}" FontSize="16" Margin="0,12,0,0" />
</StackPanel>

В конструкторе окна достаточно написать DataContext = new MainViewModel();, и приветствие будет меняться по мере ввода без единой строки кода обработчиков. Чтобы не писать шаблонный код вручную, в реальных проектах подключают пакет CommunityToolkit.Mvvm, он генерирует свойства и команды атрибутами.

Стили и ресурсы

Оформление в WPF выносится в ресурсы: словари стилей применяются ко всем кнопкам или к элементам с заданным ключом. Это экономит правки при смене фирменных цветов, а темы оформления подключаются отдельным пакетом. В Windows Forms аналога нет, там внешний вид меняют вручную или сторонними библиотеками.

Работа с данными в окне

Типичное оконное приложение показывает таблицу из базы или файла. В Windows Forms достаточно присвоить список объектов свойству DataGridView.DataSource, а для автоматического обновления использовать BindingList<T>. В WPF применяется ObservableCollection<T> и элемент DataGrid или ListView. Для доступа к SQL Server, SQLite или PostgreSQL берут Entity Framework Core, а для чтения Excel и CSV существуют отдельные библиотеки, об одной из них рассказано в статье про работу с Excel.

var people = new BindingList<Person>
{
    new("Анна", 28),
    new("Игорь", 34)
};
dataGridView1.DataSource = people;
people.Add(new Person("Мария", 25)); // строка появится в таблице сама

public record Person(string Name, int Age);

Записи (record) в роли источника данных работают для чтения, но свойства у них по умолчанию доступны только для инициализации, поэтому для редактируемой таблицы объявляйте обычный класс со свойствами get; set;.

Публикация готовой программы

Чтобы передать приложение пользователям, опубликуйте проект: в Visual Studio пункт "Опубликовать" либо команда в терминале. Вариант с флагом self-contained включает среду выполнения в папку программы, и на компьютере не нужно устанавливать .NET отдельно. Параметр единственного файла упаковывает всё в один exe.

dotnet publish -c Release -r win-x64 --self-contained true -p:PublishSingleFile=true

Для установки с ярлыками и удалением используйте MSIX или сторонние сборщики инсталляторов вроде Inno Setup. Перед выпуском проверьте приложение в конфигурации Release: различия с отладочной сборкой описаны в статье про Debug и Release.

Типичные ошибки новичков

  • Вся логика в обработчиках кнопок. Выносите расчёты в отдельные классы, тогда их можно тестировать и переиспользовать.
  • Правка файла Designer.cs руками. Конструктор перезапишет изменения, меняйте свойства в окне "Свойства" или в конструкторе формы после InitializeComponent().
  • Тяжёлые вычисления в потоке интерфейса. Используйте async/await.
  • Неосвобождённые ресурсы. Диалоги, потоки и подключения оборачивайте в using.
  • Проверка ввода только на стороне интерфейса. Данные из полей проверяйте перед использованием: int.TryParse вместо int.Parse.

Частые вопросы

С чего начать изучение оконных приложений на C#?

Сначала освойте основы языка: переменные, условия, циклы, классы. Затем установите Visual Studio, создайте проект Windows Forms на .NET 8 и сделайте простую программу, например калькулятор или список дел. Следующий шаг - работа с файлами и базой данных.

Что лучше выбрать: WinForms или WPF?

WinForms быстрее осваивается и подходит для внутренних утилит и учебных задач. WPF стоит выбирать для сложных интерфейсов, тем оформления и больших проектов с MVVM. Оба варианта поддерживаются в .NET 8.

Можно ли делать оконные приложения без Visual Studio?

Да. Проект создаётся командой dotnet new winforms или dotnet new wpf, редактируется в Rider или VS Code и запускается через dotnet run. Но визуальный конструктор форм в полном виде есть только в Visual Studio.

Будет ли оконное приложение работать на Linux или macOS?

Windows Forms и WPF рассчитаны на Windows. Для кроссплатформенных программ берите Avalonia UI или .NET MAUI.

Почему окно зависает при выполнении кнопки?

Код выполняется в потоке интерфейса, и окно не может перерисоваться. Вынесите долгую работу в Task.Run или используйте асинхронные методы с await.

Как превратить проект в один exe-файл?

Выполните публикацию с параметрами --self-contained true и -p:PublishSingleFile=true. Для маленького размера можно включить обрезку неиспользуемого кода, но в WPF и WinForms её нужно проверять тестами.