понедельник, 2 января 2012 г.

Разрабатываем простой калькулятор на Silverlight и Windows Phone 7

На новый год сделал себе подарок - телефон Samsung Omnia W под управлением прекрасной Windows Phone 7. Естественно, попробовать что то написать для этого телефона было вопросом времени. Я выбрал калькулятор, так как это самый простой способ показать, насколько легко начать программировать под WP7, даже не используя никаких сенсоров/датчиков. В результате я получил работающее приложение с кучей функций, а также библиотеку расчёта математических выражений. Вот как это выглядит



Под катом описание процесса разработки.

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

Теперь о разборе выражения. Пожалуй, это можно было бы вынести и в отдельный пост, но я опишу тут, так как это неотъемлемая часть разрабатываемого калькулятора. Итак, выражения:
1. Библиотека обработки должна быть легкой и гибкой в настройке.
2. Первоначально я планирую разбирать только унарные и бинарные операции, унарные функции. Бинарный функции я пока не буду рассматривать.

 Часть 1. Разработка библиотеки парсинга выражений

Рассмотрим выражение. Выражение содержит в себе строковое своё представление и может быть вычисляемым, или не вычисляемым, если разрешить строковое представление не удастся

  1. public interface IExpression
  2. {
  3.     double Value { get; }
  4.     bool HasValue { get; }
  5.     string StringExpression { get; }
  6. }

Далее. Выражение состоит из операндов и операций. Операция = это действие над операндами. А операнды - это те же выражения. То есть операция - это по сути действие над одним или более выражением, позволяющее получить результат.

  1. public interface IOperation
  2. {
  3.     double Value { get; }
  4. }

Я буду рассматривать 2 варианта операций: бинарные и унарные. То есть с двумя операндами (сложение, вычитание) и с одним операндом (синус, косинус).

Унарная операция

  1. public abstract class UnaryOperation : IOperation
  2. {
  3.     protected readonly IExpression Ex;
  4.  
  5.     protected UnaryOperation(IExpression ex)
  6.     {
  7.         Ex = ex;
  8.     }
  9.  
  10.     #region Implementation of IOperation
  11.  
  12.     public abstract double Value { get; }
  13.  
  14.     #endregion
  15. }

Бинарная операция

  1. public abstract class BinaryOperation : IOperation
  2. {
  3.     protected readonly IExpression Ex1;
  4.     protected readonly IExpression Ex2;
  5.  
  6.     protected BinaryOperation(IExpression exp1, IExpression exp2)
  7.     {
  8.         Ex1 = exp1;
  9.         Ex2 = exp2;
  10.     }
  11.  
  12.     #region Implementation of IOperation
  13.  
  14.     public abstract double Value { get; }
  15.  
  16.     #endregion
  17. }

Как пример, покажу реализацию бинарной операции, основанной на лямбда выражении.


  1. public class LabdaBinaryOperation : BinaryOperation
  2. {
  3.     private readonly Func<double, double, double> _func;
  4.  
  5.     public LabdaBinaryOperation(Func<double, double, double> func, IExpression exp1, IExpression exp2) : base(exp1, exp2)
  6.     {
  7.         _func = func;
  8.     }
  9.  
  10.     public override double Value
  11.     {
  12.         get { return _func(Ex1.Value, Ex2.Value); }
  13.     }
  14. }

Отлично. У нас есть выражение и операция. Но как можно определить, какое выражение сожержит какую операцию? Для того, чтобы определить операцию по строковому выражению, я определил интерфейс, который возвращает операцию по строковому выражению

  1. public interface IOperationExecutor
  2. {
  3.     IOperation GetOperation(string expression);
  4. }

Очевидно, логика класса OperationExecutor будет довольно сложная. Поэтому я ввёл 4 дополнительные абстракции IUnaryOperationProvider, IBinaryOperationProvider, IOperationRecognizer, IOperationRecognizerProvider. IOperationRecognizer - это объект, который содержит сигнатуру операции, и если он встречает совпадение сигнатуры и самой операции, то, используюя один из объектов IUnaryOperationProvider или IBinaryOperationProvider, создаёт операцию.

  1. public interface IOperationRecognizer
  2. {
  3.     IOperation Recognize(string expression, IOperationExecutor operationExecutor);
  4.     int Index(string expression, IOperationExecutor operationExecutor);
  5. }

