Не Windows единой: как писать кроссплатформенные приложения с GUI на C#
На C# можно создавать красивые приложения, но до недавних пор — не для всех платформ. Рассказываем, как писать одно приложение для всех ОС сразу.
Пока есть Xamarin, который можно использовать только для Windows 10 и мобильных устройств. Но что делать тем, кто хочет создавать графические интерфейсы для Linux или Mac OS?
Тут помогут фреймворки от сторонних разработчиков.
Пишет о программировании, в свободное время создает игры. Мечтает открыть свою студию и выпускать ламповые RPG.
Какой фреймворк выбрать
Мне удалось найти 2 более-менее популярных фреймворка (оба основаны на Skia ):
Я попробовал оба, и второй показался мне более удобным: в нём есть язык разметки, поддержка MVVM, быстрая установка, лёгкий переход с WPF. Поэтому я выбрал его.
Как начать использовать AvaloniaUI
Для начала клонируйте себе на компьютер этот репозиторий:
В нём находятся шаблоны для создания приложения с AvaloniaUI. Если вы не умеете пользоваться git, то просто скачайте содержимое и распакуйте куда-нибудь на компьютере. Затем откройте консоль и введите следующую команду:
Она установит шаблоны для создания приложения. Чтобы проверить, добавились ли шаблоны, используйте команду:
Откройте в консоли папку, в которой хотите создать проект, и введите:
Будет создано приложение с использованием MVVM. Практически вся документация по AvaloniaUI написана с использованием этого паттерна, поэтому проще будет разрабатывать на нём.
Теперь можно приступать к работе над приложением.
Создаём калькулятор на AvaloniaUI
У вас будут созданы следующие папки
Сначала посмотрим в файл Program.cs в корневом каталоге:
Нас интересует метод AppMain(). В нём создаётся окно (MainWindow) с указанием DataContext (используется для привязки данных), а потом это окно запускается.
В этом методе можно определить свою логику инициализации приложения. Например, объявить экземпляр модели и передать его в конструктор MainWindowViewModel(). Однако перед этим нужно определить конструктор, который будет принимать такой аргумент.
У нас очень простое приложение, поэтому мы реализуем всю логику прямо в MainWindowViewModel.cs. Там будут необходимые свойства и методы.
Для начала нужно подключить пространство имён ReactiveUI, которое в AvaloniaUI используется для реализации паттерна MVVM:
Кроссплатформенный GUI на C# и веб-технологии
Зачем?
Идентичный интерфейс под всеми платформами. Возможны лишь незначительные отличия при отрисовке шрифтов, при отображении элементов. Последнее всегда объясняется ошибками в верстке.
Разработка под одну ОС. Эмпирическим путем нами было выявлено, что достаточно вести основную разработку под Windows, а под остальными платформами лишь иногда проверять. Например, перед релизом.
Вся сила веб-разработки. Особенно это актуально, если команда состоит из веб-разработчиков. Можно использовать html5, css3, привычные подходы и библиотеки. Мы, кстати, используем популярный фреймворк для построения веб-приложений, в итоге у нас интерфейс только на js.
Разделение на frontend и backend. Появляется возможность вести отдельно разработку представления и логики приложения, согласовав апи. Например, наш интерфейс — это полноценное веб-приложение, взаимодействующее с «сервером» через ajax-запросы. В десктоп приложении эмулируем обработку этих запросов. Таким образом, можно разрабатывать и отлаживать интерфейс с использованием инструментов разработчика в Chrome, закинув необходимые mock ответы на локальный сервер. Особо уверенные в себе разработчики, которым достаточно доступа к dom и консоли, могут использовать firebug lite в десктоп приложении.
Есть о чем написать на хабр. Подобные эксперименты добавляют азарта при разработке и скрашивают суровые будни программиста.
Под каждую платформу создаем нативное приложение, GUI которого состоит из одного элемента пользовательского интерфейса – браузера, растянутого во все окно.
Нам нужно научиться отображать html в браузере, найти способ осуществлять вызовы js-C# и С#-js. Различия в вызовах могут показаться странными, но есть простое объяснение – в используемых браузерах реализован и работает разный функционал.
Mac OSX
Определяем методы и указываем, какие из них могут быть вызваны из js:
Теперь в js мы можем вызвать метод сallFromJs:
Ubuntu
Под Mono используем пакет webkit-sharp.
Плавно увеличивается количество не интуитивно понятного кода.
Для вызова C# из js можно перехватывать переход по ссылке.
Вызов из js будет выглядеть так:
Еще один способ завязывается на событие TitleChanged.
В js устанавливаем title у документа:
В С# срабатывает событие TitleChanged, мы десериализуем title и аналогично предыдущему подходу вызываем обработчик.
В рассматриваемой обертке WebKit можно из С# исполнять любой js код, что позволяет нам реализовать вызов js из C#:
Windows
Основную разработку мы ведем под Windows.
Подробности уже были описаны моим коллегой год назад и за это время принципиально ничего не изменилось. В какой-то степени это свидетельствует о надежности подхода. Также в статье больше деталей, которые вполне достаточно рассмотреть на примере одной ОС.
Я лишь добавлю пример на github.
Создание приложения Windows Forms на C# в Visual Studio
В рамках этого краткого знакомства с возможностями интегрированной среды разработки Visual Studio (IDE) вы создадите простое приложение на C# с пользовательским интерфейсом на основе Windows.
Установите Visual Studio бесплатно со страницы скачиваемых материалов Visual Studio, если еще не сделали этого.
Установите Visual Studio бесплатно со страницы скачиваемых материалов Visual Studio, если еще не сделали этого.
На некоторых снимках экрана в этом учебнике используется темная тема. Если вы не используете темную тему, но хотите переключиться на нее, см. страницу Персонализация интегрированной среды разработки и редактора Visual Studio.
Установите Visual Studio 2022 бесплатно со страницы скачиваемых материалов Visual Studio 2022, если еще не сделали этого.
На некоторых снимках экрана в этом учебнике используется темная тема. Если вы не используете темную тему, но хотите переключиться на нее, см. страницу Персонализация интегрированной среды разработки и редактора Visual Studio.
Создание проекта
Сначала вы создадите проект приложения на C#. Для этого типа проекта уже имеются все нужные файлы шаблонов, что избавляет вас от лишней работы.
Откройте Visual Studio 2017.
В верхней строке меню последовательно выберите Файл > Создать > Проект.
Запустите Visual Studio.
На начальном экране выберите Создать проект.
В окне Создать проект выберите шаблон Приложение Windows Forms (.NET Framework) для C#.
(При желании вы можете уточнить условия поиска, чтобы быстро перейти к нужному шаблону. Например, введите Приложение Windows Forms в поле поиска. Затем выберите C# в списке языков и Windows в списке платформ.)
Если шаблон Приложение Windows Forms (.NET Framework) отсутствует, его можно установить из окна Создание проекта. В сообщении Не нашли то, что искали? выберите ссылку Установка других средств и компонентов.
Затем нажмите кнопку Изменить в Visual Studio Installer. Вам может быть предложено сохранить результаты работы; в таком случае сделайте это. Выберите Продолжить, чтобы установить рабочую нагрузку. После этого вернитесь к шагу 2 в процедуре Создание проекта.
В поле Имя проекта окна Настроить новый проект введите HelloWorld. Затем нажмите Создать.
Новый проект открывается в Visual Studio.
Создание приложения
Когда вы выберете шаблон проекта C# и зададите имя файла, Visual Studio открывает форму. Форма является пользовательским интерфейсом Windows. Мы создадим приложение Hello World, добавив элементы управления на форму, а затем запустим его.
Добавление кнопки на форму
Щелкните Панель элементов, чтобы открыть всплывающее окно «Панель элементов».
(Если параметр для всплывающего окна Панель элементов отсутствует, его можно открыть в строке меню. Для этого выберите Вид > Панель элементов. Либо нажмите клавиши CTRL+ALT+X.)
Щелкните значок Закрепить, чтобы закрепить окно Панель элементов.
Выберите элемент управления Кнопка и перетащите его на форму.
(Если окно Свойства не отображается, его можно открыть в строке меню.) Для этого выберите Вид > Окно свойств. Или нажмите клавишу F4.)
Добавление метки на форму
Теперь, когда мы добавили элемент управления »Кнопка» для создания действия, давайте добавим элемент управления «Метка», куда можно отправлять текст.
Выберите элемент управления Метка в окне Панель элементов, а затем перетащите его на форму и расположите под кнопкой Нажмите это.
В разделе Проект или (DataBindings) окна Свойства измените имя Label1 на lblHelloWorld и нажмите клавишу ВВОД.
Добавление кода на форму
В окне Form1.cs [Проект] дважды щелкните кнопку Нажмите это, чтобы открыть окно Form1.cs.
(Кроме того, можно развернуть узел Form1.cs в обозревателе решений, а затем выбрать Form1.)
Запуск приложения
Нажмите кнопку Запустить, чтобы запустить приложение.
Будет выполнено несколько операций. В интегрированной среде разработки Visual Studio откроются окна Средства диагностики и Вывод. Кроме того, вне этой среды откроется диалоговое окно Form1. Оно будет содержать вашу кнопку Нажмите это и текст Label1.
Закройте диалоговое окно Form1, чтобы завершить работу приложения.
Следующие шаги
Для получения дополнительных сведений перейдите к следующему руководству:
Двумерная графика на C#, классы Graphics, Pen и Brush
На примере графики наглядно видны преимущества ООП, смысл использования классов, их методов и свойств. Добавляя в пространство имен своего проекта соответствующие библиотеки, вы получаете сразу набор инструментов, необходимых для графики. Это графические примитивы (линии, прямоугольники, эллипсы и т.п.), перо для черчения, кисть для закраски и много других полезных объектов и методов.
2D-графика делится, как вы знаете, на растровую и векторную. Растровое изображение — это набор цветных пикселей, заданных в прямоугольной области, хранящихся в файлах *.bmp, *.jpg, *.png и т.п. Самый простой растровый редактор — программа Paint. Векторная графика намного экономнее (по объемам памяти) растровой. Так для рисования прямоугольника достаточно задать координаты двух точек (левого верхнего и правого нижнего углов) и цвет и толщину линии. В этом разделе в основном рассмотрим методы векторной графики.
Класс Graphics предоставляет методы рисования на устройстве отображения (другие термины — графический контекст, «холст»). Определимся сразу, на чем мы хотим рисовать. Далее в примерах он обозначается как объект g.
Способы задания «холста»
1. Графический объект — «холст» для рисования на форме Form1 (указатель this) можно задать, например, одним оператором:
Graphics g = this.CreateGraphics();
Примечание. Заметим, что стандартным образом
Graphics g = new Graphics();
создать объект-холст не удается.
На этом операторе генерируется ошибка:
Для типа «System.Drawing.Graphics» не определен конструктор.
2. Еще пример задания графического контекста на визуальном компоненте PictureBox (ящик для рисования) через растровый объект класса Bitmap. В классе Form1 зададим два объекта:
Graphics g; // графический объект — некий холст
Bitmap buf; // буфер для Bitmap-изображения
В конструктор Form1() добавим следующие операторы:
buf = new Bitmap(pictureBox1.Width, pictureBox1.Height); // с размерами
g = Graphics.FromImage(buf); // инициализация g
3. В принципе, иногда (если все графические операции выполняются внутри одной функции) эти четыре строки могут быть заменены одной строкой:
Graphics g = Graphics.FromImage(new Bitmap(pictureBox1.Width, pictureBox1.Height));
После этого можно задать фон холста белым:
g.Clear(Color.White);
4. Еще один пример задания «холста» на форме через дескриптор окна:
Graphics g = Graphics.FromHwnd(this.Handle);
Далее в примерах конкретизируются эти способы.
Объекты других классов из библиотеки System.Drawing
Такие классы, как Rectangle (прямоугольник) и Point (точка) инкапсулируют элементы GDI+. Отметим, что Point вообще то является структурой (struct) с полями x,y. Это уточнение несущественно, так как в C# структуры похожи на классы, a инициализация объекта-структуры point может выглядеть так же, как инициализация объекта-класса:
Point point= new Point();
Класс Pen (перо) используется для рисования линий и кривых, а классы, производные от класса Brush (кисть) используются для закраски замкнутых контуров (см. ниже).
Класс GraphicsPath позволяет задавать последовательность соединенных линий и кривых, класс Region описывает внутреннюю часть графической формы, состоящей из многоугольников и контуров.
Класс Image – абстрактный базовый класс предоставляет функциональные возможности для производных классов Bitmap и Metafile. Bitmap используется для работы с пиксельными изображениями (см. выше пример). Metafile определяет графический метафайл, содержащий записи, описывающие последовательность графических операций, которые могут быть записаны (созданы) и воспроизведены (отображается). Этот класс не наследуется.
Класс Graphics
Он инкапсулирует поверхность рисования GDI+. Этот класс не наследуется. Методов в этом классе огромное количество, поэтому сначала представим их в таблице, а затем рассмотрим некоторые из них с примерами и пояснениями.
В третьем столбце таблицы указывается число перегрузок метода, различающихся набором параметров (используйте интеллектуальную подсказку IntelliSense для выбора нужного Вам варианта метода).
| Имя метода | Описание | Число перегрузок |
| Clear(Color) | Очищает всю поверхность рисования и выполняет заливку поверхности указанным цветом фона. | 1 |
| CopyFromScreen(Point, Point, Size) | Выполняет передачу данных о цвете, соответствующих прямоугольной области пикселей, блоками битов с экрана на поверхность рисования объекта Graphics. | 4 |
| Dispose() | Освобождает все ресурсы, используемые данным объектом Graphics. | 1 |
| DrawArc(Pen, Rectangle, Single, Single) | Рисует дугу, которая является частью эллипса, заданного структурой Rectangle. | 4 |
| DrawBezier(Pen, Point, Point, Point, Point) | Рисует кривую Безье, определяемую четырьмя структурами Point. | 3 |
| DrawBeziers(Pen, Point[]) | Рисует несколько (N) кривых Безье, определяемых массивом из (3N+1) структур Point. | 2 |
| DrawCloseCurve(Pen, Point[ ]) | Рисует замкнутый фундаментальный сплайн | 4 |
| DrawEllipse(Pen, Rectangle) | Рисует эллипс | 4 |
| DrawIcon(Icon, Rectangle) | Рисует значок | 2 |
| DrawImage(Image image, int x, int y) | Рисует заданное изображение image, используя его фактический размер в месте с координатами (x,y) | 30 |
| DrawLine(Pen, Point, Point) | Проводит линию, соединяющую две структуры Point. | 4 |
| DrawLines(Pen, Point[ ]) | Рисует набор сегментов линий, которые соединяют массив структур Point. | 2 |
| DrawPath(Pen, gp) | Рисует пером Pen объект GraphicsPath gp. | 1 |
| DrawPie(Pen, Rectangle, Single, Single) | Рисует сектор, который определяется эллипсом, заданным структурой Rectangle и двумя радиалtьными линиями. | 4 |
| DrawPolygon(Pen, Point[]) | Рисует многоугольник, определяемый массивом структур Point. | 2 |
| DrawRectangle(Pen, Rectangle) | Рисует прямоугольник, определяемый структурой Rectangle. | 3 |
| DrawRectangles(Pen, Rectangle[]) | Рисует набор прямоугольников, определяемых структурами Rectangle. | 2 |
| DrawString(String, Font, Brush, PointF) | Создает указываемую текстовую строку в заданном месте с помощью определяемых объектов Brush и Font. | 6 |
| Equals(Object) | Определяет, равен ли заданный объект текущему объекту. (Унаследовано от Object.) | 1 |
| ExcludeClip(Rectangle) | Обновляет вырезанную область данного объекта Graphics, чтобы исключить из нее часть, определяемую структурой Rectangle. | 1 |
| ExcludeClip(Region) | Обновляет вырезанную область данного объекта Graphics, чтобы исключить из нее часть, определяемую структурой Region. | 1 |
| FillClosedCurve(Brush, Point[]) | Заполняет внутреннюю часть замкнутой фундаментальной кривой, определяемой массивом структур Point. | 6 |
| FillEllipse(Brush, Rectangle) | Заполняет внутреннюю часть эллипса, определяемого ограничивающим прямоугольником, который задан структурой Rectangle. | 4 |
| FillPath(Brush, GraphicsPath) | Заполняет внутреннюю часть объекта GraphicsPath. | 1 |
| FillPie(Brush, Rectangle, Single, Single) | Заполняет внутреннюю часть сектора, определяемого эллипсом, который задан структурой RectangleF, и двумя радиальными линиями. | 3 |
| FillPolygon(Brush, Point[]) | Заполняет внутреннюю часть многоугольника, определяемого массивом точек, заданных структурами Point. | 4 |
| FillRectangle(Brush, Rectangle) | Заполняет внутреннюю часть прямоугольника, определяемого структурой Rectangle. | 4 |
| FillRegion(Brush, Region) | Заполняет внутреннюю часть объекта Region. | 1 |
| Flush() | Вызывает принудительное выполнение всех отложенных графических операций и немедленно возвращается, не дожидаясь их окончания. | 2 |
| IntersectClip(Region) | Обновляет вырезанную область данного объекта, включая в нее пересечение текущей вырезанной области и указанной структуры | 3 |
| ResetClip() | Сбрасывает выделенную область g, делая ее бесконечной | 1 |
Подробнее еще о двух классах.
Класс Pen
Класс Pen определяет объект, используемый для рисования прямых линий и кривых. Этот класс не наследуется. Конструкторы класса (тоже пример перегрузки методов):
1) Pen(Color) инициализирует новый экземпляр класса Pen с указанным цветом.
2) Pen(Color, Single) инициализирует новый экземпляр класса Pen с указанными свойствами Color и Width. (Width — устанавливает ширину пера Pen, в единицах объекта Graphics, используемого для рисования
Например:
Pen redPen = new Pen(Color.Red); // толщина пера по умолчанию 1 пиксель
или используя промежуточную переменную green
Color green = Color.Green;
Pen greenPen = new Pen(green, 4.5f);
Можно вводить новый объект без указания явного имени пера (пример создания динамического объекта — пера):
g.DrawRectangle(new Pen(Color.Red, 3), r);
например, для рисования прямоугольника r красным пером толщиной 3 пикселя, используя графический контекст («холст») g.
Класс Brush
Класс Brush определяет объекты, которые используются для заливки внутри графических фигур, таких как прямоугольники, эллипсы, круги, многоугольники и дорожки. Это абстрактный базовый класс, который не может быть реализован. Для создания объекта «кисть» используются классы, производные от Brush, такие как SolidBrush, TextureBrush и LinearGradientBrush, например:
SolidBrush br = new SolidBrush(Color.Aqua);
g.FillRectangle(br, r); // закраска прямоугольника r на «холсте» g
Или без явного объявления объекта «кисть» br:
g.FillRectangle(new SolidBrush(Color.Red), r);
Для первоначального привыкания к стилю ООП в графике рассмотрим первый пример рисования.
Графическое приложение на си шарп
Рисование закрашенного сплайна
Рисование закрашенной области типа Region
Пример использования метода FillEllipse мы приводили ранее в разделе «Рисование в окне элемента управления» этой главы. Вот еще один пример:
Однако прежде чем мы займемся рисованием, необходимо сделать маленькое отступление и рассказать о ресурсах приложения.
Если Вы раньше создавали приложения Microsoft Windows, то знаете, что формат загрузочного модуля таких приложений сложнее формата загрузочного модуля программ MS-DOS. Кроме выполняемого кода и констант в загрузочном модуле приложения Microsoft Windows находятся дополнительные данные — ресурсы.
Приложение Microsoft Windows может хранить в виде ресурсов текстовые строки, значки, курсоры, графические изображения, меню, диалоговые окна, произвольные массивы данных и т. д. Физически ресурсы находятся внутри exe-файла приложения. Они могут загружаться в оперативную память автоматически при запуске приложения или по запросу приложения (явному или неявному). Такой механизм обеспечивает экономное использование оперативной памяти, так как все редко используемые данные можно хранить на диске и загружать в память только при необходимости.
Например, приложение может иметь сложную систему диагностики ошибочных ситуаций, содержащую различные диалоговые окна, массивы сообщений об ошибках и т. п. Когда приложение работает без ошибок (что, очевидно, является нормальным явлением) ненужные диагностические ресурсы спокойно лежат на диске, не перегружая оперативную память. Но как только возникает ошибка, и приложение вызовет функцию обработки ошибки, эти ресурсы будут автоматически загружены.
Ресурсы можно редактировать без повторной трансляции программного проекта. Это означает, что Вы сможете легко перевести все сообщения и тексты меню приложения на другой национальный язык, отредактировать графические изображения или любые другие ресурсы, даже не имея в своем распоряжении исходные тексты.
Если приложение должно рисовать в своих окнах значки или графические изображения, целесообразно включить их в ресурсы приложения. В этом случае данные из файлов, содержащих значки или изображения, будут переписаны в файл сборки приложения. Таким образом, Вы сможете поставлять приложение как единый загрузочный файл, не «комплектуя» его дополнительно файлами графических изображений.
Щелкните этот файл левой клавишей мыши, а затем установите значение свойства Build Action для файла значка равным Embedded Resource (рис. 10-18).
Рис. 10-18. Включение значка в ресурсы приложения
В результате выполнения этих действий файл значка (в нашем случае это был файл FACE 02. ICO ) будет добавлен в ресурсы приложения.
Что же касается второго параметра, то через него мы передаем имя ресурса, которое в нашем случае будет совпадать с именем файла значка.
Функция DrawIcon рисует значок myIcon в точке, где был курсор мыши в момент щелчка (рис. 10-19).
Рис. 10-19. Использование метода DrawIcon
Всего существует два перегруженных метода DrawIcon :
public void DrawIcon(Icon, Rectangle);
public void DrawIcon(Icon, int, int);
В первом варианте метода параметр типа Rectangle задает прямоугольную область, внутри которой будет нарисован значок. При этом значок будет масштабирован таким образом, чтобы занять собой всю заданную область.
g.DrawIcon(myIcon, new Rectangle(0, 0, 100, 100));
Если исходный значок меньше указанных размеров, при рисовании он будет растянут, а если больше — сжат.
Для рисования значков можно также использовать метод DrawIconUnstretched :
public void DrawIconUnstretched(Icon, Rectangle);
В ОС Microsoft Windows используются два формата изображений — аппаратно-зависимый DDB ( D evice- D ependent B itmap) и аппаратно-независимый DIB ( D evice- I ndependent B itmap).
Согласно определению, данному в документации, изображение DDB есть набор битов в оперативной памяти, который может быть отображен на устройстве вывода (например, выведен на экран видеомонитора или распечатан на принтере). Внутренняя структура изображения DDB жестко привязана к аппаратным особенностям устройства вывода. Поэтому представление изображения DDB в оперативной памяти полностью зависит от устройства вывода. Иногда такие изображения называют растровыми, подчеркивая тот факт, что их можно рассматривать как совокупность строк растра (горизонтальных линий развертки).
Если бы в Microsoft Windows можно было работать только с изображениями DDB, было бы необходимо иметь отдельные наборы изображений для каждого типа видеоадаптера и каждого видеорежима, что, очевидно, крайне неудобно.
Аппаратно-независимое изображение DIB содержит описание цвета пикселов изображения, которое не зависит от особенностей устройства отображения. ОС Microsoft Windows после соответствующего преобразования может нарисовать такое изображение на любом устройстве вывода. Несмотря на некоторое замедление процесса вывода по сравнению с выводом изображений DDB, универсальность изображений DIB делает их весьма привлекательными для хранения изображений.
Сравнивая эти форматы, отметим, что каждый из них имеет свои преимущества и свои недостатки.
Растровые изображения, как правило, выводятся на экран быстрее, так как их внутренняя структура аналогична (до некоторой степени) структуре видеопамяти. К недостаткам растровых изображений можно отнести большой объем памяти, требующийся для их хранения (которых доходит до десятков и сотен Мбайт), невозможность масштабирования без потери качества изображения, а также сложность выделения и изменения отдельных объектов изображения.
Таблица 10-4. Доступные форматы графических файлов
Растровые изображения DIB (Device-Independent Bitmap)
















