Авалония для самых маленьких

  • Tutorial
В свежем превью Rider, помимо прочего, появилась поддержка Авалонии. Авалония — это самый крупный .NET фреймворк для разработки кроссплатформенного UI, и его поддержка в IDE — отличный повод наконец разобраться, как писать десктопные приложения для любых платформ.

В этой статье я на примере простой задачи по реализации калькулятора покажу:

  • как управлять разметкой,
  • как связывать функциональность с компонентами,
  • как управлять стилями.



Подготовка


Для работы я использовал:


Единственным обязательным инструментов в этом списке является сам дотнет. Остальное можете выбирать сами: любимую операционную систему и IDE (например, тот же Rider).
Для инициализации проекта мы воспользуемся шаблонами .NET приложений для Авалонии. Для этого нам потребуется клонировать репозиторий с шаблонами, а затем установить скачанные шаблоны:

git clone https://github.com/AvaloniaUI/avalonia-dotnet-templates.git
dotnet new --install /path/avalonia-dotnet-templates/

Типы проектов Авалонии
Типы проектов

Теперь, когда шаблоны установлены, мы можем создать новый проект на основе MVVM шаблона Авалонии:

dotnet new avalonia.mvvm -o ACalc

Перейдем в директорию проекта и обновим все версии пакетов на самые новые (на момент написания статьи):

dotnet add package Avalonia --version 0.10.0-preview6
dotnet add package Avalonia.Desktop --version 0.10.0-preview6
dotnet add package Avalonia.ReactiveUI --version 0.10.0-preview6

Давайте внимательнее посмотрим на структуру проекта, сгенерированную шаблоном:

image

  • В папке Assets хранятся ресурсы, используемые нами в данном проекте. На текущий момент там лежит лого Авалонии, использующееся в качестве иконки приложения.
  • В папку Model мы будем складывать все общие модели, используемые в нашем приложении. На текущий момент она пуста.
  • Папка ViewModels предназначена для хранения логики, которая будет использоваться в каждом из окон. Прямо сейчас в этой папке хранится ViewModel главного окна и базовый класс для всех ViewModel.
  • В папке Views хранится разметка окон (а также code behind файл, в который хоть и можно положить логику, но лучше для этих целей использовать ViewModel). На текущий момент у нас есть только главное окно.
  • App.xaml — общий конфиг приложения. Несмотря на то, что он и выглядит как еще одно окно, на самом деле, этот файл служит для задания общих настроек приложения.
  • ViewLocator нам в этот раз не пригодится, так как он используется для создания кастомных контролов. Подробнее о нем можно почитать в документации Авалонии.

Запустим наше приложение командой dotnet run.



Теперь все готово для разработки.

Разметка


Начнем с создания базовой разметки. Перейдем в файл Views/MainWindow.xaml — там будет храниться разметка главного окна нашего калькулятора.



В данный момент наша разметка состоит из базовых параметров окна (размеров, иконки и заголовка) и одного блока с текстом. Давайте заменим этот блок с текстом на Grid, который будет служить «скелетом» нашей разметки. Этот контрол разложит все элементы по порядку, один за другим.

Итак, заменим TextBlock на пустой Grid:

<Grid></Grid>

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

<Grid>
    <Grid.RowDefinitions>
        <RowDefinition Height="auto"/>
        <RowDefinition Height="auto"/>
        <RowDefinition Height="*"/>
    </Grid.RowDefinitions>
</Grid>

Теперь заполним разметку основными компонентами — добавим строку меню, базовый экран и вложенный Grid для блока клавиш:

<Grid>
    <Grid.RowDefinitions>
        <RowDefinition Height="auto"/>
        <RowDefinition Height="auto"/>
        <RowDefinition Height="*"/>
    </Grid.RowDefinitions>
    <!--строка меню-->
    <Menu>
    </Menu>
    <!--Импровизированный экран нашего калькулятора-->
    <TextBlock>
    </TextBlock>
    <!--Grid для клавиш-->
    <Grid></Grid>
</Grid>

Отдельно остановимся на расположении клавиш в сетке.
Для начала нужно описать количество строк и столбцов в нашем Grid. А после — разложить кнопки по соответствующим им строкам и столбцам, указав их координаты.

<Grid>
    <Grid.RowDefinitions>
        <RowDefinition/>
        <RowDefinition/>
        <RowDefinition/>
        <RowDefinition/>
        <RowDefinition/>
    </Grid.RowDefinitions>
    <Grid.ColumnDefinitions>
        <ColumnDefinition/>
         <ColumnDefinition/>
         <ColumnDefinition/>
         <ColumnDefinition/>
         <ColumnDefinition/>
    </Grid.ColumnDefinitions>
    <Button Grid.Row="0" Grid.Column="0">1</Button>
</Grid>

Стоит отметить, что элементы внутри Grid могут занимать несколько ячеек. Для этого используются параметры ColumnSpan и RowSpan:

 <Button Grid.Row="3" Grid.Column="3" Grid.ColumnSpan="2">=</Button>

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

Последнее, что нам осталось сделать — это задать параметры окна. Установим стартовые и минимальные размеры окна (они задаются в корневом элементе Window).

MinHeight="300"
MinWidth="250"
Height="300"
Width="250"

После добавления всех элементов разметки наше окно калькулятора будет выглядеть так:



Основной функционал


С разметкой закончили, пора реализовать логику!

Начнем с добавления в папку Models нового Enum, который описывает возможные операции:

public enum Operation
{
    Add,
    Subtract,
    Multiply,
    Divide,
    Result
}

Теперь перейдем в класс ViewModel/MainWindowViewModel. Здесь будет храниться основная функциональность нашего приложения.

Добавим в файл несколько приватных полей, с которыми мы будем работать:

private double _firstValue;
private double _secondValue;
private Operation _operation = Operation.Add;

Теперь реализуем основные методы:

  • AddNumber — добавляет новую цифру к числу.
  • ExecuteOperation — выполняет одну из операций, описанных в енаме Operation.
  • RemoveLastNumber — удаляет последнюю введенную цифру.
  • ClearScreen — очищает экран калькулятора.

Не будем останавливаться на реализации этих методов, в этом нет никакой специфики для Авалонии (реализацию вы можете посмотреть в репозитории проекта). Единственное, что нас интересует — это то, что помимо перечисленных выше приватных полей, эти методы также оперируют публичным свойством ShownValue. О нем — чуть позже.

Связывание


Теперь, когда у нас готовы и разметка, и логика, пора связать их друг с другом.
В Авалонию по умолчанию включен Reactive UI — это фреймворк, предназначенный как раз для связывания View и Model при использовании MVVM. Подробнее о нем вы сможете прочитать на официальном сайте и в документации Авалонии. Конкретно сейчас нас интересует возможность фреймворка обновлять View при изменении данных.

Для хранения актуального значения, выводимого на экране, реализуем свойство ShownValue:

public double ShownValue
{
    get => _secondValue;
    set => this.RaiseAndSetIfChanged(ref _secondValue, value);
}

Получаемое из этого свойства значение будет выводиться на дисплее нашего калькулятора, а метод RaiseAndSetIfChanged позаботится о вызове уведомления при изменении значения свойства.

Привяжем это свойство к созданному на этапе разметки текстовому полю:

<TextBlock Grid.Row="1" Text="{Binding ShownValue}" />

Благодаря директиве Binding и методу RaiseAndSetIfChanged значение свойства Text в этом поле будет обновляться при каждом изменении значения свойства ShownValue.

Теперь добавим в MainWindowViewModel еще три публичных свойства для команд. Команды являются обертками вокруг функций, которые будут вызываться определенными действиями на UI.