Объект IOperationRecognizerProvider определяет приоритет операций и, соответственно, порядок разбора выражения.

  1. public interface IOperationRecognizerProvider
  2. {
  3.     IEnumerable<IEnumerable<IOperationRecognizer>> GetRecognizers();
  4. }

В итоге, весь процесс разбора выражения выглядит следующим образом:
1. Выражение получает строку и объект IOperationExecutor, который даст доступ к операции.

  1. public sealed class Expression : IExpression
  2. {
  3.     private readonly string _expression;
  4.     private readonly IOperationExecutor _operationExecutor;
  5.     private IOperation _operation;
  6.  
  7.     public static Expression Create(string expression, IOperationExecutor operationExecutor)
  8.     {
  9.         return new Expression(expression, operationExecutor);
  10.     }
  11.  
  12.     private Expression(string expression, IOperationExecutor operationExecutor)
  13.     {
  14.         _expression = expression;
  15.         _operationExecutor = operationExecutor;
  16.     }
  17.  
  18.     #region Implementation of IExpression
  19.  
  20.     public string StringExpression
  21.     {
  22.         get { return _expression; }
  23.     }
  24.  
  25.     public double Value
  26.     {
  27.         get
  28.         {
  29.             if (_operation == null) _operation = _operationExecutor.GetOperation(_expression);
  30.             if (_operation == null) throw new NotSupportedException(_expression);
  31.                 return _operation.Value;
  32.         }
  33.     }
  34.  
  35.     public bool HasValue
  36.     {
  37.         get
  38.         {
  39.             if (_operation == null) _operation = _operationExecutor.GetOperation(_expression);
  40.             return _operation != null;
  41.         }
  42.     }
  43.  
  44.     #endregion
  45. }

