Почему появилась эта публикация?


Недавно пришла мысль в голову: пора переходить на использование WPF! Ну и, соответственно, изучить надо его, чтобы также «виртуозно» использовать, как и WinForms. Заодно и более старшим, консервативным, коллегам доказать, что WPF использовать более эффективно и продуктивно. (И ПО на WPF работает «шутстрее».)
Вот и решил я этой публикацией «мешок зайцев настрелять»:
— Сравнить разработку компонента на WinForms (Ссылка на статью) и WPF;
— Доказать коллегам, что WPF – это продуктивно и эффективно;
— И сделать небольшой простой урок по освоению WPF (особенно для тех, кто привык к WinForms).

Изучение WPF для тех, кто с WinForms


В прошлой публикации ссылка я рассказал о разработке компонента на WinForms. Поэтому, цель следующая: сделать такой же «по-смыслу» компонент, но на WPF. В целом, подход такой же и советы для WinForms работают и на WPF.
Забегая вперёд, получился следующий компонент, представленный на рисунке ниже.

Компонент (далее – «dpkEditWPF»), так же как и его «собрат» на WinForms является «partial» (разделён на несколько файлов (partial class), для удобства разработки).


У него также есть Свойство «Значение слова ДПК» (32 разряда) и «Текстовая метка» (чтобы там время отображать). Событие клика по «битовой ячейке» с номером бита также имеется, но только оно стало «маршрутизируемым».
Пример 1
/*Общедоступные свойства, события, генераторы событий*/
    public partial class DpkWordEditWPF
    {
        /// <summary>
        /// Значение слова ДПК (32 разряда)
        /// </summary>
        uint _dpkValue;
        /// <summary>
        /// Свойство - Значение слова ДПК (32 разряда)
        /// </summary>
        public uint DpkValue { get { return _dpkValue; } set { _dpkValue = value; Paint(); InvalidateVisual(); } }
        /// <summary>
        /// Текстовая метка (добавляется к текстовому значению слова ДПК)
        /// </summary>
        string _txtMark;
        /// <summary>
        /// Свойство - Текстовая метка (добавляется к текстовому значению слова ДПК)
        /// </summary>
        public string TextMark { get { return _txtMark; } set { _txtMark = value; Paint(); InvalidateVisual(); } }
        /******/
        /// <summary>
        /// Событие - клик по значению
        /// </summary>
        public event ReturnEventHandler<int> ClickByValue;
        /// <summary>
        /// Генератор события клик по значению
        /// </summary>
        /// <param name="index">номер бита (0-31)</param>
        void OnClickByValue(int index)
        {
            if (ClickByValue != null)
                ClickByValue(this, new ReturnEventArgs<int>(index));
         }
    }


Расчёт размеров dpkEditWPF также – относительный, на основе текущих размеров.
Пример 2
/// <summary>
        /// Установка пропорций
        /// </summary>
        void SetProportions()
        {
            _hPropTextMark = 0.2;
            _hPropAddress = 0.2;
            _hPropAddressBinVal = 0.2;
            _hPropData = 0.2;
            _hPropDataBinVal = 0.2;
        }
        /// <summary>
        /// Установка размеров
        /// </summary>
        void SetSizes()
        {
            /*TextMark*/
            _heightTextMark = RenderSize.Height * _hPropTextMark;
            /*Address*/
            _heightAddress = RenderSize.Height * _hPropAddress;
            /*AddressBinValue*/
            _heightAddressBinVal = RenderSize.Height * _hPropAddressBinVal;
            _widthCellAddressBV = RenderSize.Width / 8.0;
            /*Data*/
            _heightData = RenderSize.Height * _hPropData;
            /*DataBinValue*/
            _heightDataBinVal = RenderSize.Height * _hPropDataBinVal;
            _widthCellDataBV = RenderSize.Width / 24.0;
            /*points*/
            _ptTextMark = new Point(0,0);
            _ptAddress = new Point(0,_heightTextMark);
            _ptAddressBinVal  = new Point(0, _heightTextMark + _heightAddress);
            _ptData  = new Point(0, _heightTextMark + _heightAddress + _heightAddressBinVal);
            _ptDataBinVal  = new Point(0, _heightTextMark + _heightAddress + _heightAddressBinVal + _heightDataBinVal);
        }