public ReactiveCommand<int, Unit> AddNumberCommand { get; }
public ReactiveCommand<Unit, Unit> RemoveLastNumberCommand { get; }
public ReactiveCommand<Operation, Unit> ExecuteOperationCommand { get; }

Команды нужно инициализировать в конструкторе класса, связав их с соответствующими методами:

public MainWindowViewModel()
{
    AddNumberCommand = ReactiveCommand.Create<int>(AddNumber);
    ExecuteOperationCommand = ReactiveCommand.Create<Operation>(ExecuteOperation);
    RemoveLastNumberCommand = ReactiveCommand.Create(RemoveLastNumber);
}

Теперь обновим разметку кнопок. Например, для клавиши Backspace новая разметка будет выглядеть так:

<Button Grid.Row="3" Grid.Column="2" Command="{Binding RemoveLastNumberCommand}">←</Button>

Несколько сложнее дела обстоят с номерными кнопками и кнопками операций. Для них мы должны передать в качестве параметра вводимую цифру или операцию. Для этого в корневом теге Window нам нужно добавить пространство имен System:

xmlns:s="clr-namespace:System;assembly=mscorlib"

А затем обновить разметку кнопок, добавив в них связанный метод и параметр:

<Button Grid.Row="0" Grid.Column="0" Command="{Binding AddNumberCommand}">
    <Button.CommandParameter>
        <s:Int32>1</s:Int32>
    </Button.CommandParameter>
     1
</Button>

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



Стили


Итак, логика нашего калькулятора полностью реализована, но его визуальная сторона оставляет желать лучшего. Самое время поиграться со стилями!

В Авалонии есть три способа управлять стилями:

  • настроить стили внутри компонента,
  • настроить стили в рамках окна,
  • подключить пакет стилей.

Пройдемся по каждому из них.

Начнем с настройки стилей внутри конкретного компонента. Очевидный претендент на точечные изменения — это экран нашего калькулятора. Давайте увеличим для него размер шрифта и перенесем текст вправо.

<TextBlock Grid.Row="1" Text="{Binding ShownValue}" TextAlignment="Right" FontSize="30" />

Теперь поиграемся со стилями в рамках окна. Здесь мы можем изменить вид всех компонентов определенного типа. Например, можно немного раздвинуть кнопки.

<Window.Styles>
    <Style Selector="Button">
        <Setter Property="Margin" Value="5"></Setter>
     </Style>
</Window.Styles>

Как видите, конкретные компоненты, к которым применяется стиль, можно выбирать при помощи селектора. Больше о селекторах вы можете прочитать в документации Авалонии.

После применения изменений выше наше окно будет выглядеть так



Чтобы упростить себе жизнь, можете воспользоваться готовым пакетом стилей. Давайте, к примеру, подключим для нашего калькулятора стиль Material. Для этого добавим соответствующий nuget пакет:

dotnet add package Material.Avalonia --version 0.10.3

А теперь обновим файл App.xaml и укажем в нем используемый пакет стилей и его параметры.

<Application ...
             xmlns:themes="clr-namespace:Material.Styles.Themes;assembly=Material.Styles"
             ...>
    <Application.Resources>
        <themes:BundledTheme BaseTheme="Dark" PrimaryColor="Purple" SecondaryColor="Amber"/>
    </Application.Resources>
    <Application.Styles>
        <StyleInclude Source="avares://Material.Avalonia/Material.Avalonia.Templates.xaml" />
    </Application.Styles>
</Application>

Установленный пакет обновит визуальный стиль нашего приложения, и теперь оно будет выглядеть так:



Такие же пакеты стилей можно создавать самостоятельно — их можно использовать внутри вашего проекта или распространять в виде пакета на nuget. Больше информации о стилях и способах управления ими можно найти в документации.

Заключение


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

А еще много интересного про Авалонию и .NET UI можно будет послушать на онлайн-митапе от Контура, который пройдет сегодня, в пять по Москве.

Все исходники проекта вы можете найти в репозитории на Github.

