На новый год сделал себе подарок - телефон
Samsung Omnia W под управлением прекрасной Windows Phone 7. Естественно, попробовать что то написать для этого телефона было вопросом времени. Я выбрал калькулятор, так как это самый простой способ показать, насколько легко начать программировать под WP7, даже не используя никаких сенсоров/датчиков. В результате я получил работающее приложение с кучей функций, а также библиотеку расчёта математических выражений. Вот как это выглядит
Под катом описание процесса разработки.
Общее описание того, что я хочу получить:
1. Будет простой текстбокс, выражение в который можно будет записать как кнопками калькулятора, так и встроенной клавиатурой.
2. По нажатию кнопки "=" будет происходить разбор указанного выражения и результаты разбора появятся под текстбоксом
3. Поскольку кнопок будет немало, я использую панорамное представление. То есть наборы кнопок можно будет просто пролистывать.
Теперь о разборе выражения. Пожалуй, это можно было бы вынести и в отдельный пост, но я опишу тут, так как это неотъемлемая часть разрабатываемого калькулятора. Итак, выражения:
1. Библиотека обработки должна быть легкой и гибкой в настройке.
2. Первоначально я планирую разбирать только унарные и бинарные операции, унарные функции. Бинарный функции я пока не буду рассматривать.
Часть 1. Разработка библиотеки парсинга выражений
Рассмотрим выражение. Выражение содержит в себе строковое своё представление и может быть вычисляемым, или не вычисляемым, если разрешить строковое представление не удастся
- public interface IExpression
- {
- double Value { get; }
- bool HasValue { get; }
- string StringExpression { get; }
- }
Далее. Выражение состоит из операндов и операций. Операция = это действие над операндами. А операнды - это те же выражения. То есть операция - это по сути действие над одним или более выражением, позволяющее получить результат.
- public interface IOperation
- {
- double Value { get; }
- }
Я буду рассматривать 2 варианта операций: бинарные и унарные. То есть с двумя операндами (сложение, вычитание) и с одним операндом (синус, косинус).
Унарная операция
- public abstract class UnaryOperation : IOperation
- {
- protected readonly IExpression Ex;
-
- protected UnaryOperation(IExpression ex)
- {
- Ex = ex;
- }
-
- #region Implementation of IOperation
-
- public abstract double Value { get; }
-
- #endregion
- }
Бинарная операция
- public abstract class BinaryOperation : IOperation
- {
- protected readonly IExpression Ex1;
- protected readonly IExpression Ex2;
-
- protected BinaryOperation(IExpression exp1, IExpression exp2)
- {
- Ex1 = exp1;
- Ex2 = exp2;
- }
-
- #region Implementation of IOperation
-
- public abstract double Value { get; }
-
- #endregion
- }
Как пример, покажу реализацию бинарной операции, основанной на лямбда выражении.
- public class LabdaBinaryOperation : BinaryOperation
- {
- private readonly Func<double, double, double> _func;
-
- public LabdaBinaryOperation(Func<double, double, double> func, IExpression exp1, IExpression exp2) : base(exp1, exp2)
- {
- _func = func;
- }
-
- public override double Value
- {
- get { return _func(Ex1.Value, Ex2.Value); }
- }
- }
Отлично. У нас есть выражение и операция. Но как можно определить, какое выражение сожержит какую операцию? Для того, чтобы определить операцию по строковому выражению, я определил интерфейс, который возвращает операцию по строковому выражению
- public interface IOperationExecutor
- {
- IOperation GetOperation(string expression);
- }
Очевидно, логика класса OperationExecutor будет довольно сложная. Поэтому я ввёл 4 дополнительные абстракции IUnaryOperationProvider, IBinaryOperationProvider, IOperationRecognizer, IOperationRecognizerProvider. IOperationRecognizer - это объект, который содержит сигнатуру операции, и если он встречает совпадение сигнатуры и самой операции, то, используюя один из объектов IUnaryOperationProvider или IBinaryOperationProvider, создаёт операцию.
- public interface IOperationRecognizer
- {
- IOperation Recognize(string expression, IOperationExecutor operationExecutor);
- int Index(string expression, IOperationExecutor operationExecutor);
- }
Объект IOperationRecognizerProvider определяет приоритет операций и, соответственно, порядок разбора выражения.
- public interface IOperationRecognizerProvider
- {
- IEnumerable<IEnumerable<IOperationRecognizer>> GetRecognizers();
- }
В итоге, весь процесс разбора выражения выглядит следующим образом:
1. Выражение получает строку и объект IOperationExecutor, который даст доступ к операции.
- public sealed class Expression : IExpression
- {
- private readonly string _expression;
- private readonly IOperationExecutor _operationExecutor;
- private IOperation _operation;
-
- public static Expression Create(string expression, IOperationExecutor operationExecutor)
- {
- return new Expression(expression, operationExecutor);
- }
-
- private Expression(string expression, IOperationExecutor operationExecutor)
- {
- _expression = expression;
- _operationExecutor = operationExecutor;
- }
-
- #region Implementation of IExpression
-
- public string StringExpression
- {
- get { return _expression; }
- }
-
- public double Value
- {
- get
- {
- if (_operation == null) _operation = _operationExecutor.GetOperation(_expression);
- if (_operation == null) throw new NotSupportedException(_expression);
- return _operation.Value;
- }
- }
-
- public bool HasValue
- {
- get
- {
- if (_operation == null) _operation = _operationExecutor.GetOperation(_expression);
- return _operation != null;
- }
- }
-
- #endregion
- }
2. IOperationExecutor, с помощью IOperationRecognizerProvider, будет проходить по различным распознавателям операций (IOperationRecognizer), находя нужные операции и выполняя их
3. В результате получаем либо число типа Double, либо исключение NotSupportedException с участком выражения, которое распознать не удалось.
Для того, чтобы весь механизм запустить, нужно настроить объект IOperationRecognizerProvider и использовать его. Вот пример оъекта, который я создал для калькулятора:
- public class CalculatorOperationRecognizerProvider : IOperationRecognizerProvider
- {
- #region Implementation of IOperationRecognizerProvider
-
- /// <summary>
- /// тут находятся рекогнайзеры операций в обратном порядке приоритета.
- /// </summary>
- /// <returns></returns>
- public IEnumerable<IEnumerable<IOperationRecognizer>> GetRecognizers()
- {
- return new List<IEnumerable<IOperationRecognizer>>
- {
- new List<IOperationRecognizer>
- {
- new BinaryOperationRecognizer(
- "+",
- new LambdaBinaryOperationProvider((x, y) => x + y)),
- new BinaryOperationRecognizer(
- "-",
- new LambdaBinaryOperationProvider((x, y) => x - y))
- },
-
- new List<IOperationRecognizer>
- {
- new BinaryOperationRecognizer(
- "*",
- new LambdaBinaryOperationProvider((x, y) => x*y)),
- new BinaryOperationRecognizer(
- "/",
- new LambdaBinaryOperationProvider((x, y) => x/y)),
- },
-
-
- new List<IOperationRecognizer>
- {
- new BinaryOperationRecognizer(
- "^",
- new LambdaBinaryOperationProvider(Math.Pow)),
- },
-
-
- new List<IOperationRecognizer>
- {
- new UnaryFunctionRecognizer(
- "sin",
- new LambdaUnaryOperationProvider(Math.Sin)),
- new UnaryFunctionRecognizer(
- "cos",
- new LambdaUnaryOperationProvider(Math.Cos)),
- new UnaryFunctionRecognizer(
- "tan",
- new LambdaUnaryOperationProvider(Math.Tan)),
- new UnaryFunctionRecognizer(
- "ctan",
- new LambdaUnaryOperationProvider(x => 1/Math.Tan(x))),
-
-
- new UnaryFunctionRecognizer(
- "asin",
- new LambdaUnaryOperationProvider(Math.Asin)),
- new UnaryFunctionRecognizer(
- "acos",
- new LambdaUnaryOperationProvider(Math.Acos)),
- new UnaryFunctionRecognizer(
- "atan",
- new LambdaUnaryOperationProvider(Math.Atan)),
- new UnaryFunctionRecognizer(
- "actan",
- new LambdaUnaryOperationProvider(x => (Math.PI*0.5) - Math.Atan(x))),
-
- new UnaryFunctionRecognizer(
- "abs",
- new LambdaUnaryOperationProvider(Math.Abs)),
-
- new UnaryFunctionRecognizer(
- "sqrt",
- new LambdaUnaryOperationProvider(Math.Sqrt)),
- },
-
- new List<IOperationRecognizer>
- {
- new BracketsOperationRecognizer(
- new LambdaUnaryOperationProvider(x => x)),
- new AbsoluteBracketsOperationRecognizer(
- new LambdaUnaryOperationProvider(Math.Abs))
- },
-
- new List<IOperationRecognizer>
- {
- new ConstantRecognizer(
- "pi",
- Math.PI,
- new NumberOperationProvider()),
- new ConstantRecognizer(
- "e",
- Math.E,
- new NumberOperationProvider())
- },
-
- new List<IOperationRecognizer>
- {
- new NumberOperationRecognizer(
- new NumberOperationProvider())
- },
-
- };
- }
-
- #endregion
- }
В итоге я получил библиотеку разбора выражений, которую легко настроить или изменить логику всего лишь реализовав нужные интерфейсы.
Часть 2. Калькулятор на Silverlight с использованием Mvvm
Сам по себе подход и использованием Mvvm подразумевает, что у нас будет Xaml страница, в качестве контекста данных для неё будет выступать специальный класс ViewModel, а изменения между этим классом и представлением будут синхронизированы посредством двухстороннего биндинга. В принципе, ничего сложного.
Итак, начем с конструирования ViewModel. Оговорюсь сразу, я использую библиотеку
Galasoft.MvvmLight, так как она позволяет довольно просто связать события контролов в представлении с командами в классе ViewModel.
Так как все команды, которые есть в калькуляторе, могут изменять состояние модели, я определил базовый класс для команды
- public abstract class CalculatorCommand : ICommand
- {
- protected MainViewModel _target;
-
- protected CalculatorCommand(MainViewModel target)
- {
- _target = target;
- }
-
- public bool CanExecute(object parameter)
- {
- return _target != null;
- }
-
- public abstract void Execute(object parameter);
-
- public event EventHandler CanExecuteChanged;
- }
Далее, я разделил все команды на те, которые добавляют текст
- public class AddToTextCommand : CalculatorCommand
- {
- public AddToTextCommand(MainViewModel target) : base(target)
- {
-
- }
-
- public override void Execute(object parameter)
- {
- var str = parameter as String;
- if (!string.IsNullOrEmpty(str))
- {
- _target.CalculatorExpression += str;
- }
- }
- }
Очищают поле ввода
- public class ClearCommand : CalculatorCommand
- {
- public ClearCommand(MainViewModel target)
- : base(target)
- {
-
- }
-
- public override void Execute(object parameter)
- {
- _target.CalculatorExpression = string.Empty;
- }
- }
Убирают последний символ (если он есть)
- public class RemoveLastCharCommand : CalculatorCommand
- {
- public RemoveLastCharCommand(MainViewModel target) : base(target)
- {
-
- }
-
- public override void Execute(object parameter)
- {
- if (!string.IsNullOrEmpty(_target.CalculatorExpression))
- _target.CalculatorExpression = _target.CalculatorExpression.Substring(0, _target.CalculatorExpression.Length - 1);
- }
- }
И производят разбор и вычисление выражения
- public class ExecuteExpressionCommand: CalculatorCommand
- {
- public ExecuteExpressionCommand(MainViewModel target)
- : base(target)
- {
-
- }
-
- public override void Execute(object parameter)
- {
- try
- {
- var oex = new OperationExecutor(new CalculatorOperationRecognizerProvider());
- var ex = Expression.Create(_target.CalculatorExpression, oex);
- _target.CalculatorResult = Math.Round(ex.Value, 10).ToString(CultureInfo.InvariantCulture);
- }
- catch(Exception ex)
- {
- _target.CalculatorResult = string.Format("Expression '{0}' not suported", ex.Message);
- }
- }
- }
Реалиация класса MainViewModel тривиальна
- public class MainViewModel : ViewModelBase
- {
- public MainViewModel()
- {
- AddToTextCommand = new AddToTextCommand(this);
- RemoveLastCharCommand = new RemoveLastCharCommand(this);
- ClearCommand = new ClearCommand(this);
- ExecuteExpressionCommand = new ExecuteExpressionCommand(this);
- CalculatorResult = " ";
- }
-
- public ICommand AddToTextCommand { get; set; }
- public ICommand RemoveLastCharCommand { get; set; }
- public ICommand ClearCommand { get; set; }
- public ICommand ExecuteExpressionCommand { get; set; }
-
-
- private string _calculatorExpression;
- public string CalculatorExpression
- {
- get
- {
- return _calculatorExpression;
- }
- set
- {
- if (_calculatorExpression != value)
- {
- _calculatorExpression = value;
- RaisePropertyChanged("CalculatorExpression");
- }
- }
- }
-
-
- private string _calculatorResult;
- public string CalculatorResult
- {
- get
- {
- return _calculatorResult;
- }
- set
- {
- if (_calculatorResult != value)
- {
- _calculatorResult = value;
- RaisePropertyChanged("CalculatorResult");
- }
- }
- }
- }
Осталось только разработать страницу, на которой всё это будет отображаться. Нет смысла приводить тут весь её код, покажу только основные моменты:
1. Использоваие статических ресурсов заложено в шаблоне приложения, и это хорошо, так как при разных темах значения этих ресурсов может изменяться:
- FontFamily="{StaticResource PhoneFontFamilyNormal}"
- FontSize="{StaticResource PhoneFontSizeNormal}"
- Foreground="{StaticResource PhoneForegroundBrush}"
Теперь, если пользователь изменит тему с тёмной на светлую, то приложение легко к этому подстроится
2. Контрол Panorama я разместил под текстбоксом. Это позволяет перелистывать страницы не теряя текстбокс из виду.
3. Привязка команд к событиям происходит декларативно, благодаря библиотеке
Galasoft.MvvmLight
- <Button Content="+" Grid.Row="4" Grid.Column="3" >
- <i:Interaction.Triggers>
- <i:EventTrigger EventName="Click">
- <Command:EventToCommand Command="{Binding AddToTextCommand, Mode=OneWay}" CommandParameter="+" />
- </i:EventTrigger>
- </i:Interaction.Triggers>
- </Button>
4. Ну, и последнее. При смене ориантации телефона мне хотелось увидеть анимацию перехода страницы из одного состояния в другое. Этого я добился с помощью библиотеки
Silverlight Toolkit
- public partial class MainPage : PhoneApplicationPage
- {
- private readonly MainViewModel _mainViewModel;
- PageOrientation _lastOrientation;
-
- // Constructor
- public MainPage()
- {
- InitializeComponent();
- var locator = new ViewModelLocator();
- _mainViewModel = locator.Main;
- DataContext = _mainViewModel;
-
- OrientationChanged += MainPageOrientationChanged;
- _lastOrientation = Orientation;
- }
-
- void MainPageOrientationChanged(object sender, OrientationChangedEventArgs e)
- {
- var newOrientation = e.Orientation;
-
- var transitionElement = new RotateTransition();
-
- switch (newOrientation)
- {
- case PageOrientation.Landscape:
- case PageOrientation.LandscapeRight:
- transitionElement.Mode = _lastOrientation == PageOrientation.PortraitUp ? RotateTransitionMode.In90Counterclockwise : RotateTransitionMode.In180Clockwise;
- break;
- case PageOrientation.LandscapeLeft:
- transitionElement.Mode = _lastOrientation == PageOrientation.LandscapeRight ? RotateTransitionMode.In180Counterclockwise : RotateTransitionMode.In90Clockwise;
- break;
- case PageOrientation.Portrait:
- case PageOrientation.PortraitUp:
- transitionElement.Mode = _lastOrientation == PageOrientation.LandscapeLeft ? RotateTransitionMode.In90Counterclockwise : RotateTransitionMode.In90Clockwise;
- break;
- default:
- break;
- }
-
- var phoneApplicationPage = (PhoneApplicationPage)(((PhoneApplicationFrame)Application.Current.RootVisual)).Content;
- var transition = transitionElement.GetTransition(phoneApplicationPage);
- transition.Completed += delegate
- {
- transition.Stop();
- };
- transition.Begin();
-
- _lastOrientation = newOrientation;
- }
- }
На этом закончена основная работа над калькулятором. Оставалось только навести красоту, да прикрутить иконку (которую я снова взял среди
бесплатных). Я назвал проект Calculon и выложил его на
кодеплекс, так что вы легко можете скачать исходники и поиграть с кодом. Также я отправил работу в маркет, приложение сделал, естественно, бесплатным.
В итоге я получил калькулятор, работающий на Windows Phone 7. Учитывая, что моей специализацией является Web разработка, и что я не профессионал в Silverlight, возможность так легко написать готовое приложения для мобильного телефона говорит о низком пороге вхождения в технологию.
Результат работы:
На этом всё. Всем спасибо.