WinPhone: пути к совершенству

в 20:11, , рубрики: .net, XAML, Программирование, разработка под windows phone, метки: , , ,

В этой статье я расскажу о том, как сделать не сложный, но интерактивный и функциональный графический редактор-рисовалку под Windows Phone. Думаю, даже опытные разработчики смогут найти для себя что-то интересное и новое. Уникальной фишкой редактора будет история, которую можно в буквальном смысле отматывать на нужнай момент с помощью ползунка-слайдера. И да, в завершение мы нарисуем радугу! Поехали…

Конечно же, я подготовил качественный пример.

image

Расширения разметки (markup extensions)

Когда я только начал разрабатывать на WinPhone, практически сразу был разочарован целым рядом ограничений этой платформы. Например, оказалось, что здесь даже нет привычных расширений разметки, как в WPF или Silverlight. Ведь, например, для локализации или картинок намного красивее писать следующий код в xaml:

<TextBlock Text={Localizing Hello}/>
<Button Content={Picture New.png}/>

«Как же так?! Ведь настолько удобная штука», — подумал я и на досуге решил исследовать этот вопрос детальнее, и не зря.

Покопавшись дизассемблером от решарпера в библиотечных классах, я вдруг заметил, что класс Binding не помечен атрибутом sealed, в отличие от аналогичного в WPF. А что если унаследоваться от него, мелькнуло в голове? Попробвал, и получилось!

   public abstract class MarkupExtension : Binding, IValueConverter
    {
        protected MarkupExtension()
        {
            Source = Converter = this;
        }

        protected MarkupExtension(object source) // set Source to null for using DataContext
        {
            Source = source;
            Converter = this;
        }

        protected MarkupExtension(RelativeSource relativeSource)
        {
            RelativeSource = relativeSource;
            Converter = this;
        }

        public abstract object Convert(object value, Type targetType, object parameter, CultureInfo culture);

        public virtual object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }

Вот такой базовый класс вышел. А дальше пример реализации расширения для локализации.

    public class Localizing : MarkupExtension
    {
        public static readonly LocalizingManager Manager = new LocalizingManager();

        public Localizing()
        {
            Source = Manager;
            Path = new PropertyPath("Source");
        }

        public string Key { get; set; }

        public override string ToString()
        {
            return Convert(Manager.Source, null, Key, Thread.CurrentThread.CurrentCulture) as string ??
                   string.Empty;
        }

        public override object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            var key = Key;
            var resourceManager = value as ResourceManager;
            if (resourceManager == null || string.IsNullOrEmpty(key)) return ":" + key + ":";
            var localizedValue = resourceManager.GetString(key);
            return localizedValue ?? ":" + key + ":";
        }
    }

    public class LocalizingManager : INotifyPropertyChanged
    {
        private ResourceManager _source;
        public ResourceManager Source
        {
            get { return _source; }
            set
            {
                _source = value;
                PropertyChanged(this, new PropertyChangedEventArgs("Source"));
            }
        }

        public event PropertyChangedEventHandler PropertyChanged = (sender, args) => { };
    }

Теперь и на WinPhone можно писать

 <TextBlock Text="{f:Localizing Key=ApplicationTitle}"/>

К сожалению, от префикса f: избавиться не удалось из-за ограничений платформы, также обязательно указывать имя свойства Key, но и это уже лучше, чем стандартная запись

<TextBlock Text="{Binding Path=LocalizedResources.ApplicationTitle, Source={StaticResource LocalizedStrings}}"/>

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

Предостерегу от возможного подводного камня. Если вы используете конструктор MarkupExtension(RelativeSource relativeSource), то в метод Convert в параметре value придёт контрол, к которому осуществляется привязка. С контролом вы можете делать всё, что угодно, но если будете хранить на него жёсткую ссылку, то может возникнуть ситуация с утечкой памяти (например, если расширение связано со статическим экземпляром класса, то будет удерживаться от сборки мусора часть интерфейса, даже в том случае, когда представление закрыто и не нужно). Поэтому используйте для подобных целей ленивые ссылки (WeakReferences).

Разделяемые команды (shared commands)

С командами в WinPhone дела обстоят не очень хорошо, их нужно реализовывать самому. Причём, тут тоже нужно быть осторожным, поскольку контрол подписывается на событие CanExecuteChanged, и если эту подписку неграмотно реализовать, то можно получить всё те же утечки памяти. Я и сам не обращал внимания на данный нюанс, но мне указал на него мой товарищ и отличный разработчик Юрий Калинов, за что хочу поблагодарить его. Почитать об этой проблеме можно тут.

То ли дело в WPF существует прекрасный механизм RoutedCommands и CommandBindings. В предыдущей статье я рассказал, как его можно красиво использовать. А что если реализовать на WinPhone нечто подобное? Посвятив вечер этой задачей, я-таки достиг некоторово результата и реализовал концепцию разделяемых команд (shared commands). Они не маршрутизируются по визуальному дереву, но для задач приложения подходят как нельзя лучше. Сразу отсылаю читателя к примеру, чтобы увидеть их реализацию, здесь же расскажу, как ими пользоваться. Всё просто и удобно, во вью модели пишем как-то так