Отрисовка компонента также разделена на несколько визуальных буферов и соответственно -процедур отрисовки.
Пример 3
        /// <summary>
        /// визуальный Буфер значения слова ДПК + текстовая метка
        /// </summary>
        DrawingVisual _imgTextMark;
        /// <summary>
        /// визуальный буфер значения адреса
        /// </summary>
        DrawingVisual _imgAddress;
        /// <summary>
        /// визуальный буфер двоичного значения адреса (кликабельные ячейки)
        /// </summary>
        DrawingVisual _imgAddressBinVal;
        /// <summary>
        /// визуальный буфер значения данных
        /// </summary>
        DrawingVisual _imgData;
        /// <summary>
        /// визуальный буфер двоичного значения данных (кликабельные ячейки)
        /// </summary>
        DrawingVisual _imgDataBinVal;



Пример 4
        /// <summary>
        /// Отрисовка области текстового значения Адреса
        /// </summary>
        void PaintAddress()
        {
            if (_imgAddress == null) return;
            using (DrawingContext dc = _imgAddress.RenderOpen())
            {
                dc.DrawRectangle(Brushes.LightBlue, new Pen(Brushes.DarkGray, 1), new Rect(_ptAddress, new Size(RenderSize.Width, _heightAddress)));
                string str = "Адрес: 0x" + (_dpkValue & 0xFF).ToString("X").PadLeft(2, '0');
                DrawTxt(dc, str, _ptAddress, new Size(RenderSize.Width, _heightAddress), Brushes.Black);
            }
        }


Реализация клика по ячейке двоичного значения («битовой ячейке» с номером бита) реализована с помощью обработчика MouseUp, в котором генерируется событие клика по ячейке.
Пример 5
/// <summary>
        /// Обработка клика по ячейке
        /// </summary>
        void DpkWordEditWPF_ClickByValue(object sender, ReturnEventArgs<int> e)
        {
            uint mask = (uint)0x1 << e.Result;
            if ((_dpkValue & mask) > 0)
                _dpkValue &= (~mask); 
            else
                _dpkValue |= mask;
            Paint();
            InvalidateVisual();
        }
        /// <summary>
        /// обработка клика мыши (превращение в клик по ячейке)
        /// </summary>
        protected override void OnMouseUp(MouseButtonEventArgs e)
        {
            base.OnMouseUp(e);
            Point curPt = e.GetPosition(this);
            /*Клик в области адреса*/
            if ((curPt.X >= _ptAddressBinVal.X) && (curPt.X <= (_ptAddressBinVal.X+RenderSize.Width)))
                if ((curPt.Y >= _ptAddressBinVal.Y) && (curPt.Y <= (_ptAddressBinVal.Y + _heightAddressBinVal)))
                {
                    if ((curPt.X % _widthCellAddressBV) == 0) return;
                    int index = (int)(curPt.X / _widthCellAddressBV);
                    OnClickByValue(index);
                    return;
                }
            /*клик в области данных*/
            if ((curPt.X >= _ptDataBinVal.X) && (curPt.X <= (_ptDataBinVal.X + RenderSize.Width)))
                if ((curPt.Y >= _ptDataBinVal.Y) && (curPt.Y <= (_ptDataBinVal.Y + _heightDataBinVal)))
                {
                    if ((curPt.X % _widthCellDataBV) == 0) return;
                    int index = (int)(curPt.X / _widthCellDataBV);
                    OnClickByValue(index + 8);
                    return;
                }
        }