2. IOperationExecutor, с помощью IOperationRecognizerProvider, будет проходить по различным распознавателям операций (IOperationRecognizer), находя нужные операции и выполняя их
3. В результате получаем либо число типа Double, либо исключение NotSupportedException с участком выражения, которое распознать не удалось.

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

  1. public class CalculatorOperationRecognizerProvider : IOperationRecognizerProvider
  2. {
  3.     #region Implementation of IOperationRecognizerProvider
  4.  
  5.     /// <summary>
  6.     /// тут находятся рекогнайзеры операций в обратном порядке приоритета.
  7.     /// </summary>
  8.     /// <returns></returns>
  9.     public IEnumerable<IEnumerable<IOperationRecognizer>> GetRecognizers()
  10.     {
  11.         return new List<IEnumerable<IOperationRecognizer>>
  12.                    {
  13.                        new List<IOperationRecognizer>
  14.                            {
  15.                                new BinaryOperationRecognizer(
  16.                                 "+",
  17.                                 new LambdaBinaryOperationProvider((x, y) => x + y)),
  18.                                new BinaryOperationRecognizer(
  19.                                 "-",
  20.                                 new LambdaBinaryOperationProvider((x, y) => x - y))
  21.                            },
  22.  
  23.                        new List<IOperationRecognizer>
  24.                            {
  25.                                new BinaryOperationRecognizer(
  26.                                 "*",
  27.                                 new LambdaBinaryOperationProvider((x, y) => x*y)),
  28.                                new BinaryOperationRecognizer(
  29.                                 "/",
  30.                                 new LambdaBinaryOperationProvider((x, y) => x/y)),
  31.                            },
  32.  
  33.  
  34.                        new List<IOperationRecognizer>
  35.                            {
  36.                                new BinaryOperationRecognizer(
  37.                                 "^",
  38.                                 new LambdaBinaryOperationProvider(Math.Pow)),
  39.                            },
  40.  
  41.  
  42.                        new List<IOperationRecognizer>
  43.                            {
  44.                                new UnaryFunctionRecognizer(
  45.                                 "sin",
  46.                                 new LambdaUnaryOperationProvider(Math.Sin)),
  47.                                new UnaryFunctionRecognizer(
  48.                                 "cos",
  49.                                 new LambdaUnaryOperationProvider(Math.Cos)),
  50.                                new UnaryFunctionRecognizer(
  51.                                 "tan",
  52.                                 new LambdaUnaryOperationProvider(Math.Tan)),
  53.                                new UnaryFunctionRecognizer(
  54.                                 "ctan",
  55.                                 new LambdaUnaryOperationProvider(x => 1/Math.Tan(x))),
  56.  
  57.  
  58.                                new UnaryFunctionRecognizer(
  59.                                 "asin",
  60.                                 new LambdaUnaryOperationProvider(Math.Asin)),
  61.                                new UnaryFunctionRecognizer(
  62.                                 "acos",
  63.                                 new LambdaUnaryOperationProvider(Math.Acos)),
  64.                                new UnaryFunctionRecognizer(
  65.                                 "atan",
  66.                                 new LambdaUnaryOperationProvider(Math.Atan)),
  67.                                new UnaryFunctionRecognizer(
  68.                                 "actan",
  69.                                 new LambdaUnaryOperationProvider(x => (Math.PI*0.5) - Math.Atan(x))),
  70.  
  71.                                new UnaryFunctionRecognizer(
  72.                                 "abs",
  73.                                 new LambdaUnaryOperationProvider(Math.Abs)),
  74.  
  75.                                new UnaryFunctionRecognizer(
  76.                                 "sqrt",
  77.                                 new LambdaUnaryOperationProvider(Math.Sqrt)),
  78.                            },
  79.  
  80.                        new List<IOperationRecognizer>
  81.                            {
  82.                                new BracketsOperationRecognizer(
  83.                                 new LambdaUnaryOperationProvider(x => x)),
  84.                                new AbsoluteBracketsOperationRecognizer(
  85.                                 new LambdaUnaryOperationProvider(Math.Abs))
  86.                            },
  87.  
  88.                        new List<IOperationRecognizer>
  89.                            {
  90.                                new ConstantRecognizer(
  91.                                 "pi",
  92.                                 Math.PI,
  93.                                 new NumberOperationProvider()),
  94.                                new ConstantRecognizer(
  95.                                 "e",
  96.                                 Math.E,
  97.                                 new NumberOperationProvider())
  98.                            },
  99.  
  100.                        new List<IOperationRecognizer>
  101.                            {
  102.                                new NumberOperationRecognizer(
  103.                                 new NumberOperationProvider())
  104.                            },
  105.  
  106.                    };
  107.     }
  108.  
  109.     #endregion
  110. }

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

 Часть 2. Калькулятор на Silverlight с использованием Mvvm

Сам по себе подход и использованием Mvvm подразумевает, что у нас будет Xaml страница, в качестве контекста данных для неё будет выступать специальный класс ViewModel, а изменения между этим классом и представлением будут синхронизированы посредством двухстороннего биндинга. В принципе, ничего сложного.
Итак, начем с конструирования ViewModel. Оговорюсь сразу, я использую библиотеку Galasoft.MvvmLight, так как она позволяет довольно просто связать события контролов в представлении с командами в классе ViewModel.
Так как все команды, которые есть в калькуляторе, могут изменять состояние модели, я определил базовый класс для команды

  1. public abstract class CalculatorCommand : ICommand
  2. {
  3.      protected MainViewModel _target;
  4.  
  5.     protected CalculatorCommand(MainViewModel target)
  6.     {
  7.         _target = target;
  8.     }
  9.  
  10.     public bool CanExecute(object parameter)
  11.     {
  12.         return _target != null;
  13.     }
  14.  
  15.     public abstract void Execute(object parameter);
  16.  
  17.     public event EventHandler CanExecuteChanged;
  18. }

Далее, я разделил все команды на те, которые добавляют текст

  1. public class AddToTextCommand : CalculatorCommand
  2. {
  3.     public AddToTextCommand(MainViewModel target) : base(target)
  4.     {
  5.         
  6.     }
  7.     
  8.     public override void Execute(object parameter)
  9.     {
  10.         var str = parameter as String;
  11.         if (!string.IsNullOrEmpty(str))
  12.         {
  13.             _target.CalculatorExpression += str;
  14.         }
  15.     }
  16. }

