В свежем превью Rider, помимо прочего,
появилась поддержка Авалонии. Авалония это самый крупный .NET
фреймворк для разработки кроссплатформенного UI, и его поддержка в
IDE отличный повод наконец разобраться, как писать десктопные
приложения для любых платформ.
В этой статье я на примере простой задачи по реализации
калькулятора покажу:
- как управлять разметкой,
- как связывать функциональность с компонентами,
- как управлять стилями.
Подготовка
Для работы я использовал:
Единственным обязательным инструментов в этом списке является сам
дотнет. Остальное можете выбирать сами: любимую операционную
систему и IDE (например, тот же Rider).
Для инициализации проекта мы воспользуемся
шаблонами .NET приложений для Авалонии. Для этого нам
потребуется клонировать репозиторий с шаблонами, а затем установить
скачанные шаблоны:
git clone https://github.com/AvaloniaUI/avalonia-dotnet-templates.gitdotnet new --install /path/avalonia-dotnet-templates/
Типы проектов
Теперь, когда шаблоны установлены, мы можем создать новый проект на
основе MVVM шаблона Авалонии:
dotnet new avalonia.mvvm -o ACalc
Перейдем в директорию проекта и обновим все версии пакетов на самые
новые (на момент написания статьи):
dotnet add package Avalonia --version 0.10.0-preview6dotnet add package Avalonia.Desktop --version 0.10.0-preview6dotnet add package Avalonia.ReactiveUI --version 0.10.0-preview6
Давайте внимательнее посмотрим на структуру проекта,
сгенерированную шаблоном:
- В папке 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> <RowDefinition Height="auto"></RowDefinition> <RowDefinition Height="*"></RowDefinition> </Grid.RowDefinitions></Grid>
Теперь заполним разметку основными компонентами добавим строку
меню, базовый экран и вложенный Grid для блока клавиш:
<Grid> <Grid.RowDefinitions> <RowDefinition Height="auto"></RowDefinition> <RowDefinition Height="auto"></RowDefinition> <RowDefinition Height="*"></RowDefinition> </Grid.RowDefinitions> <!--строка меню--> <Menu> </Menu> <!--Импровизированный экран нашего калькулятора--> <TextBlock> </TextBlock> <!--Grid для клавиш--> <Grid></Grid></Grid>
Отдельно остановимся на расположении клавиш в сетке.
Для начала нужно описать количество строк и столбцов в нашем Grid.
А после разложить кнопки по соответствующим им строкам и столбцам,
указав их координаты.
<Grid> <Grid.RowDefinitions> <RowDefinition></RowDefinition> <RowDefinition></RowDefinition> <RowDefinition></RowDefinition> <RowDefinition></RowDefinition> <RowDefinition></RowDefinition> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition></ColumnDefinition> <ColumnDefinition></ColumnDefinition> <ColumnDefinition></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.
На этом все! Оставайтесь на связи, мы вернемся со статьями о более
продвинутых возможностях Авалонии.