Масштабирование и вывод на экран сделаны по тому же принципу, что и в компоненте на WinForms. Масштабирование – вызов отрисовки в буфер и вывод на экран. Вывод на экран – отрисовка визуальных буферов.
Пример 6
/// <summary>
        /// первая отрисовка при появлении
        /// </summary>
        protected override void OnInitialized(EventArgs e)
        {
            base.OnInitialized(e);
            _imgTextMark = new DrawingVisual();
            _imgAddress = new DrawingVisual();
            _imgAddressBinVal = new DrawingVisual();
            _imgData = new DrawingVisual();
            _imgDataBinVal = new DrawingVisual();
            Paint();
            InvalidateVisual();
        }
        /// <summary>
        /// обработка вывода на экран
        /// </summary>
        protected override void OnRender(DrawingContext drawingContext)
        {
            base.OnRender(drawingContext);
            if (System.ComponentModel.DesignerProperties.GetIsInDesignMode(this))
            {
                /*режим дизайнера*/
                _dpkValue = 0xFFa42312;
                Paint();
            }
            drawingContext.DrawDrawing(_imgTextMark.Drawing);
            drawingContext.DrawDrawing(_imgAddress.Drawing);
            drawingContext.DrawDrawing(_imgAddressBinVal.Drawing);
            drawingContext.DrawDrawing(_imgData.Drawing);
            drawingContext.DrawDrawing(_imgDataBinVal.Drawing);
        }


Компонент написан на MS Visual Studio 2010, .Net Framework 4. Ссылка на проект: Ссылка на проект

Вывод


В целом код получился компактнее, чем на WinForms и работает «шустрее». Два тестовых проекта были запущены на машине со следующими характеристиками: Windows XP SP2, процессор 1-ядерный, 1 Гб ОЗУ.
WPF продемонстрировал работу без фризов при изменении размеров формы, WinForms – в свою очередь, изрядно «подтормаживал» при тех же манипуляциях.
P.S. Надеюсь, скоро все проекты на WPF будем на заводе делать.
P.P.S. Трудно бороться с консерватизмом!