Очищают поле ввода

  1. public class ClearCommand : CalculatorCommand
  2. {
  3.     public ClearCommand(MainViewModel target)
  4.         : base(target)
  5.     {
  6.         
  7.     }
  8.  
  9.     public override void Execute(object parameter)
  10.     {
  11.         _target.CalculatorExpression = string.Empty;
  12.     }
  13. }

Убирают последний символ (если он есть)

  1. public class RemoveLastCharCommand : CalculatorCommand
  2. {
  3.     public RemoveLastCharCommand(MainViewModel target) : base(target)
  4.     {
  5.         
  6.     }
  7.     
  8.     public override void Execute(object parameter)
  9.     {
  10.         if (!string.IsNullOrEmpty(_target.CalculatorExpression))
  11.         _target.CalculatorExpression = _target.CalculatorExpression.Substring(0, _target.CalculatorExpression.Length - 1);
  12.     }
  13. }

И производят разбор и вычисление выражения

  1. public class ExecuteExpressionCommand: CalculatorCommand
  2. {
  3.     public ExecuteExpressionCommand(MainViewModel target)
  4.         : base(target)
  5.     {
  6.         
  7.     }
  8.  
  9.     public override void Execute(object parameter)
  10.     {
  11.         try
  12.         {
  13.             var oex = new OperationExecutor(new CalculatorOperationRecognizerProvider());
  14.             var ex = Expression.Create(_target.CalculatorExpression, oex);
  15.             _target.CalculatorResult = Math.Round(ex.Value, 10).ToString(CultureInfo.InvariantCulture);
  16.         }
  17.         catch(Exception ex)
  18.         {
  19.             _target.CalculatorResult = string.Format("Expression '{0}' not suported", ex.Message);
  20.         }
  21.     }
  22. }

Реалиация класса MainViewModel тривиальна

  1. public class MainViewModel : ViewModelBase
  2. {
  3.     public MainViewModel()
  4.     {
  5.         AddToTextCommand = new AddToTextCommand(this);
  6.         RemoveLastCharCommand = new RemoveLastCharCommand(this);
  7.         ClearCommand = new ClearCommand(this);
  8.         ExecuteExpressionCommand = new ExecuteExpressionCommand(this);
  9.         CalculatorResult = " ";
  10.     }
  11.  
  12.     public ICommand AddToTextCommand { get; set; }
  13.     public ICommand RemoveLastCharCommand { get; set; }
  14.     public ICommand ClearCommand { get; set; }
  15.     public ICommand ExecuteExpressionCommand { get; set; }
  16.  
  17.     
  18.     private string _calculatorExpression;
  19.     public string CalculatorExpression
  20.     {
  21.         get
  22.         {
  23.             return _calculatorExpression;
  24.         }
  25.         set
  26.         {
  27.             if (_calculatorExpression != value)
  28.             {
  29.                 _calculatorExpression = value;
  30.                 RaisePropertyChanged("CalculatorExpression");
  31.             }
  32.         }
  33.     }
  34.  
  35.  
  36.     private string _calculatorResult;
  37.     public string CalculatorResult
  38.     {
  39.         get
  40.         {
  41.             return _calculatorResult;
  42.         }
  43.         set
  44.         {
  45.             if (_calculatorResult != value)
  46.             {
  47.                 _calculatorResult = value;
  48.                 RaisePropertyChanged("CalculatorResult");
  49.             }
  50.         }
  51.     }
  52. }

Осталось только разработать страницу, на которой всё это будет отображаться. Нет смысла приводить тут весь её код, покажу только основные моменты:

1. Использоваие статических ресурсов заложено в шаблоне приложения, и это хорошо, так как при разных темах значения этих ресурсов может изменяться:

  1. FontFamily="{StaticResource PhoneFontFamilyNormal}"
  2. FontSize="{StaticResource PhoneFontSizeNormal}"
  3. Foreground="{StaticResource PhoneForegroundBrush}"

Теперь, если пользователь изменит тему с тёмной на светлую, то приложение легко к этому подстроится


