Русский
Русский
English
Статистика
Реклама

Crossplatform

Детальный разбор навигации в Flutter

24.07.2020 14:12:14 | Автор: admin

Навигация во Flutter


image


Flutter набирает популярность среди разработчиков. Большенство подходов впостроении приложений уже устоялись иприменяются ежедневно вразработке E-commerce приложений. Тема навигации опускают навторой или третий план. Какой API навигации предоставляет Фреймворк? Какие подходы выработаны? Как использовать эти подходы иначто они годятся?


Введение


Начнём с того, что такое навигация? Навигация это метод который позволяет перемещаться между пользовательским интерфейсом с заданными параметрами.
К примеру в IOS мире организовывает навигацию UIViewController, а в Android Navigation component. А чтопредоставляет Flutter?



Экраны в Flutter называются route. Для перемещениями между route существует класс Navigator который имеющий обширный API для реализации различных видов навигации.



Начнём спростого.Навигация нановый экран(route) вызывается методом push() который принимает всебя один аргумент это Route.


Navigator.push(MaterialPageRoute(builder: (BuildContext context) => MyPage()));

Давайте детальнее разберёмся ввызове метода push:


Navigator Виджет, который управляет навигацией.
Navigator.push() метод который добавляет новый route виерархию виджетов.
MaterialPageRoute() Модальный route, который заменяет весь экран адаптивным кплатформе переходом анимации.
builder обязательный аргумент конструктора MaterialPageRoute, который возвращает пользовательский интерфейс Фреймворк для отрисовки.
[MyPage](https://miro.medium.com/max/664/1Xm96KtLeIAAMtAYWcr1-MA.png)* пользовательский интерфейс реализованный при помощи Stateful/Stateless Widget


Возвращение на предыдущий route


Для возвращения с экрана на предыдущий необходимо использовать метод pop().


Navigator.pop();

Переда данных между экранами


Данные на новый экран передаются через конструктор при создании экрана в builder методе. Этот принцип работает в методах Navigator, где нужно реализовать метод build.


Navigator.push(context, MaterialPageRoute(builder: (context) => MyPage(someData: data)));

В примере продемонстрирована передача данных в класс MyPage (в этом классе хранится пользовательский интерфейс).


Для того чтобы передать данные на предыдущий экран нужно вызвать метод pop() и передать опциональным аргументом туда данные.


Navigator.pop(data);


Состояние виджета Navigator, который вызван внутри одного из видов MaterialApp/CupertinoApp/WidgetsApp. State отвечает за хранение истории навигации и предоставляет API для управления историей.
Базовые методы навигации повторяют структуру данных Stack. В диаграмме можно наблюдать методы и "call flow" NavigatorState.


http://personeltest.ru/aways/habrastorage.org/webt/5w/dg/nb/5wdgnb-tjlngub4c8y4rlpqkeqi.png


Императивный vs Декларативный подход в навигации


Во всех примерах которые были приведены выше использовался императивный подход в навигации. Разница между императивным и декларативным подходом в том как будет выглядеть вызов нового route, и кто будет хранить реализацию перехода.


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


Императивный подход , отвечает на вопрос как?
Пример: Я вижу, что тот угловой столик свободен. Мы пойдём туда и сядем там.


Декларативный подход, отвечает на вопрос что?
Пример: Столик для двоих, пожалуйста.


Для более глубокого понимания разницы советую прочитать эту статью Imperative vs Declarative Programming


Императивная навигация


Вернёмся к реализации навигации. В императивном подходе описывается детали работы в вызывающем коде. В нашем случае это поля Route. В Flutter много типов route, например MaterialPageRoute и CupertinoPageRoute. Например в CupertinoPageRoute задаётся title, или settings.


Пример:


Navigator.push(    context,    CupertinoPageRoute<T>(        title: "Setting",        builder: (BuildContext context) => MyPage(),        settings: RouteSettings(name:"my_page"),    ),);

Этот код и знания о новом route будут хранитьсяв ViewModel/Controller/BLoC/ У этот подхода существует недостаток.


Представим что потребовалось внести изменения в конструкторе в MyPage или в CupertinoPageRoute. Нужно искать каждый вызов метода push в проекте и изменять кодовую базу.


Вывод:


Этот подход не имеет единообразный подход к навигации, и знание о реализации route проникает в бизнес логику.

Декларативная навигация


Принцип декларативной навигации заключается в использовании декларативного подхода в программировании. Давайте разберём на примере.


Пример:


Navigator.pushNamed(context, '/my_page');

Принцип императивной навигации выглядит куда проще. Говорите "Отправь пользователя на экран настроек" передавая путь одним из аргументов навигации.
Для хранении реализации роста в самом Фреймворке предусмотрен механизм у MaterialApp/CupertinoApp/WidgetsApp. Это 2 колбэка onGenerateRoute и onUnknownRoute отвеспющие за хранение деталей реализации.


Пример:


MaterialApp(    onUnknownRoute: (settings) => CupertinoPageRoute(      builder: (context) {                return UndefinedView(name: settings.name);            }    ),  onGenerateRoute: (settings) {    if (settings.name == '\my_page') {      return CupertinoPageRoute(                title: "MyPage",                settings: settings,        builder: (context) => MyPage(),      );    }        // Тут будут описание других роутов  },);

Разберёмся подробнее в реализации:
Метод onGenerateRoute данный метод срабатывает когда был вызван Navigator.pushNamed(). Метод должен вернуть route.
Метод onUnknownRoute срабатывает когда метод onGenerateRoute вернул null. должен вернуть дефолтный route, по аналогии с web сайтами 404 page.


Этот принцип упрощает вызывающий код и хранить логику переходов в одном едином месте с возможностью переиспользования кодовой базы внутри. Но так же существует недостаток, это универсальность для разных типов раутов.

Диалоговые и модальные окна


Для того чтобы вызвать диалоговое окна разного типа в Фреймворке предусмотрены глобальные методы. Давайте разберёмся в типах диалоговых окон.


Методы для вызова диалоговых и модальных окон:


  • showAboutDialog
  • showBottomSheet
  • showDatePicker
  • showGeneralDialog
  • showMenu
  • showModalBottomSheet
  • showSearch
  • showTimePicker
  • showCupertinoDialog
  • showDialog
  • showLicensePage
  • showCupertinoModalPopup

Эти методы покрывают базовые потребности разработчиков которые хотят работать с окнами. Если не хватает этого API, тогда стоит разобраться как эти методы работают.


Как работает это под капотом?


Давайте рассмотрим исходный код одного из методов, например showGeneralDialog.


Исходный код:


Future<T> showGeneralDialog<T>({  @required BuildContext context,  @required RoutePageBuilder pageBuilder,  bool barrierDismissible,  String barrierLabel,  Color barrierColor,  Duration transitionDuration,  RouteTransitionsBuilder transitionBuilder,  bool useRootNavigator = true,  RouteSettings routeSettings,}) {  assert(pageBuilder != null);  assert(useRootNavigator != null);  assert(!barrierDismissible || barrierLabel != null);  return Navigator.of(context, rootNavigator: useRootNavigator).push<T>(_DialogRoute<T>(    pageBuilder: pageBuilder,    barrierDismissible: barrierDismissible,    barrierLabel: barrierLabel,    barrierColor: barrierColor,    transitionDuration: transitionDuration,    transitionBuilder: transitionBuilder,    settings: routeSettings,  ));}

Давайте детальнее разберёмся в устройстве этого метода. showGeneralDialog вызывает метод push у NavigatorState с _DialogRoute(). Нижнее подчёркивание обозначает что этот класс приватный и используется только в пределах области видимости в которой сам описан, то есть в пределах этого файла.


Диалоговые и модальные окна которые отображаются при помощи глобальных методов это кастомные route которые реализованы разработчиками Фреймворка.

Типы route в фреймворке


Теперь понятно что"every thins is a route", то есть что связанное с навигацией. Давайте взглянем на то, какие route уже реализованы в Фреймворке.


Два основных route в Flutter это PageRoute и PopupRoute.


PageRoute Модальный route, который заменяет весь экран.
PopupRoute Модальный route, который накладывает виджет поверх текущего route.


Реализации PageRoute:


  • MaterialPageRoute
  • CupertinoPageRoute
  • _SearchPageRoute
  • PageRouteBuilder

Реализации PopupRoute:


  • _ContexMenuRoute
  • _DialogRoute
  • _ModalBottomSheetRoute
  • _CupertinoModalPopupRoute
  • _PopupMenuRoute

Реализация PopupRoute приватна и скрыты для внешних потребителей, но роуты используют глобальные методы для показа диалоговых и модальных окон.


Вывод:


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

Best practices


В этой части статьи можно задаться вопросом, "а насколько мне всё это нужно?". Ответить на этот вопрос можно легко, если у есть желание сделать масштабируемый API навигации которая в любой момент будет модифицирована под нужды проекта, то это один из вариантов. Если приложение будет в будущем расширяться со сложной логикой.


Начнём с того что мы сделаем некий сервис который будет будет соблюдать следующим аспектам:


  • Декларативный вызов навигации.
  • Отказ от использования BuildContext для навигации (Это критично если сервис навигации будет вызываться в компонентах, в которых нет возможности получить BuildContext).
  • Модульность. Можно вызвать любой route, CupertinoPageRoute, BottomSheetRoute, DialogRoute и т.д.

Для нашего сервиса навигации нам понадобится интерфейс:


abstract class IRouter {  Future<T> routeTo<T extends Object>(RouteBundle bundle);  Future<bool> back<T extends Object>({T data, bool rootNavigator});  GlobalKey<NavigatorState> rootNavigatorKey;}

Разберём методы:
routeTo - выполняет навигацию на новый экран.
back возвращает на предыдущий экран.
rootNavigatorKey GlobalKey умеющий вызывать методы NavigatorState.

После того как мы сделали интерфейс навигации, давайте сделаем реализацию этого интерфейса.


class Router implements IRouter {    @override  GlobalKey<NavigatorState> rootNavigatorKey = GlobalKey<NavigatorState>();    @override  Future<T> routeTo<T>(RouteBundle bundle) async {   // Push logic here  }    @override  Future<bool> back<T>({T data, bool rootNavigator = false}) async {        // Back logic here    }}

Супер, теперь нам нужно реализовать метод routeTo().


@override  Future<T> routeTo<T>(RouteBundle bundle) async {        assert(bundle != null, "The bundle [RouteBundle.bundle] is null");    NavigatorState rootState = rootNavigatorKey.currentState;    assert(rootState != null, 'rootState [NavigatorState] is null');    switch (bundle.route) {      case "/routeExample":        return await rootState.push(          _buildRoute<T>(            bundle: bundle,            child: RouteExample(),          ),        );      case "/my_page":        return await rootState.push(          _buildRoute<T>(            bundle: bundle,            child: MyPage(),          ),        );      default:        throw Exception('Route is not found');    }  }

Данный метод вызывает у root NavigatorState (который описан в WidgetsApp) метод push и конфигурирует его относительно RouteBundle который приходит одним из аргументов в данный метод.


Теперь нужно реализовать класс RouteBundle. Это просто модель, которая хранит в себе набор полей для конфигурации.


enum ContainerType {  /// The parent type is [Scaffold].  ///  /// In IOS route with an iOS transition [CupertinoPageRoute].  /// In Android route with an Android transition [MaterialPageRoute].  ///  scaffold,  /// Used for show child in dialog.  ///  /// Route with [DialogRoute].  dialog,  /// Used for show child in [BottomSheet].  ///  /// Route with [ModalBottomSheetRoute].  bottomSheet,  /// Used for show child only.  /// [AppBar] and other features is not implemented.  window,}class RouteBundle {  /// Creates a bundle that can be used for [Router].  RouteBundle({    this.route,    this.containerType,  });  /// The route for current navigation.  ///  /// See [Routes] for details.  final String route;  /// The current status of this animation.  final ContainerType containerType;}

enum ContainerType тип контейнера, котрый будет задаваться декларативно из вызываемого кода.
RouteBundle класс-холдер данных отвечающих конфигурацию нового route.


Как вы могли заметить у я использовал метод _buildRoute. Именно он отвечает за то, кой тип route будет вызван.


Route<T> _buildRoute<T>({@required RouteBundle bundle, @required Widget child}) {    assert(bundle.containerType != null, "The bundle.containerType [RouteBundle.containerType] is null");    switch (bundle.containerType) {      case ContainerType.scaffold:        return CupertinoPageRoute<T>(          title: bundle.title,          builder: (BuildContext context) => child,          settings: RouteSettings(name: bundle.route),        );      case ContainerType.dialog:        return DialogRoute<T>(          title: '123',          settings: RouteSettings(name: bundle.route),          pageBuilder: (BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) {                        return child;                    },        );      case ContainerType.bottomSheet:        return ModalBottomSheetRoute<T>(          settings: RouteSettings(name: bundle.route),          isScrollControlled: true,          builder: (BuildContext context) => child,        );      case ContainerType.window:        return CupertinoPageRoute<T>(          settings: RouteSettings(name: bundle.route),          builder: (BuildContext context) => child,        );      default:        throw Exception('ContainerType is not found');    }  }

Думаю что в этой функции стоит рассказать о ModalBottomSheetRoute и DialogRoute, которые использую. Исходный код этих route позаимствован из раздела Material исходного кода Flutter.


Осталось сделать метод back.


@overrideFuture<bool> back<T>({T data, bool rootNavigator = false}) async {    NavigatorState rootState = rootNavigatorKey.currentState;  return await (rootState).maybePop<T>(data);}

Ну и конечно перед использованием сервиса необходимо передать rootNavigatorKey в App следующим образом:


MaterialApp(    navigatorKey: widget.router.rootNavigatorKey,    home: Home());

Кодовая база для нашего сервиса готова, давайте вызовем наш route. Для этого создадим инстанс нашего сервиса и каким-либо образом "прокинуть" в объект, который будет вызывать этот инстанс, например при помощи Dependency Injection.



router.routeTo(RouteBundle(route: '/my_page', containerType: ContainerType.window));

Теперь мы имеем единый подход к навигации, позволяющий решить вышеперечисленные проблемы:


  • Декларативный вызов навигации
  • Отказ от BuildContext по средствам GlobalKey
  • Модульность достигнута возможностью конфигурирования route относительно имени пути и контейнера для View

Итог


В Фреймворке Flutter существуют различные методы для навигации, которые дают преимущества и недостатки.


Ну и конечно полезные ссылки:
Мой телеграм канал
Мои друзья Flutter Dev Podcast
Вакансии Flutter разработчиков

Подробнее..

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

26.11.2020 12:17:06 | Автор: admin
В свежем превью 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

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

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>        <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.

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

Из песочницы Пишем автодополнение для ваших CLI проектов

20.08.2020 14:16:33 | Автор: admin

Приветствие


Всем привет! Хочу поделиться своим опытом написания кроссплатформенного проекта на C++ для интеграции автодополнения в CLI приложения, усаживайтесь поудобнее.




Формулировка задания


  • Приложение должно работать на Linux, macOS, Windows
  • Необходима возможность задавать правила для автодополнения
  • Предусмотреть наличие опечаток
  • Предусмотреть смену подсказок стрелками клавиатуры

Приготовления


Сразу предупрежу, использовать будем C++17


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


#if defined(_WIN32) || defined(_WIN64)    #define OS_WINDOWS#elif defined(__APPLE__) || defined(__unix__) || defined(__unix)    #define OS_POSIX#else    #error unsupported platform#endif

Также сделаем небольшую заготовку:


#if defined(OS_WINDOWS)    #define ENTER 13    #define BACKSPACE 8    #define CTRL_C 3    #define LEFT 75    #define RIGHT 77    #define DEL 83    #define UP 72    #define DOWN 80    #define SPACE 32#elif defined(OS_POSIX)    #define ENTER 10    #define BACKSPACE 127    #define SPACE 32    #define LEFT 68    #define RIGHT 67    #define UP 65    #define DOWN 66    #define DEL 51#endif    #define TAB 9

Так как мы нацелены на CLI проекты, и терминалы Linux и macOS имеют одинаковый API, объединим их в один define OS_POSIX. Windows, как всегда, стоит в стороне, вынесем для нее отдельный define OS_WINDOWS.


Следующим шагом мы должны понять, как будут выглядеть автодополнения. С самого начала я был вдохновлен Redis CLI, поэтому будем просто выводить посказки другим нейтральным цветом. Но это не помешает нам в итоге использовать любые цвета.


Следовательно, требуется написать функцию установки нужного цвета для вывода в консоль:


/** * Sets the console color. * * @param color System code of target color. * @return Input parameter os. */#if defined(OS_WINDOWS)std::string set_console_color(uint16_t color) {    SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE), color);    return "";#elif defined(OS_POSIX)std::string set_console_color(std::string color) {    return "\033[" + color + "m";#endif}

Опять таки из-за разницы API приходится искать компромисс, будем всегда возвращать строку для того, чтобы можно было использовать функцию после оператора вывода << для повышения читаемости кода.


Для тех, кому интересно, как именно работает API для цвета в Posix и Windows, и какие цветовые профили вообще бывают, предлагаю почитать ответы добрых людей на stackoverflow:



Так как нам придется постоянно перерисовывать строку из-за подсказок, необходимо написать функцию для "стирания" строки.


/** * Get count of terminal cols. * * @return Width of terminal. */#if defined(OS_WINDOWS)size_t console_width() {    CONSOLE_SCREEN_BUFFER_INFO info;    GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), &info);    short width = --info.dwSize.X;    return size_t((width < 0) ? 0 : width);}#endif/** * Clear terminal line. * * @param os Output stream. * @return input parameter os. */std::ostream& clear_line(std::ostream& os) {#if defined(OS_WINDOWS)    size_t width = console_width();    os << '\r' << std::string(width, ' ');#elif defined(OS_POSIX)    std::cout << "\033[2K";#endif    return os;}

На Posix платформах все просто, достаточно вывести в консоль \033[2K, но естественно в Windows нет аналогов, конкретно я не смог найти, приходится писать свою реализацию.


Осталось понять, как осуществлять ввод символов пользователем. Обычный cin явно не подойдет, в процессе считывания не получится выводить предсказания.


Тут приходит на ум функция _getch(), доступная в Windows, которая получает код символа нажатой клавиши на клавиатуре это именно то, что нам надо. Но в этот раз с Posix платформами все плохо, увы, но придется писать свою реализацию.


#if defined(OS_POSIX)/** * Read key without press ENTER. * * @return Code of key on keyboard. */int _getch() {    int ch;    struct termios old_termios, new_termios;    tcgetattr( STDIN_FILENO, &old_termios );    new_termios = old_termios;    new_termios.c_lflag &= ~(ICANON | ECHO );    tcsetattr( STDIN_FILENO, TCSANOW, &new_termios );    ch = getchar();    tcsetattr( STDIN_FILENO, TCSANOW, &old_termios );    return ch;}#endif

Правила автодополнения




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


git    config        --global            user.name                "[name]"            user.email                "[email]"        user.name            "[name]"        user.email            "[email]"    init        [repository name]    clone        [url]

Идея такая. За каждым словом могут идти слова на расстоянии 1 табуляции от него. Т.е. после слова git могут идти слова config, init и global. После слова config могут идти слова --global, user.name и user.email и т.д. Также введем возможность указывать опциональные слова, в моем случае это слова внутри символов [] (вместо этих слов пользователь должен вводить свои данные).


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


typedef std::map<std::string, std::vector<std::string>> Dictionary;

Давайте напишем функцию для парсинга файла с правилами.


/** * Parse config file to dictionary. * * @param file_path The path to the configuration file. * @return Tuple of dictionary with autocomplete rules, status of parsing and message. */std::tuple<Dictionary, bool, std::string> parse_config_file(const std::string& file_path) {    Dictionary dict;            // Словарь с правилами автозаполнения    std::map<int, std::string>  // Массив для запоминания корневого слова    root_words_by_tabsize;      //  для определенной длины табуляции    std::string line;           // Строка для чтения    std::string token;          // Полученное слово из строки    std::string root_word;      // Корневое слово для вставки в словарь как ключ    long tab_size = 0;          // Базовая длина табуляции (пробелов)    long tab_count = 0;         // Колличество табуляций в строке    // Открытие файла конфигураций    std::ifstream config_file(file_path);    // Возвращаем сообщение об ошибке, если файл не был открыт    if (!config_file.is_open()) {        return std::make_tuple(            dict,            false,            "Error! Can't open " + file_path + " file."        );    }    // Считываем все строки    while (std::getline(config_file, line)) {        // Пропускаем строку если она пустая        if (line.empty()) {            continue;        }        // Если в файле обнаружен символ табуляции, возвращаем сообщение о ошибке        if (std::count(line.begin(), line.end(), '\t') != 0) {            return std::make_tuple(                dict,                false,                "Error! Use a sequence of spaces instead of a tab character."            );        }        // Получение количества пробелов в начале строки        auto spaces = std::count(            line.begin(),            line.begin() + line.find_first_not_of(" "),            ' '        );        // Устанавливаем базовый размер табуляции, если        // была найдена строка с пробелами в начале        if (spaces != 0 && tab_size == 0) {            tab_size = spaces;        }        // Получаем слово из строки        token = trim(line);        // Проверка длины табуляции        if (tab_size != 0 && spaces % tab_size != 0) {            return std::make_tuple(                dict,                false,                "Error! Tab length error was made.\nPossibly in line: " + line            );        }        // Получаем количество табуляций        tab_count = (tab_size == 0) ? 0 : (spaces / tab_size);        // Запоминаем корневое слово для заданного количества табуляций        root_words_by_tabsize[tab_count] = token;        // Получаем корневое слово для текущего токена        root_word = (tab_count == 0) ? "" : root_words_by_tabsize[tab_count - 1];        // Вставка токена в словарь, если его там нет        if (std::count(dict[root_word].begin(), dict[root_word].end(), token) == 0) {            dict[root_word].push_back(token);        }    }    // Закрываем файл    config_file.close();    // Если все ОК возвращаем готовый словарь    return std::make_tuple(        dict,        true,        "Success. The rule dictionary has been created."    );}

Разберемся с накопившимися вопросами.


  1. Функция возвращает кортеж, так как по моему использование исключений не очень удачный вариант.
  2. Почему использование символа \t в файле запрещено? Потому что будем привыкать к хорошей практике использования последовательности пробелов вместо табуляции.
  3. Откуда взялась функция trim, и что она делает? Сейчас покажу ее простую реализацию.

/** * Remove extra spaces to the left and right of the string. * * @param str Source string. * @return Line without spaces on the left and right. */std::string trim(std::string_view str) {    std::string result(str);    result.erase(0, result.find_first_not_of(" \n\r\t"));    result.erase(result.find_last_not_of(" \n\r\t") + 1);    return result;}

Функция просто отрезает лишнее пространство слева и справа у строки


Автодополнение


Хорошо. У нас есть словарь с правилами, а что дальше? Осталось сделать само автодополнение.
Представим, что пользователь вводит что-то с клавиатуры. Что мы имеем? Одно или несколько введенных слов.


Давайте научимся получать последнее слово из строки.


/** * Get the position of the beginning of the last word. * * @param str String with words. * @return Position of the beginning of the last word. */size_t get_last_word_pos(std::string_view str) {    // Вернуть 0 если строка состоит только из пробелов    if (std::count(str.begin(), str.end(), ' ') == str.length()) {        return 0;    }    // Получаем позицию последнего пробела    auto last_word_pos = str.rfind(' ');    // Вернуть 0, если пробел не найден, иначе вернуть позицию + 1    return (last_word_pos == std::string::npos) ? 0 : last_word_pos + 1;}/** * Get the last word in string. * * @param str String with words. * @return Pair Position of the beginning of the *         last word and the last word in string. */std::pair<size_t, std::string> get_last_word(std::string_view str) {    // Поулчаем позицию    size_t last_word_pos = get_last_word_pos(str);    // Получаем последнее слово из строки    auto last_word = str.substr(last_word_pos);    // Возвращаем пару из слова и позиции слова в строке (для удобства)    return std::make_pair(last_word_pos, last_word.data());}

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


Давайте научимся получать предпоследнее слово из строки.


// Не использовал std::min из-за странного // поведения MSVC компилятора/** * Get the minimum of two numbers. * * @param a First value. * @param b Second value. * @return Minimum of two numbers. */size_t min_of(size_t a, size_t b) {    return (a < b) ? a : b;}/** * Get the penultimate words. * * @param str String with words. * @return Pair Position of the beginning of the penultimate *         word and the penultimate word in string. */std::pair<size_t, std::string> get_penult_word(std::string_view str) {    // Находим правую границу поиска    size_t end_pos = min_of(str.find_last_not_of(' ') + 2, str.length());    // Получаем позицию начала последнего слова    size_t last_word = get_last_word_pos(str.substr(0, end_pos));    size_t penult_word_pos = 0;    std::string penult_word = "";    // Находим предпоследнее слово если позиция     // начала последнего была найдена    if (last_word != 0) {        // Находим начало предпоследнего слова        penult_word_pos = str.find_last_of(' ', last_word - 2);        // Находим предпоследнее слово если позиция начала найдена        if (penult_word_pos != std::string::npos) {            penult_word = str.substr(penult_word_pos, last_word - penult_word_pos - 1);        }        // Иначе предпоследнее слово - все, что дошло до последнего слова        else {            penult_word = str.substr(0, last_word - 1);        }    }    // Обрезаем строку    penult_word = trim(penult_word);    // Возвращаем пару из позиции и слова (для удобства)    return std::make_pair(penult_word_pos, penult_word);}

Нахождение слов для автодополнения




Что же мы забыли? Функцию для нахождения слов, которые начинаются также, как и последнее слово в строке.


/** * Find strings in vector starts with substring. * * @param substr String with which the word should begin. * @param penult_word Penultimate word in user-entered line. * @param dict Vector of words. * @param optional_brackets String with symbols for optional values. * @return Vector with words starts with substring. */std::vector<std::string>words_starts_with(std::string_view substr, std::string_view penult_word,                  Dictionary& dict, std::string_view optional_brackets) {    std::vector<std::string> result;    // Выход если нет ключа равного penult_word или    // substr имеет символы для опциональных слов     if (!dict.count(penult_word.data()) ||        substr.find_first_of(optional_brackets) != std::string::npos)     {        return result;    }    // Возвращаем все слова, которые могут быть     // после last_word, если substr пуста    if (substr.empty()) {        return dict[penult_word.data()];    }    // Находим строки, начинающиеся с substr    std::vector<std::string> candidates_list = dict[penult_word.data()];    for (size_t i = 0 ; i < candidates_list.size(); i++) {        if (candidates_list[i].find(substr) == 0) {            result.push_back(dict[penult_word.data()][i]);        }    }    return result;}

А что по поводу проверки орфографии? Мы же хотели ее добавить? Давайте сделаем это.


/** * Find strings in vector similar to a substring (max 1 error). * * @param substr String with which the word should begin. * @param penult_word Penultimate word in user-entered line. * @param dict Vector of words. * @param optional_brackets String with symbols for optional values. * @return Vector with words similar to a substring. */std::vector<std::string>words_similar_to(std::string_view substr, std::string_view penult_word,                  Dictionary& dict, std::string_view optional_brackets) {    std::vector<std::string> result;    // Выход, если строка пустая    if (substr.empty()) {        return result;    }    std::vector<std::string> candidates_list = dict[penult_word.data()];    for (size_t i = 0 ; i < candidates_list.size(); i++) {        int errors = 0;        // Получаем кандидата        std::string candidate = candidates_list[i];        // Посимвольная проверка кандидата        for (size_t j = 0; j < substr.length(); j++) {            // Пропуск, если кандидат содержит символы для опциональных слов            if (optional_brackets.find_first_of(candidate[j]) != std::string::npos) {                errors = 2;                break;            }            if (substr[j] != candidate[j]) {                errors += 1;            }            if (errors > 1) {                break;            }        }        // Добавляем кандидата, если максимум одна ошибка        if (errors <= 1) {            result.push_back(candidate);        }    }    return result;}

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


/** * Get the word-prediction by the index. * * @param buffer String with user input. * @param dict Dictionary with rules. * @param number Index of word-prediction. * @param optional_brackets String with symbols for optional values. * @return Tuple of word-prediction, phrase for output, substring of buffer *         preceding before phrase, start position of last word. */std::tuple<std::string, std::string, std::string, size_t>get_prediction(std::string_view buffer, Dictionary& dict, size_t number,               std::string_view optional_brackets) {    // Получаем информацию о последнем слове    auto [last_word_pos, last_word] = get_last_word(buffer);    // Получаем информацию о предпоследнем слове    auto [_, penult_word] = get_penult_word(buffer);    std::string prediction; // предсказание    std::string phrase;     // фраза для вывода    std::string prefix;     // подстрока буфера, предшествующая фразе    // Ищем предсказания    std::vector<std::string> starts_with = words_starts_with(        last_word, penult_word, dict, optional_brackets    );    // Устанавливаем значения, если предсказания были найдены    if (!starts_with.empty()) {        prediction = starts_with[number % starts_with.size()];        phrase = prediction;        prefix = buffer.substr(0, last_word_pos);    }    // Если слова не были найдены    else {        // Ищем слова с учетом орфографии        std::vector<std::string> similar = words_similar_to(            last_word, penult_word, dict, optional_brackets        );        // Устанавливаем значения, если предсказания были найдены        if (!similar.empty()) {            prediction = similar[number % similar.size()];            phrase = " maybe you mean " + prediction + "?";            prefix = buffer;        }    }    // Возвращаем необходимые данные    return std::make_tuple(prediction, phrase, prefix, last_word_pos);}

Ввод пользователя с клавиатуры




Осталось одно из самых сложных заданий. Написать саму функцию ввода с клавиатуры.


/** * Gets current terminal cursor position. * * @return Y position of terminal cursor. */short cursor_y_pos() {#if defined(OS_WINDOWS)    CONSOLE_SCREEN_BUFFER_INFO info;    GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), &info);    return info.dwCursorPosition.Y;#elif defined(OS_POSIX)    struct termios term, restore;    char ch, buf[30] = {0};    short i = 0, pow = 1, y = 0;    tcgetattr(0, &term);    tcgetattr(0, &restore);    term.c_lflag &= ~(ICANON|ECHO);    tcsetattr(0, TCSANOW, &term);    write(1, "\033[6n", 4);    for (ch = 0; ch != 'R'; i++) {        read(0, &ch, 1);        buf[i] = ch;    }    i -= 2;    while (buf[i] != ';') {        i -= 1;    }    i -= 1;    while (buf[i] != '[') {        y = y + ( buf[i] - '0' ) * pow;        pow *= 10;        i -= 1;    }    tcsetattr(0, TCSANOW, &restore);    return y;#endif}/** * Move terminal cursor at position x and y. * * @param x X position to move. * @param x Y position to move. * @return Void. */void goto_xy(short x, short y) {#if defined(OS_WINDOWS)    COORD xy {--x, y};    SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE), xy);#elif defined(OS_POSIX)    printf("\033[%d;%dH", y, x);#endif}/** * Printing user input with prompts. * * @param buffer String - User input. * @param dict Vector of words. * @param line_title Line title of CLI when entering command. * @param number Hint number. * @param optional_brackets String with symbols for optional values. * @param title_color System code of title color     (line title color). * @param predict_color System code of predict color (prediction color). * @param default_color System code of default color (user input color). * @return Void. */#if defined(OS_WINDOWS)void print_with_prompts(std::string_view buffer, Dictionary& dict,                        std::string_view line_title, size_t number,                        std::string_view optional_brackets,                        uint16_t title_color, uint16_t predict_color,                        uint16_t default_color) {#elsevoid print_with_prompts(std::string_view buffer, Dictionary& dict,                        std::string_view line_title, size_t number,                        std::string_view optional_brackets,                        std::string title_color, std::string predict_color,                        std::string default_color) {#endif    // Получить прогнозируемую фразу и часть буфера, предшествующую фразе    auto [_, phrase, prefix, __] =         get_prediction(buffer, dict, number, optional_brackets);    std::string delimiter = line_title.empty() ? "" : " ";    std::cout << clear_line;    std::cout << '\r' << set_console_color(title_color) << line_title              << set_console_color(default_color) << delimiter << prefix              << set_console_color(predict_color) << phrase;    std::cout << '\r' << set_console_color(title_color) << line_title              << set_console_color(default_color) << delimiter << buffer;}/** * Reading user input with autocomplete. * * @param dict Vector of words. * @param optional_brackets String with symbols for optional values. * @param title_color System code of title color     (line title color). * @param predict_color System code of predict color (prediction color). * @param default_color System code of default color (user input color). * @return User input. */#if defined(OS_WINDOWS)std::string input(Dictionary& dict, std::string_view line_title,                  std::string_view optional_brackets, uint16_t title_color,                  uint16_t predict_color, uint16_t default_color) {#elsestd::string input(Dictionary& dict, std::string_view line_title,                  std::string_view optional_brackets, std::string title_color,                  std::string predict_color, std::string default_color) {#endif    std::string buffer;       // Буфер    size_t offset = 0;        // Смещение курсора от конца буфера    size_t number = 0;        // Номер (индекс) посдказки, для переключения    short y = cursor_y_pos(); // Получаем позицию курсора по оси Y в терминале    // Игнорируемые символы    #if defined(OS_WINDOWS)    std::vector<int> ignore_keys({1, 2, 19, 24, 26});    #elif defined(OS_POSIX)    std::vector<int> ignore_keys({1, 2, 4, 24});    #endif    while (true) {        // Выводим строку пользователя с предсказанием        print_with_prompts(buffer, dict, line_title, number, optional_brackets,                           title_color, predict_color, default_color);        // Перемещаем курсор в нужную позицию        short x = short(            buffer.length() + line_title.length() + !line_title.empty() + 1 - offset        );        goto_xy(x, y);        // Считываем очередной символ        int ch = _getch();        // Возвращаем буфер, если нажат Enter        if (ch == ENTER) {            return buffer;        }        // Обработка выхода из CLI в Windows        #if defined(OS_WINDOWS)        else if (ch == CTRL_C) {            exit(0);        }        #endif        // Изменение буфера при нажатии BACKSPACE        else if (ch == BACKSPACE) {            if (!buffer.empty() && buffer.length() - offset >= 1) {                buffer.erase(buffer.length() - offset - 1, 1);            }        }        // Применение подсказки при нажатии TAB        else if (ch == TAB) {            // Получаем необходимую информацию            auto [prediction, _, __, last_word_pos] =                 get_prediction(buffer, dict, number, optional_brackets);            // Дописываем предсказание, если имеется            if (!prediction.empty() &&                 prediction.find_first_of(optional_brackets) == std::string::npos) {                buffer = buffer.substr(0, last_word_pos) + prediction + " ";            }            // Очищаем индекс подсказки и смещение            offset = 0;            number = 0;        }        // Обработка стрелок        #if defined(OS_WINDOWS)        else if (ch == 0 || ch == 224)        #elif defined(OS_POSIX)        else if (ch == 27 && _getch() == 91)        #endif                switch (_getch()) {                    case LEFT:                        // Увеличьте смещение, если нажата левая клавиша                        offset = (offset < buffer.length())                                     ? offset + 1                                    : buffer.length();                        break;                    case RIGHT:                        // Уменьшить смещение, если нажата правая клавиша                        offset = (offset > 0) ? offset - 1 : 0;                        break;                    case UP:                        // Увеличить индекс подсказки                        number = number + 1;                        std::cout << clear_line;                        break;                    case DOWN:                        // Уменьшить индекс подсказки                        number = number - 1;                        std::cout << clear_line;                        break;                    case DEL:                    // Изменение буфера, при нажатии DELETE                    #if defined(OS_POSIX)                    if (_getch() == 126)                    #endif                    {                        if (!buffer.empty() && offset != 0) {                            buffer.erase(buffer.length() - offset, 1);                            offset -= 1;                        }                    }                    default:                        break;                }        // Добавить символ в буфер с учетом смещения        // при нажатии любой другой клавиши        else if (!std::count(ignore_keys.begin(), ignore_keys.end(), ch)) {            buffer.insert(buffer.end() - offset, (char)ch);            if (ch == SPACE) {                number = 0;            }        }    }}

В принципе, все готово. Давайте проверим наш код в деле.


Пример использования


#include <iostream>#include <string>#include "../include/autocomplete.h"int main() {    // Расположение файла конфигурации    std::string config_file_path = "../config.txt";    // Символы, с которых начинаются опциональные     // значения (необязательный параметр)    std::string optional_brackets = "[";    // Возможность задать цвет    #if defined(OS_WINDOWS)        uint16_t title_color = 160; // by default 10        uint16_t predict_color = 8; // by default 8        uint16_t default_color = 7; // by default 7    #elif defined(OS_POSIX)        // Set the value that goes between \033 and m ( \033{your_value}m )        std::string title_color = "0;30;102";  // by default 92        std::string predict_color = "90";      // by default 90        std::string default_color = "0";       // by default 90    #endif    // Перменная для заголовка строки    size_t command_counter = 0;    // Получаем словарь    auto [dict, status, message] = parse_config_file(config_file_path);    // Если получение словаря успешно    if (status) {        std::cerr << "Attention! Please run the executable file only" << std::endl                  << "through the command line!\n\n";        std::cerr << "- To switch the prompts press UP or DOWN arrow." << std::endl;        std::cerr << "- To move cursor press LEFT or RIGHT arrow." << std::endl;        std::cerr << "- To edit input press DELETE or BACKSPACE key." << std::endl;        std::cerr << "- To apply current prompt press TAB key.\n\n";        // Начинаем слушать        while (true) {            // Заготавливаем заголовок строки            std::string line_title = "git [" + std::to_string(command_counter) + "]:";            // Ожидаем ввода пользователя с отображением подсказок            std::string command = input(dict, line_title, optional_brackets,                                        title_color, predict_color, default_color);            // Делаем что-нибудь с полученной строкой            std::cout << std::endl << command << std::endl << std::endl;            command_counter++;        }    }    // Вывод сообщения, если файл конфигурации не был считан    else {        std::cerr << message << std::endl;    }    return 0;}



Код был проверен на macOS, Linux, Windows. Все работает отлично.


Заключение:


Как вы видите, писать кроссплатформенный код довольно не просто (в нашем случае пришлось писать, то что есть на Windows из коробки для Linux вручную и наоборот), однако это очень интересно и сам факт, что это все работает на всех трех ОС, крайне доставляет.


Надеюсь, я был кому-нибудь полезен. Если вам есть что дополнить, буду внимательно слушать в комментариях.


Исходный код можно взять тут.
Пользуйтесь на здоровье.

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru