"ААА! Пришло время переписывать на .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)


  1. SirEdvin
    29.12.2017 10:46

    А как же PyQt? Вроде довольно неплох.


    1. DarkByte2015
      29.12.2017 11:11

      PyQt это опять же — Qt. Лично по мне так он довольно сложен для понимания, особенно QML. Все-таки самый лучший способ писать интерфейс разметкой — это xml (и его расширения типа XAML и т.д.). А вот в json разметку еще и намешанную с js воспринимать очень тяжело.


      1. zorn_v
        29.12.2017 11:51

        XML легче читать чем json… Вы это серьезно?


        1. kefirr Автор
          29.12.2017 11:56

          В JSON даже комментарии нельзя вставлять, не говоря уже о схемах и прочем.
          Для передачи данных да, он компактнее и, может, более читаем. Но для разметки, конфигов я предпочту XML.


          1. lassana
            29.12.2017 12:06
            +1

            В QML-то есть комментарии, безо всяких извращений вида <!--… -->


          1. spmbt
            29.12.2017 14:21
            -1

            Комментарий в JSON — {...«node»:«нечто по этому узлу», «nodeCOMMENT»:«Комментарий»...}


            1. evnuh
              29.12.2017 16:58
              -1

              это не комментарий


        1. kekekeks
          29.12.2017 12:03
          +2

          У JSON в плане использования как языка разметки есть фатальные недостатки — неименованые типы объектов и отсутствие встроенных текстовых нод. Из-за этого при определении списка элементов приходится писать что-то типа


          {
            "@type": "StackPanel",
            "children": [
              {
                "@type": "TextBlock",
                "text": "Hello world"
              },
              {
                "@type": "Button",
                "text": "Click me"
              }
            ]
          }

          вместо


          <StackPanel>
            <TextBlock>Hello world</TextBlock>
            <Button Text="Click me"/>
          </StackPanel>


          1. Dima_Sharihin
            29.12.2017 12:17
            +2

            В QML это будет


            ColumnLayout {
               Text {
                 text: qsTr("Hello world")
               }
               Button {
                 text: qsTr("Click me")
               }
            }

            Единственный косяк QML — возможность писать код прямо в разметке — так и поощряет быдлокодить и приходится каждый раз себя бить по рукам для разделения вида контролов и их логики


            1. ad1Dima
              29.12.2017 12:33
              +1

              Это выглядит лучше чем json, но я бы не сказал, что лучше, чем XAML. Примерно одинаково и дело привычки. (хотя я бы в вашем примере еще комментировал закрывающие скобки, если большая вложенность)


              1. Dima_Sharihin
                29.12.2017 12:40

                QML позволяет быдлокодить примерно так:


                Button {
                     property int times = 0
                     text: qsTr("Clicked %1 times").arg(times)
                     onClicked: times++
                }

                Что в XAML получится кудааа длиннее. Но в то же время WPF заставит сделать аккуратно, а QML на все это пофиг. Поэтому от мало-мальски сложного проекта на QtQuick начинают течь слезы, если только разработчик не заставил себя следовать какой-нибудь методологии (вроде Flux)


                1. ad1Dima
                  29.12.2017 12:48
                  +1

                  В UWP Можно сделать примерно так:

                    <Button Click="Onclick" Content="{x:Bind GetClickCount()}"/>
                  

                  А логику уже в codebehind, если это логика UI. Ну или во вьюмодель если бизнес.

                  Кстати, в андроидовском axml можно тоже в биндингах делать некую логику. Отличная замена конвертерам.

                  Но вот полностью функции писать в верстке. Сомнительное это преимущество.


                  1. Dima_Sharihin
                    29.12.2017 12:49

                    Но вот полностью функции писать в верстке. Сомнительное это преимущество.

                    Так я его в недостатки записал


                1. Antervis
                  29.12.2017 17:14

                  Поэтому от мало-мальски сложного проекта на QtQuick начинают течь слезы, если только разработчик не заставил себя следовать какой-нибудь методологии

                  QML — это всего лишь GUI, и javascript там нужен для реализации логики GUI. Бизнес-логика должна реализовываться отдельно. Имо если в проекте много говнокода, слезы потекут независимо от того, на чем он написан


          1. ETCDema
            29.12.2017 14:39

            Тут на самом деле есть один тонкий момент — никто не запрещает написать так:


            {
                "StackPanel": 
                {
                    "TextBlock": "Hello world",
                    "Button":
                    {
                        "Text": "Click me"
                    }
                }
            }

            но тут встает вопрос с повторением свойств, например:


            {
                "StackPanel": 
                {
                    "TextBlock": "Hello world",
                    "Button":
                    {
                        "Text": "Click me"
                    },
                    "Button":
                    {
                        "Text": "And me!"
                    }
                }
            }

            Парсеры, тот же JSON.parse, при повторении свойства вернет значение именно последнего свойства (работает аналогично инициализации объекта), а вот при потоковом парсинге (например с помощью JsonTextReader из Json.NET) мы увидим все свойства, даже повторяющиеся и можем их корректно обработать. Но это очень холиварный момент. Очень.


        1. ad1Dima
          29.12.2017 12:29
          +1

          Пока я из XML читал только XAML и ATOM мне всегда было не ясно, как JSON может быть более читаем: он менее строгий и ограничен единственной конструкцией Ключ-Значение, которой описывается и свойства и иерархия. Когда у xml свойства могут быть атрибутами, а иерархия это всегда вложенные теги.

          Потом я уже столкнулся с plist. Когда xml используется по json-идеалогии. тогда его из-за излишней многословности читать действительно сложнее.

          Воспользуюсь примером kekekeks:
          Если сравнивать такой json:

          {
            "@type": "StackPanel",
            "children": [
              {
                "@type": "TextBlock",
                "text": "Hello world"
              },
              {
                "@type": "Button",
                "text": "Click me"
              }
            ]
          }
          

          И XML написанный в том же стиле (примерно как plist):
          <panel>
            <type>StackPanel</type>
            <children>
              <control>
                <type>TextBlock</type>
                <text>Hello world</text>
              </control>
              <control>
                <type>Button</type>
                <text>Click me</text>
              </control>
            </children>
          </panel>
          

          То, конечно, json выигрывает. Но нормальный XML намного удобнее:
          <StackPanel>
            <TextBlock>Hello world</TextBlock>
            <Button Text="Click me"/>
          </StackPanel>
          


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


      1. mr_bag
        29.12.2017 12:29

        По выразительности ни XML, ни JSON не сравнится с QML.


        1. kekekeks
          29.12.2017 13:12

          Любителям синтаксиса QML рекомендую посмотреть в сторону AmmyUI.


  1. DROS
    29.12.2017 10:55

    Напомнило демку с диска к книге Страуструпа за 99 год вроде. Там аналогичный эффект был, правда без паралакса и под DOS. Хороший был диск. А сколько там было исходников вирусни того времени — ух…


  1. eyeofhell
    29.12.2017 11:10
    +1

    Ухты. А как Avalonia это делает? Dotnet core — он же GUI биндингов не имеет? Они что, через интроспекцию лезут напрямую в ось, в рантайме проверяют что за ось и в рантайме же биндятся к нужным GUI апишкам?


    1. kefirr Автор
      29.12.2017 11:14

      Вот кое-что есть на хабре: https://habrahabr.ru/post/328684/


    1. kekekeks
      29.12.2017 11:25
      +2

      Там всё достаточно примитивно в плане детекта — RuntimePlatform.GetRuntimeInfo().OperatingSystem выдаёт текущую операционку. А дальше создаются окошки уже через P/Invoke либо к Win32 API, либо к GTK3, либо к Cocoa.


      Подробнее по архитектуре см. с 15:00 здесь


      1. eyeofhell
        29.12.2017 11:26

        Ага, то есть «P/Invoke». Неторопливая штука в целом. Не факт, что быстрее Electron.


        1. kekekeks
          29.12.2017 11:41
          +2

          Вы точно понимаете, как работает P/Invoke? Вы точно понимаете, как именно работает электрон и почему именно он тормозит?


          1. eyeofhell
            29.12.2017 11:45

            Если .net хочет вызвать, к примеру, «SendMessageW» на винде, то она делает преобразования для всех аргументов (потому что строка в .net и null-terminated UCS-2 в WinAPI — это разные штуки), потом вызов, потом преобразование возвращаемых знаечений, если есть. Electron — это просто Chromium. В нем оптимизированный движок для рендеринга HTML, он компилирует JS в нативный код, все взаимодействие между скомпилированным JS и HTML бесшовно.

            Но специально я не сравнивал — любопытствую. Демка из статьи выше у меня запустилась сильно медленнее, чем стартует Visual Studio Code. Да, воторой запуск такой же медленный, как первый :)


            1. kekekeks
              29.12.2017 11:57
              +1

              потому что строка в .net и null-terminated UCS-2 в WinAPI — это разные штуки

              На самом деле нет, дотнетные строки нуль-терминированы, вы можете взять указатель и передать как есть. Да, в ряде случаев маршалинг нужен, но это контролируемый процесс. Так, всё взаимодействие с DirectX у нас идёт через msil-инструкцию calli, которая выполняет прямой вызов по указателю без автоматических преобразований.


              Демка из статьи выше у меня запустилась сильно медленнее, чем стартует Visual Studio Code

              А вы её запускали через dotnet run? Он каждый раз дёргает MSBuild и кучу ненужностей, чтобы определить, надо ли пересобрать проект (и обычно пересобирает даже тогда, когда не надо). Процесс этот не быстрый. Сделайте dotnet publish под нужную платформу и уже собранный релизный вариант запустите.


              1. kefirr Автор
                29.12.2017 12:03
                +1

                msil-инструкцию calli, которая выполняет прямой вызов по указателю без автоматических преобразований

                Интересно, а где можно на это посмотреть?


                1. kekekeks
                  29.12.2017 12:07
                  +2

                  См. SharpGenTools, которым обрабатывается используемый нами SharpDX в процессе сборки. Мы себя отдельно SharpGenTools используем в AvalonStudio для генерации кода интеропа с libdbgshim/ICorDebug для отладки кода под .NET Core.


        1. kefirr Автор
          29.12.2017 11:50

          "Неторопливая" — очень относительное понятие. Да, PInvoke медленный по сравнению с прямым вызовом функции, он добавляет от 10 до 30 инструкций (см MSDN). Но надо понимать, что эта разница — порядка наносекунд, и в случае вызова функций сложнее a+b просто теряется. Например, Ignite.NET работает с JVM через PInvoke, и тормозов от этого не испытывает.


          Electron:


          • тащит за собой целый браузер, жрёт немеряно памяти и создаёт новый процесс на каждый чих
          • C# быстрее JavaScript и лучше во всех отношениях
          • XAML лучше HTML

          Другое дело, что layout & rendering в Electron действительно может быть быстрее для некоторых сценариев, браузерный движок очень круто заоптимизирован под это дело.


          1. kekekeks
            29.12.2017 12:09
            +2

            layout & rendering в Electron действительно может быть быстрее

            Так рендерилку (Skia) мы у них в цельнотянутом виде утащили. На не-windows-платформах используется.


  1. ink-shtil
    29.12.2017 11:11
    +1

    Спасибо за настроение и за код!!


  1. KvanTTT
    29.12.2017 13:48
    +4

    Спасибо за статью и рекламу Avalonia! Очень нравится концепция, надеюсь выстрелит.


  1. sunman
    29.12.2017 14:41
    +2

    Классический эффект ранних интро.
    Единственное — в этом эффекте у снежинок обычно добавляли небольшие флуктуации по горизонтали — как это и выглядит в натуре, если нет ветра.
    Добавил эту фичу и сделал pull-request: https://github.com/ptupitsyn/let-it-snow/pull/1/commits


  1. NoMad42
    29.12.2017 15:11
    +2

    Я бы посмотрел ответочку от

    • Java
    • Kotlin
    • WebAssembly? O_o


    1. izzholtik
      29.12.2017 15:32

      Какую именно?
      Падающий песок делается за один вечер. Что интересует — удобство, скорость, вес? Просто статья о том, как писалось?


  1. crea7or
    29.12.2017 19:38

    Господа, а нормальный-то WPF не планируется к .net core что ли?


    1. kekekeks
      29.12.2017 22:00

      Нет. Вместо него поверх CoreCLR работает UWP. Который нигде кроме Windows 10 работать не планирует.


      1. izzholtik
        29.12.2017 23:03

        Есть ли шанс поддержки вайном?


        1. kekekeks
          29.12.2017 23:27

          Где? В .NET Core? Или поддержка вайном UWP? Первое технически слабореализуемо (Mono уже пытались когда-то давно реализовать поддержку WinForms поверх вайна, получилось плохо), во второе слабо верится, ввиду общей непопулярности UWP как платформы.


          1. izzholtik
            30.12.2017 19:44

            UWP. Майки будут продвигать сабж со временем. Если под вайном это сделать невозможно, печаль будет полная, вплоть до, лол, смерти десктопного линукса.


            1. kekekeks
              30.12.2017 20:15

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


              1. izzholtik
                30.12.2017 20:37

                Под win S запускаются только приложения рз маркета.


                • скоро появятся планшеты на ARM, где WPS будут иметь преимущество над win32.


                1. kekekeks
                  30.12.2017 20:45

                  Под win S запускаются только приложения рз маркета.

                  Через Desktop Bridge на этой Win S хоть Windows Forms можно запустить.


  1. keydon2
    29.12.2017 23:27

    А расскажите что мешает например сделать биндинги к Qt(зачем ещё одна графическая либа)? Известный фреймворк, сделан профессионально, из коробки до кучи фич от лайв редактирования до трансляции в браузер. Кастомизируется по самое нехочу. Отличная лицензия. Из недостатков только что веб в один клик не запилить(хотя зная современный веб это можно считать плюсом) и что заставляет под себя подстраиваться (хотелось бы более универсальное как стандартная библиотеки, чтобы не тянуть зависимости ядра, а сразу заюзал функционал в любой проект без остального qt).


    1. kekekeks
      30.12.2017 09:40
      +5

      Дата-биндингов нормальных нет, шаблонов нормальных нет, lookless-контролов нет, т. е. идеологически Qt застрял где-то во временах Windows Forms и Delphi 7. А так хороший фреймворк, никто не спорит.


  1. w1ld
    02.01.2018 13:19
    +1

    Здорово! Приятно видеть, что UI dotNet с XAML можно запускать на этих платформах.