2. Контрол Panorama я разместил под текстбоксом. Это позволяет перелистывать страницы не теряя текстбокс из виду.
3. Привязка команд к событиям происходит декларативно, благодаря библиотеке Galasoft.MvvmLight

  1. <Button Content="+" Grid.Row="4" Grid.Column="3" >
  2.                             <i:Interaction.Triggers>
  3.                                 <i:EventTrigger EventName="Click">
  4.                                     <Command:EventToCommand Command="{Binding AddToTextCommand, Mode=OneWay}" CommandParameter="+" />
  5.                                 </i:EventTrigger>
  6.                             </i:Interaction.Triggers>
  7.                         </Button>
4. Ну, и последнее. При смене ориантации телефона мне хотелось увидеть анимацию перехода страницы из одного состояния в другое. Этого я добился с помощью библиотеки Silverlight Toolkit

  1. public partial class MainPage : PhoneApplicationPage
  2. {
  3.     private readonly MainViewModel _mainViewModel;
  4.     PageOrientation _lastOrientation;
  5.  
  6.     // Constructor
  7.     public MainPage()
  8.     {
  9.         InitializeComponent();
  10.         var locator = new ViewModelLocator();
  11.         _mainViewModel = locator.Main;
  12.         DataContext = _mainViewModel;
  13.  
  14.         OrientationChanged += MainPageOrientationChanged;
  15.         _lastOrientation = Orientation;
  16.     }
  17.     
  18.     void MainPageOrientationChanged(object sender, OrientationChangedEventArgs e)
  19.     {
  20.         var newOrientation = e.Orientation;
  21.  
  22.         var transitionElement = new RotateTransition();
  23.  
  24.         switch (newOrientation)
  25.         {
  26.             case PageOrientation.Landscape:
  27.             case PageOrientation.LandscapeRight:
  28.                 transitionElement.Mode = _lastOrientation == PageOrientation.PortraitUp ? RotateTransitionMode.In90Counterclockwise : RotateTransitionMode.In180Clockwise;
  29.                 break;
  30.             case PageOrientation.LandscapeLeft:
  31.                 transitionElement.Mode = _lastOrientation == PageOrientation.LandscapeRight ? RotateTransitionMode.In180Counterclockwise : RotateTransitionMode.In90Clockwise;
  32.                 break;
  33.             case PageOrientation.Portrait:
  34.             case PageOrientation.PortraitUp:
  35.                 transitionElement.Mode = _lastOrientation == PageOrientation.LandscapeLeft ? RotateTransitionMode.In90Counterclockwise : RotateTransitionMode.In90Clockwise;
  36.                 break;
  37.             default:
  38.                 break;
  39.         }
  40.         
  41.         var phoneApplicationPage = (PhoneApplicationPage)(((PhoneApplicationFrame)Application.Current.RootVisual)).Content;
  42.         var transition = transitionElement.GetTransition(phoneApplicationPage);
  43.         transition.Completed += delegate
  44.         {
  45.             transition.Stop();
  46.         };
  47.         transition.Begin();
  48.  
  49.         _lastOrientation = newOrientation;
  50.     }
  51. }

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

В итоге я получил калькулятор, работающий на Windows Phone 7. Учитывая, что моей специализацией является Web разработка, и что я не профессионал в Silverlight, возможность так легко написать готовое приложения для мобильного телефона говорит о низком пороге вхождения в технологию.

Результат работы:

На этом всё. Всем спасибо.