this[SharedCommands.Back].CanExecute += (sender, args) => args.CanExecute = TouchIndex > 0;
this[SharedCommands.Next].CanExecute += (sender, args) => args.CanExecute = TouchIndex < _toches.Count;
this[SharedCommands.Back].Executed += (sender, args) => TouchIndex--;
this[SharedCommands.Next].Executed += (sender, args) => TouchIndex++; 

А на представлении примерно следующее

<Button Command="{f:Command Key=Back}" Content="{f:Picture Key=/Resources/IconSet/Next.png, Width=32, Height=32}"/>
<Button Command="{f:Command Key=Next}" Content="{f:Picture Key=/Resources/IconSet/Next.png, Width=32, Height=32}"/>

Лямбда-выражения

Нотифиация свойств вью-модели посредством лямбда-выражений это классика

        public Tool Tool
        {
            get { return Get(() => Tool); }
            set { Set(() => Tool, value); }
        }

Ещё очень удобно перегрузить индексатор

            this[() => Background].PropertyChanged += (sender, args) =>
            {
                Canvas.Children.Clear();
                Canvas.Background = Background;
                _toches.GetRange(0, TouchIndex).ForEach(Canvas.Children.Add);
            };

Если у кого-то ещё остаются опасения насчёт скорости работы таких конструкций, то смею их развеять — всё работает быстро. Помню, однажды кто-то выложил на хабре дизассемблированные исходники клиентской версии скайпа для WinPhone, а затем эти исходники оперативно убрали, но я успел их скачать и подсмотреть, как там устроены вью-модели. Это не показатель, конечно, но как раз-таки в них применялись лямбда-выражения для нотификации свойств. Думаю, что скайп делали умные люди из Майкрасофт, поэтому некоторое доверие к ним есть. А исходники скайпа потом были удалены с компьютера ;)

Рисование

Мы подошли к самому интересному – рисованию. Как его лучше всего организовать с учётом мобильности платформы? На мой взгляд, проще и логичнее всего будет воспользоваться стандартными средствами, а именно использовать примитивы Canvas, Polyline и тому подобные. Почему рекомендую именно их, а не велосипед на основе, например, WritableBitmap, да потому, что для рендеринга интерфейса используется графическое ядро либо SIMD инструкции процессора, но если мы будем делать рисование руками, то просто переложим нагрузку на обычные инструкции процессора, что снизит производительность и значительно усложнит разработку.

Суть механизма в следующем. У нас есть холст (Canvas), который отображается на интерфейсе, а любые прикосновения к нему мы интерпретируем как мазки определённой кистью. Для каждого мазка создаётся примитив, а затем он добавляется в коллекцию Canvas.Children. Но что если у нас будет тысяча таких мазков, отразится ли это на производительности? Да, отразится существенно, поэтому нужно хотя бы временами делать растеризацию изображения, то есть очищать Canvas.Children и устанавливать в Canvas.Background тот рисунок, который получился на данный момент. Выглядит это примерно так

            var raster = new WriteableBitmap(Canvas, null);
            Canvas.Background = new ImageBrush
            {
                AlignmentX = AlignmentX.Left,
                AlignmentY = AlignmentY.Top,
                Stretch = Stretch.None,
                ImageSource = raster,
            };

            Canvas.Children.Clear();

Растеризация в примере происходит каждый раз перед добавлением нового примитива, но как же нам тогда организовать историю, ведь при растеризации она утрачивается? Здесь тоже ничего сложного – всего лишь сохраним изначальный Background холста, который был до начала рисования, и заведём коллекцию List<UIElement> _toches для хранения всех прикосновений. Когда нам нужно отмотать историю на определённый момент, мы просто восстанавливаем первоначальнвй фон у холста и переносим нужное число элементов из _toches в Canvas.Children, при этом элементы не удаляются из коллекции _toches.

В приложении рисование реализовано двумя способами: на основе Polyline и свойства OpacityMask у Canvas. Второй способ мне подсказал талантливый программист Ярошевич Юрий, за что говорю ему спасибо.

Интерес представляет также логика работы стёрки (вода). Рассказывать о ней сейчас не буду, но самые пытливые могут сами изучить этот вопрос.

Радуга

Как и обещал, рассказываю о рисовании палитры цветов, которую можно применять для выбора кисти. Всё, что нам нужно – это радужная кисть.

        <LinearGradientBrush x:Key="RainbowBrush" StartPoint="0 0" EndPoint="0 0.6">
            <LinearGradientBrush.Transform>
                <ScaleTransform ScaleY="2.8"/>
            </LinearGradientBrush.Transform>
            <LinearGradientBrush.GradientStops>
                <GradientStop Color="#FFFF0000" Offset="0.0"/>
                <GradientStop Color="#FFFFFF00" Offset="0.1"/>
                <GradientStop Color="#FF00FF00" Offset="0.2"/>
                <GradientStop Color="#FF00FFFF" Offset="0.3"/>
                <GradientStop Color="#FF0000FF" Offset="0.4"/>
                <GradientStop Color="#FFFF00FF" Offset="0.5"/>
                <GradientStop Color="#FFFF0000" Offset="0.6"/>
            </LinearGradientBrush.GradientStops>
        </LinearGradientBrush>

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

Всем мир!

Автор: poemmuse

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js