На этом все! Оставайтесь на связи, мы вернемся со статьями о более продвинутых возможностях Авалонии.
Контур
Делаем веб-сервисы для бизнеса

Похожие публикации

Комментарии 34

    –1

    Я выиграл в игре "Угадай технологию по UI"

      0

      А что тут угадывать то, если ответ в заголовке?

        +3

        А мне вот очень понравился калькулятор с тёмно-розовыми кнопками. Справедливости ради стоит заметить, что в Avalonia 0.10.0, который уже вот-вот зарелизят, сделали тему, основанную на Microsoft Fluent Design. А ещё есть набор стилей Citrus.Avalonia. С релизом 0.10 будет сложнее угадать эту технологию по UI!


          +2

          Мне UI тоже нравится, и очень-очень.
          И особенно это цвет фиолетовый (или сиреневый, или темно-розовый) — фирменный цвет дот-нета, очень даже хорош.


          То что на скрине у вас — тоже, кстати очень приятно выглядит.

        0
        Хорошо, что avalonia есть, но лично мне очень не хватает какого-нибудь компактного и удобного DSL, чтобы не верстать и не стилизовывать в xml (сейчас из кода можно верстать, но назвать это компактным и удобным язык не поворачивается).
        Примерно так сейчас пишутся стили
        var styles = new Styles() {
          new Style(s=>s.OfType<Button>().Class("primary").Class(":focus")) {
            Setters = {
              new Setter(Button.BorderProperty, ...)
            }    
          }
        }
        

        против XML
        <Styles>
          <Style Selector="Button.primary:focus">
            <Setter Property="Button.Border" Value="#ff0000"/>
          </Style>
        </Styles>
        


        В этом аспекте мне очень сильно нравится Jetpack Compose на котлине и Fabulous на F#.
        Avalonia FuncUI — имхо не очень удобна, тк значения свойств записываются через список, а не через объект.
        Условно:
        let textBox= TextBox.create [TextBox.text "Hello world"; TextBox.width 100] 
        

        против

        let textBox = TextBox(text = "Hello world", width = 100)
        
          +1
          Согласен, это приятно выглядит, но описание больших и сложных UI в таком стиле порой ощущается перегруженным.
          Кстати, я планировал в январе написать статью о разработке UI на F#, следите за обновлениями :)
            0
            В данном замечании нельзя не отметить наличие у Авалонии поддержки F# с mvu синтаксисом, который действительно походит на dsl и kotlin compouse
             let view (state: CounterState) (dispatch): IView =
                    DockPanel.create [
                        DockPanel.children [
                            Button.create [
                                Button.onClick (fun _ -> dispatch Increment)
                                Button.content "click to increment"
                            ]
                            Button.create [
                                Button.onClick (fun _ -> dispatch Decrement)
                                Button.content "click to decrement" 
                            ]
                            TextBlock.create [
                                TextBlock.dock Dock.Top
                                TextBlock.text (sprintf "the count is %i" state.count)
                            ]
                        ]
                    ]
              0
              Я его как раз и упомянул в комментарии :)
              И мне в нём не нравится, что свойства пишутся в списке, а не в объекте/параметрах — не удобно исследовать API через автокомплит.
                0

                Нужно ли при этом весь остальной проект писать на F#? Если да, то не везде его использование оправдано — лучше использовать что-то отдельное, независимое от основного языка разработки.

                  0
                  Можно писать либы на F# или наоборот, основной проект на C# а либы со стилями или котролами на F#
                    0

                    Ну, на уровне сборок разруливается. Т.е. в одной сборке можно использовать или F#, или C#. Но вообще, я бы вот F# как раз для логики использовал, а вот для разметки xaml — самое оно

                  0

                  На альтернативный DSL заведена issue: Alternative markup syntax proposal.

                    +1

                    Тогда стоит попробовать Flutter. Код на нем выглядит почти так вы описали.


                    Вот так, например:


                    Card(
                      child: Container(
                        padding: EdgeInsets.all(10),
                        child: TextFormField(
                          textInputAction: TextInputAction.search,
                          onFieldSubmitted: (_) => _onSearch(),
                          autofocus: true,
                          controller: queryController,
                          decoration: InputDecoration(labelText: 'Ключевые слова'),
                        ),
                      )
                    )
                    +5

                    Кстати, в авалонии есть прикольная штука для гридов, можно писать вот так


                    <Grid RowDefinitions="*,*,*,"/>

                    вместо вот этого


                    <Grid>
                       <Grid.RowDefinitions>
                            <RowDefinition Height="*"></RowDefinition>
                            <RowDefinition Height="*"></RowDefinition>
                            <RowDefinition Height="*"></RowDefinition>
                        </Grid.RowDefinitions>
                    </Grid>
                      +1
                      Класс, спасибо за подсказку!
                        0

                        А мне привычнее писать


                        Rows.Add(rows);

                        Авалония на самом деле приятная, как посмотрел.

                        +7
                        Краткий пересказ статьи для экономии Вашего времени: все тоже самое, что и в WPF
                          +1
                          Многое очень похоже, да. Кстати, привыкшим к WPF разработчикам будет интересна вот эта страничка документации — тут рассказывается об основных различиях.
                            0
                            Честно говоря, именно это искал в статьею Спасибо.
                            +2
                            Собственно в этом и вся магия. Берешь привычный подход к wpf и используешь. Но Авалония зашла дальше и расширила его, как платформенно (Добавились Linux, MacOs, уже виден горизонт в поддержке IOS и есть определенные работы под Android), так и в рамках синтаксиса (появилась более удобная стилизация, новые возможности в биндигах, новые уникальные контролы).
                              0
                              Так может и статейку по этому поводу?
                              Ну там про значимые различия, про сахар, с которым вкуснее, про перспективы, особенно в мобилки, как компилить под разные оси (что для этого надо), что там с производительностью и ограничениям.
                              А то получилось что-то больше похожее на Ctrl+H из 2006 года.
                                +1
                                Может быть, спасибо за хорошую идею. Заголовок статьи достаточно полно отражает ее содержание и скорее призывает поверхностно знакомых разработчиков с платформой .net обратить внимание на Avalonia UI.
                            +2
                            ViewLocator нам в этот раз не пригодится, так как он используется для создания кастомных контролов.

                            Правильнее, наверное, сказать «пользовательских контролов», а не «кастомных». По крайней мере, если использовать терминологию WPF.
                            И ещё… В Avalonia строки и столбцы сетки можно указывать куда проще:
                            Например,
                            <Grid RowDefinitions="auto * *" />

                              0
                              А как у данного GUI-фрейворка обстоят дела с потреблением памяти (по сравнению, например с Electron)
                                0
                                Если упрощать — в среднем лучше, чем Electron, но куда хуже, чем что-то плюсовое. По сравнению с WPF… зависит. Фреймворковская версия поэкономнее будет (многое прекомпилировано и лежит в GAC), неткоровский сопоставимо жрет.
                                0

                                Если следите за комментариями, скажите для ясности (я не уверен): разве можно Avalonia, который фреймворк для юи на решетках, сравнивать с Electron, который веб?

                                  0
                                  Electron не совсем веб. Оба фреймворка (и Авалония, и Электрон) нужны для создания кроссплатформенных приложений на десктоп. Разница здесь в способе создания этого приложения, но пользователь все еще получает кроссплатформенное десктопное приложение.
                                    +1
                                    Electron не совсем веб.

                                    Почему я Электрон вебом обозвал — так у него веб (клиент-серверное взаимодействие) в крови, а у второго, (наверное), — нет.

                                    0

                                    Не туда вопрос, простите. Дочитался до сути, и понял что не то спросил. Сравнение в контексте потребления памяти.

                                    +1
                                    У меня вот только 1 вопрос — почему не Xamarin? Он еще и на мобильных платформах работает, и есть крутейший fabulous.
                                    0
                                    Утверждение, что Авалония самый крупный .NET фреймворк для кроссплатформенной разработки — явно ложный. Есть более крупные и известные Xamarin Forms, Uno Platform. Которые поддерживают (и в более лучшем виде чем это ваша Авалония) и iOS, Android, Windows, Mac, Linux.
                                    А с выходом .NET MAUI многие кроссплатформенные разработчики так и пишут, прощай Xamarin Forms (И уж тем более Авалон). Ибо подобные фреймворки теряют свою актуальность и оказываются на свалке технологий, там же где и Borland Delphi, Visual Basic 6 и список можно продолжать еще долго…
                                      +4

                                      На самом деле всё не совсем так, как вы пишете. Да, действительно, с выходом .NET MAUI Xamarin.Forms отправится на свалку, потому что .NET MAUI и есть, по сути своей, эволюция Xamarin.Forms с новыми API. При этом, в Microsoft собираются сделать API .NET MAUI совместимым с API Xamarin.Forms. Подробнее о планах команды можно посмотреть в видео с виртуальной конференции ReactiveUI под названием Dualscreen, .NET MAUI and ReactiveUI. Там разработчик нового MAUI и старого Xamarin.Forms делится инсайдами о новом API.


                                      Далее, на странице репозитория dotnet/maui мы видим сводку о поддерживаемых платформах, согласно которой Linux всё так же остаётся community-maintained. Это значит, что, ну, Microsoft Linux поддерживать не будет, а Avalonia уже поддерживает. Не исключено, что может появиться бакенд MAUI, основанный на Avalonia, с помощью которого можно будет обеспечить поддержку Linux — будем посмотреть.


                                      Далее, ниши у Xamarin.Forms (или MAUI) и AvaloniaUI (или WPF) принципиально разные. Про производительность Avalonia в сравнении с WPF можно посмотреть в Core2D rendering performance WPF vs Avalonia+Direct2D,SkiaSharp,Cairo vs WPF+SkiaSharp. На Avalonia уже пишут сложные кроссплатформенные редакторы наподобие таких:


                                      image


                                      Было бы любопытно посмотреть на что-то подобное, написанное на Xamarin.Forms и работающее на десктопах. Потому что есть мнение, что основной нишей Xamarin.Forms (и MAUI) останутся приложения попроще, больше ориентированные на мобильный сегмент, а не на высокопроизводительный десктоп.


                                      А Uno пока, как говорят, needs more love. Но я слабо знаком с этим фреймворком — про производительность или сегмент, который оно покрывает, ничего не могу сказать. Надо пробовать.


                                      image

                                      +2
                                      Avalonia стремительно развивается и это не может не радовать! Выбор технологий для разработки кроссплатформенного UI с помощью XAML стал очень широк.

                                      Когда нам в далёком 2013 потребовалось сделать хороший UI для игр и захотелось использовать опыт WPF/XAML, выбор пал на NoesisGUI — проприетарная кроссплатформенная библиотека, имплементирующая WPF API с полной поддержкой всех фич XAML (и даже сверх того за счёт отдельных расширения для 3D эффектов, text stroke и т. п.), с оптимизацией под видеоигры. Спустя много лет и багрепортов (включая сотни моих) проект дорос до достойного уровня и теперь это уже полноценный middleware используемый даже в таких ААА проектах, как Baldur's Gate 3. Ну и наши две инди-игры его успешно используют. Например, (немного устаревшие) скриншоты UI из последней игры twitter.com/noesisengine/status/978583221398114304 при этом исходные коды игры (кроме нашего движка) доступны публично github.com/AtomicTorchStudio/CryoFall Можно посмотреть, как многое устроено (сотни XAML файлов) или даже поиграться в live editing (исходники идут в открытом виде с игрой, во многом благодаря Roslyn это удалось осуществить).

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

                                      Самое читаемое