Комментарии (32)


  1. A1ien
    12.09.2015 11:02
    +3

    Весь кайф в разработке на WPF раскрывается при использовании паттерна MVVM. А если воспользоваться еще и такой штукой как Caliburn.Micro то разработка вообще превращается в сплошное удовольствие:)
    ПС: да, и в вашем случае пользоваться кастом отрисовкой вообще нет ни какой необходимости, для этого в WPF существуют стили, DataTemplates и другие механизмы.


    1. BlackEngineer
      12.09.2015 11:32

      Да, согласен в WPF много хороших и удобных вещей, но в данном случае они не нужны…
      1. Пример — простой и призван показать лёкгость и простоту освоения (особенно для тех, кто с WinForms)
      2. Скорость работы (чем больше xaml кода, тем медленнее (нативный код c# всё-таки быстрее))
      3. Ну и не освоил я ещё MVVM — только изучаю


      1. A1ien
        12.09.2015 11:44

        Начните с переписывания вашего кода без кода рендеринга, и обработки низкоуровнего события от мыши. Его можно переписать на списко объектов, каждый из которых отвечает за представление бита(элемента) этот список при помощи байндинга привязать к какому нибуть итем контейнеру(ListBox) например, и определить DataTemplate для этого объекта и ControlTemplate для ListBox. Это и будет вашим первым погружением в MVVM :)


      1. A1ien
        12.09.2015 13:12

        Вот, накрапал маленький пример.
        github.com/AlexDovgan/ShortWpfExample


        1. BlackEngineer
          12.09.2015 22:33

          Благодарю


      1. impwx
        13.09.2015 00:33
        +3

        2. Это распространенное заблуждение. Разница в скорости если и будет заметна, то только на супер-сложных интерфейсах. В вашем случае вариант с идиоматической XAML-разметкой и биндингами ничуть не проиграл бы в скорости, зато код был бы на порядок чище и легче для поддержки.
        3. Стоило сначала более подробно изучить окружающие инструменты и подходы к разработке, а не кидаться писать обучающую статью на Хабр. Спешка тем более неуместна, учитывая что WPF существует уже десять лет.


  1. WNeZRoS
    12.09.2015 19:16
    +2

    Статья должна называться не «Разработка WPF пользовательских компонентов», а «Как быстро портировать WinForms компонент на WPF»


  1. Athari
    12.09.2015 23:38
    +6

    Неканонично. Слепое портирование с WinForms.

    public uint DpkValue { get { return _dpkValue; } set { _dpkValue = value; Paint(); InvalidateVisual(); } }

    Следует использовать dependency property с метаданными, которые предписывают перерисовать содержимое. Ну или цепочкой зависимостей приводить к изменению таких свойств.

    Никаких методов Paint быть не должно, такого понятия не существует.

    public event ReturnEventHandler<int> ClickByValue;

    Следует использовать RoutedEvent, чтобы события могли роутиться.

    А дальше уже совсем вакханалия пошла с ручной отрисовкой. Вам на что дадены биндинги, шаблоны, стили, триггеры и прочее? Не, круто, вы изменили названия классов с System.Windows.Forms на System.Windows. Но при чём здесь WPF?


  1. NeoNN
    13.09.2015 11:18
    +1

    Не знаю, есть ли смысл в таких статьях — с одной стороны, да, они показывают какие-то изменения к лучшему, но с другой стороны, использовать технологию без всех ее преимуществ — это как забивать гвозди микроскопом. Приходилось видеть некоторые такие проекты (и, о ужас, в начале профессиональной деятельности даже писать самому) и считаю, что для успешной работы все-таки сначала надо подтянуть матчасть, а потом уже применять технологию в production — иначе код со временем превратится в адовый ад и придется все переписывать.


  1. ZOXEXIVO
    13.09.2015 11:49
    +3

    Если сделать на WPF приложение не понимая смысла в WPF, то можно нарваться на ситуации, когда все будет тормозить и загружать любой процессор на 80%.
    После современного Web такие приложения уже кажутся динозаврами, даже если они и используют WPF.


  1. cyber_roach
    13.09.2015 19:07
    +2

    У нас целая тонна наработок по контролам WPF различной сложности.
    От всяких спидометров, джойстиков, контролов для пультов управления умных домов, кастомных ListView поддерживающих анимацию, игровых элементов и пр. до собственного окна с производительной прерастиризованной анимацией у навигации (как в WinStore приложениях)
    И главное — мы умеем потом это стилизовать в XAML, собственно без нормального XAML интерфейса, контрол на WPF теряет смысл.

    Если у аудитории есть интерес, могу попробовать что-то написать

    Извиняюсь за большие картинки под спойлером, нарезать поленился


    image
    image


    1. Athari
      15.09.2015 05:36

      Пишите, нормальных технических статей на Хабре никогда не бывает слишком много.


    1. darked
      15.09.2015 13:03
      +1

      Большая просьба написать!!! В инете так мало толково по WPF, где рисуют что — то посложнее треугольников и закрашенных кругов.


  1. CrazyViper
    13.09.2015 20:42
    +1

    Недавно пришла мысль в голову: пора переходить на использование WPF!
    Сначала прочитал это предложение как стеб, но почитал дальше и понял, что нет. Все уже давно обсуждают умер ли WPF или все еще жив, а вам только пришла в голову мысль переходить на него? Поздновато уже его начинать изучать — вам проще дождаться когда он окончательно умрет и перейти на следующую технологию.

    Ну и выше уже написали, что вы просто заменили названия классов с одного пространства имен на другое. Советую почитать про DataBinding, стили и прочее. Тогда ваш код станет еще более лаконичным и сможете коллегам доказать.


    1. lightman
      13.09.2015 21:01
      +1

      А какие-то технологии уже прогнозируются ему на замену?


      1. CrazyViper
        13.09.2015 21:13

        Microsoft пока хранит молчание и говорит что для десктопов главная технология — это WPF. Но этой технологии уже почти 10 лет. Да, она хороша и выполняет свои задачи на отлично. Но сейчас десктопы — это узкий сегмент рынка корпоративных систем. Все остальное — это веб/мобаил. Да и java постепенно перебирается на рынок десктопных приложений.


    1. NeoNN
      13.09.2015 21:10

      А кто его хоронит? В Windows Store тот же самый (ну ладно, почти тот же самый) XAML, всякие MVVM с калибурнами и Behaviors. Плюс эта тема недавно подробно освещалась в одной из передач на .net rocks, и на замену WPF для десктопных приложений вроде как пока ничего не предвидится.


      1. withkittens
        13.09.2015 21:15
        +1

        WPF != XAML. WPF и Windows Store обе используют XAML, но под капотом они разные.
        В конце концов, XAML — это просто язык разметки.


        1. NeoNN
          14.09.2015 10:47

          Вы можете привести несколько примеров кардинальной разницы между разработкой wpf и win store приложения? Я говорю здесь о разработке views и view models. Я понимаю, что подмножества языка разные, но чем отличается процесс рендеринга в том и другом случае, например? Цикл Measure-arrange тот же самый, стили и шаблоны, поведения и триггеры, которые, конечно, беднее, но они есть. MVVM используется точно таким же образом, Caliburn.micro ставится из nuget. Blend работает. Итак, в чем же существенная разница для разработчика?


          1. withkittens
            14.09.2015 11:13
            +1

            Между разработкой на Wpf и WinRT/UAP кардинальной разницы нет. Можно сказать, в большинстве случаев просто отличаются пространства имён, синтаксис там и сям, где-то плюс фича, где-то минус.

            А вот рендеринг и технологии под капотом разные. Wpf был создан во времена DirectX 9 и, хоть он GPU-accelerated, рендеринг неоптимизирован и очень груб. WinRT Xaml же — это Direct2D (появившийся, если не ошибаюсь, в WIndows 8). То, каким бы мог быть Wpf.

            Есть отличная статья на эту тему — A Critical Deep Dive into the WPF Rendering System (хабрахабровский перевод).


            1. NeoNN
              14.09.2015 23:11

              Большое спасибо за статью.


            1. Ilya81
              22.09.2015 15:32

              По указанной в статье причине как раз для вспомогательных геометрических объектов я всегда использую OnRender. Хотя, о том, как работает DrawingContext в статье почти не написано. Но если это не просто какие-то линии и фигуры, а они должны принимать нажатия мыши и т. п. случаи, т. е. когда их проще делать элементами пользовательского интерфейса, и их нужно больше примерно 10-и — вот тут и обнаруживается слабое место WPF — то, что он шлёт их в память GPU по одному.


      1. CrazyViper
        13.09.2015 21:23

        Хоронить его пытаются все кому не лень. Уже лет 5 как. Но я пока сам на нем пишу и не жалуюсь — он стабилен, багов мало, в корпоративном сегменте еще лет 10 проживет. Но начинать с WinForms переходить на WPF сейчас, когда саму WPF уже почти 10 лет стукнуло, это странно.

        Что касается Windows Store — да, XAML тот же. Но это просто язык разметки, все равно что сказать, что WinForms и WPF — это тот же самый C#. Да, паттерны они туда перетащили, но начинка разная и когда пишешь серьезные приложения, то в начинку надо погружаться всегда.


        1. Athari
          15.09.2015 05:47

          Но начинать с WinForms переходить на WPF сейчас, когда саму WPF уже почти 10 лет стукнуло, это странно.

          А какие варианты? Оставаться на WinForms? Переходить на UWP? Забить на C# и уходить на Qt5? Собственно, сейчас WPF — самый актуальный гуёвый фреймворк на дотнете для десктопа. Вариантов нет.

          Это не столько WPF умирает, сколько мелкомягкие забили на десктоп в целом.


          1. withkittens
            15.09.2015 11:24
            +2

            Не забили, просто стали мыслить в другом направлении.
            1. Они выпустили W10, в которой приложения Windows Store и UAP способны запускаться в привычных для каждого окошках.
            2. Они назвали W10 последней версией и всячески пытаются переманить всех на неё (особенно народ с W7).
            Косвенно это означает, что популярность и востребованность UAP-приложений со временем будет расти.
            Вот, пожалуйста, получаем новый десктоп. (А ещё мобильники и планшеты, почти бесплатно.)

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


            1. Athari
              15.09.2015 15:05

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

              Скажем так, я буду всерьёз воспринимать «универсальные» приложения, когда Adobe портирует на эту платформу Photoshop. И не ту игрушку, напоминающую инстаграм по функциональности, а полноценный взрослый фотошоп.

              Энтерпрайзу эти универсальные приложения тоже нафиг не нужны. Если приложение должно работать и на планшетах, и на мобилках, и на десктопах, и на прочих устройствах (скажем, гуй для заказа в ресторане, которым могут пользоваться официанты и клиенты) — да, универсальные приложения выглядят как удобная платформа. Но вот гуй для менеджеров со стандартным нагромождением гридов портировать под урезанные приложения для тача, где ни черта не работает — кому оно надо?


              1. withkittens
                15.09.2015 16:43
                +2

                1.

                Заголовок спойлера
                image


  1. xtraroman
    14.09.2015 13:20
    -1

    И WinForms и WPF хорошо прекрасно подходят для таких проектов как этот. Разница лишь в том, что если в WinForms проекте что-то не работает, при падении получите честный колстек проблемы. Если есть проблемы в WPF проекте, придется сидеть и думать, потому что проблема может быть в разметке, в асинхронном вызове — их очень много там. А еще упомянутые выше MVVM фреймворки делают инъекции в код, что тоже усложняет диагностику. Вобщем, если нравится медитировать над кодом со словами «Что же я сделал не так» — ваш выбор WPF.


    1. Athari
      15.09.2015 05:43
      +1

      Не надо медитировать. Биндинги трейсятся, визуальные деревья со всеми свойствами просматриваются, инъекции логируются и так далее. Да, это несколько отличается от последовательности «поймал исключение — посмотрел в стек», но инструментов для отладки достаточно.


      1. xtraroman
        15.09.2015 11:04
        -1

        Не буду ходить далеко. Смотрим сорцы этого проекта. Все на OnRender рисуется — в визуальном дереве ничего не увидите :). Кроме биндингов в разметке может быть много проблем всяких, разобраться в которых можно только прочитав немаленький объем исходного кода WPF — медитировать. Вот несколько примеров: определить почему отвалилась виртуализация, определить откуда прилетел Measure.


        1. Athari
          15.09.2015 16:01
          +1

          Ручной рендеринг используется в исключительных случаях, когда очень критична производительность. Часто это пишется не вручную, а берётся из чужих библиотек. Скажем, всякие графики так оптимизируют, но об этом голова разработчика приложения не болит.

          Проблемы с виртуализацией и лишними вычислениями — это оптимизация, а нужда в ней возникает не так уж часто (хотя от приложения зависит, да). Можно считать это платой за мощь.

          Скажем, если приложение состоит из нескончаемых гридов, то разницы между WinForms и WPF примерно ноль. Если же в приложении дикое разнообразие современных интерфейсов, то в случае WinForms в коде будет совершенно нечитаемое месиво, а в WPF можно будет хоть что-то понять, и кода будет меньше.

          Взять этот же метод, рисующий всё вручную. В WinForms из такой нечитаемой и невизуализируемой каши будет состоять весь код сложных контролов. Да, если глюк где-то среди этой каши, то у вас будет идеальный стек и никакой магии, но насколько будет приятно эту кашу разбирать?


          1. xtraroman
            15.09.2015 19:15

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

            А вы пытаетесь доказать здесь что разработчику приложения никогда не придется заглядывать «под капот». Это не так.