"ААА! Пришло время переписывать на .NET Core?", говорили они, WPF в комментариях обсуждали. Так давайте же проверим, можно ли написать кросс-платформенное GUI приложение на .NET / C#.
Новогоднее настроение навеяло идею сделать анимацию падающего снега. Были такие демки под DOS, горящий огонь, фракталы, снежок, падающий на ёлочку, и так далее.
Как увидим ниже, это не только весело, но и позволит испытать ключевой функционал UI фреймворка. Поехали!
Создание проекта и UI
Для Avalonia есть Visual Studio Extension с шаблоном проекта. Устанавливаем, создаём Avalonia .NET Core Application
. Видим привычные по WPF App.xaml
и MainWindow.xaml
. Однако, проект содержит <TargetFrameworks>netcoreapp1.1;net461</TargetFrameworks>
, меняем на <TargetFramework>netcoreapp2.0</TargetFramework>
, мы же не в каменном веке.
Расширение Avalonia для студии содержит XAML Designer, но у меня он не заработал. Решарпер немного сходит с ума в редакторе разметки, хочет везде вставить явные неймспейсы, так что и без него тоже обойдёмся.
В остальном у нас в руках привычный XAML с привычными контролами и пропертями. Обо всех отличиях можно почитать в документации.
Для произвольного рисования существует одноимённый аналог WriteableBitmap
из WPF. Огромный плюс в том, что нет проблем рисовать в нём из любого потока, выглядит это так:
<Image Source="{Binding Bitmap}" Stretch="Fill" />
using (ILockedFramebuffer buf = writeableBitmap.Lock())
{
uint* ptr = (uint*) buf.Address;
// Рисуем
*ptr = uint.MaxValue;
}
Однако Image
, который привязан к нашему writeableBitmap, не обновится сам по себе, ему необходимо сказать InvalidateVisual()
.
Таким образом, мы можем рисовать анимацию в фоновом потоке, не нагружая UI thread. Помимо Image
добавим пару слайдеров для управления скоростью падения снега и количеством снежинок, здесь всё стандартно, {Binding Mode=TwoWay}
. Плюс кнопка "начать заново", тоже стандартная привязка к ICommand
. Замечу, что использованы векторные иконки на XAML, скопипащенные из гугла, <Path>
фунциклирует как положено.
Разметка целиком: MainWindow.xaml
Снежный алгоритм
"Физика"
Двигаем снежинку вниз на один пиксель. Если пиксель уже занят "лежащей" снежинкой, проверяем точки слева и справа, и двигаем туда, где свободно. Всё занято — помечаем точку как "лежащую". Таким образом достигается скатывание снежинок с наклонных поверхностей.
Параллакс
Для достижения объёмного эффекта зададим каждой снежинке рандомную скорость. Чем скорость ниже, тем более тёмный оттенок используем для рисования.
Чтобы наша "физика" работала корректно, необходимо перемещать снежинки не более, чем на 1 пиксель за кадр. То есть самые быстрые снежинки двигаются каждый кадр на пиксель, остальные — пропускают некоторые кадры. Для этого можно применить float
координаты и просто перерисовывать каждую снежинку на каждый кадр. Вместо этого я использую два целочисленных short
поля и перерисовываю снежинку только если она реально сдвинулась.
Рендеринг
Основная идея — избежать полной перерисовки кадра. Нам надо как-то хранить "лежащий" снег, нарисованные пользователем точки, загруженные изображения (да, можно рисовать мышью и грузить пикчи правым кликом — снежок будет прилипать к ёлочкам и надписям).
Простое и эффективное решение — использовать сам WriteableBitmap
. "Перманентные" пиксели пусть будут полностью непрозрачными (A = 255), а для движущихся снежинок A = 254.
Падающих снежинок всегда фиксированное количество, позицию и скорость каждой храним в массиве. В итоге, если снежинка сдвинулась — стираем точку на старой позиции и рисуем на новой. Если превратилась в "лежачую" — выставляем альфа-канал точки в 255, перемещаем живую снижинку обратно наверх.
Как это запустить?
Благодаря возможности рисования прямо "по живому" получилась доволно залипательная штука, попробуйте :)
Для всех ОС инструкция одинаковая:
- Установить .NET Core SDK
git clone https://github.com/ptupitsyn/let-it-snow.git
cd let-it-snow/AvaloniaCoreSnow
dotnet run
Заключение
.NET Core молод, Avalonia ещё в альфе, но уже сейчас эти инструменты решают поставленную задачу! Код простой и понятный, никаких хаков и лишних приседаний, прекрасно работает на Windows, macOS, Linux.
Альтернативы?
- Qt (посложнее будет в использовании)
- Java (нет нормального unsafe)
- Electron (JavaScript + HTML — нет уж, спасибо)
UI в данной демке очень прост, но он использует несколько наиболее важных фич:
- Layout (Grid, StackPanel, выравнивание) — основа вёрстки
- Binding (привязка контролов к свойствам модели)
- ItemsControl, ItemsPanel, DataTemplate — работа с коллекциями данных
- WriteableBitmap — прямая работа с изображениями
- OpenFileDialog, нативный на каждой платформе
Этого уже достаточно, чтобы построить UI любой сложности. Так что можем сказать, что закрыт последний пробел в экосистеме .NET: есть возможность создавать веб (ASP.NET Core), мобильные (Xamarin) и десктопные (Avalonia) приложения, при этом переиспользовать код, располагая его в библиотеках .NET Standard.
Ссылки
Комментарии (46)
DROS
29.12.2017 10:55Напомнило демку с диска к книге Страуструпа за 99 год вроде. Там аналогичный эффект был, правда без паралакса и под DOS. Хороший был диск. А сколько там было исходников вирусни того времени — ух…
eyeofhell
29.12.2017 11:10+1Ухты. А как Avalonia это делает? Dotnet core — он же GUI биндингов не имеет? Они что, через интроспекцию лезут напрямую в ось, в рантайме проверяют что за ось и в рантайме же биндятся к нужным GUI апишкам?
kekekeks
29.12.2017 11:25+2Там всё достаточно примитивно в плане детекта —
RuntimePlatform.GetRuntimeInfo().OperatingSystem
выдаёт текущую операционку. А дальше создаются окошки уже через P/Invoke либо к Win32 API, либо к GTK3, либо к Cocoa.
Подробнее по архитектуре см. с 15:00 здесь
eyeofhell
29.12.2017 11:26Ага, то есть «P/Invoke». Неторопливая штука в целом. Не факт, что быстрее Electron.
kekekeks
29.12.2017 11:41+2Вы точно понимаете, как работает P/Invoke? Вы точно понимаете, как именно работает электрон и почему именно он тормозит?
eyeofhell
29.12.2017 11:45Если .net хочет вызвать, к примеру, «SendMessageW» на винде, то она делает преобразования для всех аргументов (потому что строка в .net и null-terminated UCS-2 в WinAPI — это разные штуки), потом вызов, потом преобразование возвращаемых знаечений, если есть. Electron — это просто Chromium. В нем оптимизированный движок для рендеринга HTML, он компилирует JS в нативный код, все взаимодействие между скомпилированным JS и HTML бесшовно.
Но специально я не сравнивал — любопытствую. Демка из статьи выше у меня запустилась сильно медленнее, чем стартует Visual Studio Code. Да, воторой запуск такой же медленный, как первый :)kekekeks
29.12.2017 11:57+1потому что строка в .net и null-terminated UCS-2 в WinAPI — это разные штуки
На самом деле нет, дотнетные строки нуль-терминированы, вы можете взять указатель и передать как есть. Да, в ряде случаев маршалинг нужен, но это контролируемый процесс. Так, всё взаимодействие с DirectX у нас идёт через msil-инструкцию calli, которая выполняет прямой вызов по указателю без автоматических преобразований.
Демка из статьи выше у меня запустилась сильно медленнее, чем стартует Visual Studio Code
А вы её запускали через
dotnet run
? Он каждый раз дёргает MSBuild и кучу ненужностей, чтобы определить, надо ли пересобрать проект (и обычно пересобирает даже тогда, когда не надо). Процесс этот не быстрый. Сделайтеdotnet publish
под нужную платформу и уже собранный релизный вариант запустите.kefirr Автор
29.12.2017 12:03+1msil-инструкцию calli, которая выполняет прямой вызов по указателю без автоматических преобразований
Интересно, а где можно на это посмотреть?
kekekeks
29.12.2017 12:07+2См. SharpGenTools, которым обрабатывается используемый нами SharpDX в процессе сборки. Мы себя отдельно SharpGenTools используем в AvalonStudio для генерации кода интеропа с libdbgshim/ICorDebug для отладки кода под .NET Core.
kefirr Автор
29.12.2017 11:50"Неторопливая" — очень относительное понятие. Да, PInvoke медленный по сравнению с прямым вызовом функции, он добавляет от 10 до 30 инструкций (см MSDN). Но надо понимать, что эта разница — порядка наносекунд, и в случае вызова функций сложнее
a+b
просто теряется. Например, Ignite.NET работает с JVM через PInvoke, и тормозов от этого не испытывает.
Electron:
- тащит за собой целый браузер, жрёт немеряно памяти и создаёт новый процесс на каждый чих
- C# быстрее JavaScript и лучше во всех отношениях
- XAML лучше HTML
Другое дело, что layout & rendering в Electron действительно может быть быстрее для некоторых сценариев, браузерный движок очень круто заоптимизирован под это дело.
kekekeks
29.12.2017 12:09+2layout & rendering в Electron действительно может быть быстрее
Так рендерилку (Skia) мы у них в цельнотянутом виде утащили. На не-windows-платформах используется.
KvanTTT
29.12.2017 13:48+4Спасибо за статью и рекламу Avalonia! Очень нравится концепция, надеюсь выстрелит.
sunman
29.12.2017 14:41+2Классический эффект ранних интро.
Единственное — в этом эффекте у снежинок обычно добавляли небольшие флуктуации по горизонтали — как это и выглядит в натуре, если нет ветра.
Добавил эту фичу и сделал pull-request: https://github.com/ptupitsyn/let-it-snow/pull/1/commits
crea7or
29.12.2017 19:38Господа, а нормальный-то WPF не планируется к .net core что ли?
kekekeks
29.12.2017 22:00Нет. Вместо него поверх CoreCLR работает UWP. Который нигде кроме Windows 10 работать не планирует.
izzholtik
29.12.2017 23:03Есть ли шанс поддержки вайном?
kekekeks
29.12.2017 23:27Где? В .NET Core? Или поддержка вайном UWP? Первое технически слабореализуемо (Mono уже пытались когда-то давно реализовать поддержку WinForms поверх вайна, получилось плохо), во второе слабо верится, ввиду общей непопулярности UWP как платформы.
izzholtik
30.12.2017 19:44UWP. Майки будут продвигать сабж со временем. Если под вайном это сделать невозможно, печаль будет полная, вплоть до, лол, смерти десктопного линукса.
kekekeks
30.12.2017 20:15Они его продвигать могут сколько угодно, но разработчиков под UWP от этого не прибавится. По фичам он проигрывает WPF всухую, а единственную платформу, ради которой это дело хоть кто-то использовал, благополучно похоронили.
izzholtik
30.12.2017 20:37Под win S запускаются только приложения рз маркета.
- скоро появятся планшеты на ARM, где WPS будут иметь преимущество над win32.
kekekeks
30.12.2017 20:45Под win S запускаются только приложения рз маркета.
Через Desktop Bridge на этой Win S хоть Windows Forms можно запустить.
keydon2
29.12.2017 23:27А расскажите что мешает например сделать биндинги к Qt(зачем ещё одна графическая либа)? Известный фреймворк, сделан профессионально, из коробки до кучи фич от лайв редактирования до трансляции в браузер. Кастомизируется по самое нехочу. Отличная лицензия. Из недостатков только что веб в один клик не запилить(хотя зная современный веб это можно считать плюсом) и что заставляет под себя подстраиваться (хотелось бы более универсальное как стандартная библиотеки, чтобы не тянуть зависимости ядра, а сразу заюзал функционал в любой проект без остального qt).
kekekeks
30.12.2017 09:40+5Дата-биндингов нормальных нет, шаблонов нормальных нет, lookless-контролов нет, т. е. идеологически Qt застрял где-то во временах Windows Forms и Delphi 7. А так хороший фреймворк, никто не спорит.
w1ld
02.01.2018 13:19+1Здорово! Приятно видеть, что UI dotNet с XAML можно запускать на этих платформах.
SirEdvin
А как же PyQt? Вроде довольно неплох.
DarkByte2015
PyQt это опять же — Qt. Лично по мне так он довольно сложен для понимания, особенно QML. Все-таки самый лучший способ писать интерфейс разметкой — это xml (и его расширения типа XAML и т.д.). А вот в json разметку еще и намешанную с js воспринимать очень тяжело.
zorn_v
XML легче читать чем json… Вы это серьезно?
kefirr Автор
В JSON даже комментарии нельзя вставлять, не говоря уже о схемах и прочем.
Для передачи данных да, он компактнее и, может, более читаем. Но для разметки, конфигов я предпочту XML.
lassana
В QML-то есть комментарии, безо всяких извращений вида <!--… -->
spmbt
Комментарий в JSON — {...«node»:«нечто по этому узлу», «nodeCOMMENT»:«Комментарий»...}
evnuh
это не комментарий
kekekeks
У JSON в плане использования как языка разметки есть фатальные недостатки — неименованые типы объектов и отсутствие встроенных текстовых нод. Из-за этого при определении списка элементов приходится писать что-то типа
вместо
Dima_Sharihin
В QML это будет
Единственный косяк QML — возможность писать код прямо в разметке — так и поощряет быдлокодить и приходится каждый раз себя бить по рукам для разделения вида контролов и их логики
ad1Dima
Это выглядит лучше чем json, но я бы не сказал, что лучше, чем XAML. Примерно одинаково и дело привычки. (хотя я бы в вашем примере еще комментировал закрывающие скобки, если большая вложенность)
Dima_Sharihin
QML позволяет быдлокодить примерно так:
Что в XAML получится кудааа длиннее. Но в то же время WPF заставит сделать аккуратно, а QML на все это пофиг. Поэтому от мало-мальски сложного проекта на QtQuick начинают течь слезы, если только разработчик не заставил себя следовать какой-нибудь методологии (вроде Flux)
ad1Dima
В UWP Можно сделать примерно так:
А логику уже в codebehind, если это логика UI. Ну или во вьюмодель если бизнес.
Кстати, в андроидовском axml можно тоже в биндингах делать некую логику. Отличная замена конвертерам.
Но вот полностью функции писать в верстке. Сомнительное это преимущество.
Dima_Sharihin
Так я его в недостатки записал
Antervis
QML — это всего лишь GUI, и javascript там нужен для реализации логики GUI. Бизнес-логика должна реализовываться отдельно. Имо если в проекте много говнокода, слезы потекут независимо от того, на чем он написан
ETCDema
Тут на самом деле есть один тонкий момент — никто не запрещает написать так:
но тут встает вопрос с повторением свойств, например:
Парсеры, тот же JSON.parse, при повторении свойства вернет значение именно последнего свойства (работает аналогично инициализации объекта), а вот при потоковом парсинге (например с помощью JsonTextReader из Json.NET) мы увидим все свойства, даже повторяющиеся и можем их корректно обработать. Но это очень холиварный момент. Очень.
ad1Dima
Пока я из XML читал только XAML и ATOM мне всегда было не ясно, как JSON может быть более читаем: он менее строгий и ограничен единственной конструкцией Ключ-Значение, которой описывается и свойства и иерархия. Когда у xml свойства могут быть атрибутами, а иерархия это всегда вложенные теги.
Потом я уже столкнулся с plist. Когда xml используется по json-идеалогии. тогда его из-за излишней многословности читать действительно сложнее.
Воспользуюсь примером kekekeks:
Если сравнивать такой json:
И XML написанный в том же стиле (примерно как plist):
То, конечно, json выигрывает. Но нормальный XML намного удобнее:
Тут тег однозначно сопоставляется с экземпляром объекта, свойства объекта с атрибутами. Иерархия тоже легче читается.
mr_bag
По выразительности ни XML, ни JSON не сравнится с QML.
kekekeks
Любителям синтаксиса QML рекомендую посмотреть в сторону AmmyUI.