20 комментариев:

  1. Да, у меня тоже есть решение вычисления выражения из строки вот тут при помощи AST.
    У этого решения есть несколько ограничений.

    ОтветитьУдалить
  2. Поясни, какие ограничения имеешь ввиду?

    ОтветитьУдалить
  3. Один вопрос, Вы платили $100 за регистрацию и возможность разработки для своего устройства? Если да, можете расписать процедуру? И какие плюсы это даёт. Или может есть альтернативный способ?

    ОтветитьУдалить
  4. Да, я заплатил 2600 рублей. В принципе, для регистрации, достаточно зарегаться вот тут, заполнить нужные поля и заплатить деньги. Я пользовался картой Альфа-банка, потому весь процесс у меня занял не более получаса. Это даёт возможность разблокировать до 3х устройств, а значит, вы сможете тестировать свои программы на реальных телефонах. Однако, чтобы иметь возможность получать деньги оттуда, нужно сделать ещё телодвижения.
    Больше информации можно легко нагуглить (например, раз и два)

    ОтветитьУдалить
  5. У меня проблема такая. Есть TextBox.
    1. Как сделать так, чтобы ввести туда можно было только цифры?
    2. Как правильно обработать его значение, чтобы можно было выполнять математические операции (ведь со String их провести не получается)?

    ОтветитьУдалить
  6. Сергей
    1. Непонятно, зачем оно вам надо. Если есть требования вычислять только арифметику, тогда есть смысл. А нужно ли себя ограничивать?
    2. Тут есть два подхода. Первый - ОПН и ему подобные разборы строки стеком. Второй - это AST. Пример есть в моем блоге.

    ОтветитьУдалить
  7. Видите ли, проблема в том, что в программировании я не разбираюсь абсолютно. Но простые приложения писать для WP7 получается.
    А тут - застопорился. Проблема в том, что мне необходимо проводить математические операции с текстом из TextBox, и я, конечно же, при такой попытке получаю ошибку, что так нельзя.
    Как это решить? Помогите мне, пожалуйста.

    ОтветитьУдалить
  8. Ну проводить математические операции с текстом никак не получится. Если этот самый текст содержит, например, только целое число, то нужно его сначала привести методом Int32.Parse к Int32. Потом с используйте результат в вычислениях.

    ОтветитьУдалить
  9. Сергей.
    1. <TextBox InputScope="Number"></TextBox>
    2. Если Вы почитаете статью, то увидите, что я разбираю именно String

    Я приветствую, когда программисты стремятся развиваться, и это хорошо, когда вы задаёте вопросы, но хотелось бы, чтобы прежде, чем спрашивать, вы попробовали поискать информацию хотя бы в каком-либо поисковике. А ещё лучше воспользоваться тематическим форумом (например, gotdotnet, MSDN, wp7forum).

    ОтветитьУдалить
  10. Напишите, пожалуйста, саму строку.
    Есть значение "text_pri.Text", нужно его превратить из строкового в числовое.

    ОтветитьУдалить
  11. Пользовался поисковиком, но конкретно ничего не нашел...
    При использовании "Number" (это было) выдается клавиатура с числами, но помимо чисел там ещё множество сторонних нематематических символов, и избавиться от них не выходит.

    И всё-таки сейчас меня больше волнует вопрос о конвертации значения строки.

    ОтветитьУдалить
  12. var str = "987";
    double number;
    if (double.TryParse(str, out number))
    {
    // Получилось разобрать
    }
    else
    {
    // пользователь ввёл не число
    }

    ОтветитьУдалить
  13. Спасибо большое!
    Но это - проверка на возможность перевода в число, а сам перевод?

    ОтветитьУдалить
  14. где тут сторонние нематематические символы?

    var str = "987";
    double number;
    // Иногда необходимо учитывать культуру
    if (double.TryParse(str, NumberStyles.Any, CultureInfo.InvariantCulture, out number))
    {
    // Получилось разобрать
    // результат разбора в переменной number
    // её можно уже использовать

    var sqrt = Math.Sqrt(number);
    }
    else
    {
    // пользователь ввёл не число
    }

    ОтветитьУдалить
  15. Спасибо огромное!!)) Проблему с конвертацией решил. А про символы...
    Странно - но у меня при клавиатуре Number выдается обычная клавиатура, разве что в режиме чисел и символов...

    ОтветитьУдалить
  16. Сергей. Я Вам посоветовал бы сперва изучить матчасть. Найдите книгу по .NET или ещё лучше по WP7 и пройдитесь по ней от начала и до конца. Это даст Вам понимание, как оно всё работает. Книга по .NET же вам даст основы работы с платформой - я считаю, что это необходимый минимум для начала программирования телефона. Сам вот уже скачал эту книгу, но всё руки никак не дойдут поглядеть.

    ОтветитьУдалить
    Ответы
    1. поможешь калькулятор написать для windows phone расчитывать закон Ома . а то мозгов не хватает буду рад помощи

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

      Удалить
  17. А где класс BinaryOperationRecognizer?

    ОтветитьУдалить
    Ответы
    1. Я не приводил весь код, так как он и так выложен в интернете, о чем писал в этой статье. Вот сам класс.

      Удалить