• Введение
  • Стратегии проектирования высокопроизводительных пользовательских интерфейсов
  • Использование встроенных средств повышения производительности
  • Выполняйте тесты с использованием реальных объемов данных, которые будут отображаться в вашем приложении 
  • Отсроченный выбор — это благо! Откладывайте, откладывайте, откладывайте… 
  • Будьте внимательны, когда работаете с кодом, управляемым событиями
  • Не допускайте, чтобы пользователю оставалось лишь догадываться о ходе выполнения приложения
  • Выбор подходящих форматов и размеров растровых изображений
  • Размеры изображения имеют существенное значение
  • Так много файловых форматов и так мало времени…
  • Как поступать в тех случаях, когда источником изображения с высоким разрешением является само мобильное устройство
  • Стратегии проектирования высокопроизводительного графического кода
  • Способы интеграции графики с кодом пользовательского интерфейса
  • Где рисовать — на экране или вне экрана?
  • Определите собственный процесс визуализации
  • Отсрочка — зло, используйте предварительные вычисления
  • Кэшируйте часто используемые ресурсы
  • Старайтесь избегать распределения памяти для объектов при выполнении повторяющихся или непрерывно продолжающихся операций рисования
  • Резюме 
  • ГЛАВА 11

    Производительность графического кода и пользовательского интерфейса

    М-р Брэддок: "Скажи-ка мне, ради чего тогда надо было так тяжко трудиться последние четыре года?"

    Бенджамин Брэддок: "Ты меня достал…"

    ("Выпускник" (The Graduate) (1967))

    Элен Робинсон: "Я не хочу, чтобы ты брался за что- либо без четко составленного плана".

    ("Выпускник" (1967))

    Введение

    Несомненно, "Выпускник" — один из лучших фильмов всех времен и народов, но какое это имеет отношение к разработке пользовательских интерфейсов мобильных приложений?

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

    Наиболее бросающимся в глаза аспектом производительности приложения является пользовательский интерфейс. То, насколько быстро приложение реагирует на команды пользователя, и с какой скоростью выполняются задачи, требующие перерисовки экрана, оказывает огромнейшее влияние на пользовательское восприятие приложения. Замедленное выполнение, мерцание пользовательского интерфейса и любые остановки при выполнении тех операций, которые должны были бы осуществляться без какой-либо задержки, формируют у пользователя отрицательное отношение к вашему приложению. Поскольку мобильными устройствами люди часто пользуются, держа их в руках, и поэтому относятся к ним, как к простым механическим инструментам, от них ожидается еще большая способность к отклику, чем от настольных компьютеров; ни с чем похожим на мерцание циферблата наручных часов, периодическое исчезновение из поля зрения карты города или страниц записной книжки в обычных условиях сталкиваться не приходится. Многие разработчики беззаботно погружают приложение в неэффективный интерфейс, в результате чего создается впечатление, что само по себе неплохое приложение работает очень медленно. Добиться удовлетворительной производительности пользовательского интерфейса на мобильных устройствах вполне возможно, но это требует большого внимания к деталям. Что необходимо — так это стратегия, которая могла бы гарантировать, что будут решаться именно те проблемы, от которых зависит быстродействие пользовательского интерфейса и комфортность работы с приложением.

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

    Как и в случае управления памятью и других аспектов производительности, технические принципы эффективного проектирования мобильного программного обеспечения не являются уникальными для мобильных устройств. Опыт, приобретенный вами при проектировании и написании эффективно функционирующих программных кодов пользовательских интерфейсов мобильных приложений, позволит вам создавать и более эффективные приложения для настольных компьютеров. Рабочие среды современных настольных компьютеров предоставляют настолько широкие возможности, что многие разработчики вообще не задумываются о производительности, когда проектируют пользовательские интерфейсы. В результате иногда оказывается, что интерфейс работает чересчур медленно, хотя этого можно было бы избежать. Разница между обоими подходами станет очевидной, если вы сравните между собой два пользовательских интерфейса, один из которых проектировался с выделением в качестве первостепенной задачи достижения высокой производительности, а при проектировании второго эта задача отодвигалась на второй или даже на третий план. Первое приложение продемонстрирует действительно высокую скорость выполнения, которое не будет прерываться неожиданными паузами или сопровождаться миганием экрана при его обновлении; такое приложение воспринимается как безукоризненно работающий часовой механизм. Второе же приложение будет казаться "тяжеловатым"; вполне вероятно, что при работе с ним будет наблюдаться мигание экрана при обновлении таких элементов управления, как окна списков, а временами оно будет приостанавливаться. Такое приложение будет напоминать автомобиль, все системы которого требуют регулировки. Даже при абсолютном совпадении базовой функциональности обоих приложений способность пользовательского интерфейса к отклику и видимые проявления не прерываемого никакими паузами процесса выполнения окажут огромное влияние на восприятие качества функционирования приложения. Это справедливо для настольных компьютеров, но вдвойне справедливо для мобильных устройств. Сравните два мобильных телефона, один из которых отличается безукоризненной работой, а второй работает слегка замедленно, и вы сразу же почувствуете разницу. Независимо от возможностей и качества базового программного обеспечения каждого из этих телефонов, более надежным и более эффективным с функциональной точки зрения будет восприниматься тот, который "живее" откликается на ваши действия.

    Можно выделить следующие аспекты вашей работы, от которых зависит эффективное функционирование создаваемых вами пользовательских интерфейсов: 

    ■ При написании кода пользовательского интерфейса применяйте стратегии, ориентированные на достижение высокой производительности. В большинстве современных сред программирования предлагаются высокоуровневые абстракции, облегчающие создание пользовательских интерфейсов. Организацию работы с окнами и диалогами вместе с относящимися к ним элементами управления берет на себя каркас приложения. Разработчик пишет код, выполняющийся в ответ на события, которые генерируются в результате взаимодействия пользователя с элементами управления пользовательского интерфейса. По причине применения высокоуровневых абстракций легко забыть о том, что с любой программной операцией, затребованной пользовательским интерфейсом, могут быть связаны ощутимые накладные расходы. В этих условиях часто имеется несколько возможных путей решения, различающихся своей эффективностью, и от разработчика зависит, какой вариант решения он выберет — удачный или неудачный. Оптимизация такого высокоуровневого кода имеет огромное значение для повышения быстродействия результирующего пользовательского интерфейса. 

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

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

    Стратегии проектирования высокопроизводительных пользовательских интерфейсов

    Сложность написания кода пользовательского интерфейса объясняется тем, что он представляет собой пограничную область, в которой ваш код взаимодействует как с операционной системой, так и с конечным пользователем. Пользовательский интерфейс находится в буквальном смысле слова между ними и играет роль посредника при обмене запросами, связывающими между собой пользователя и операционную систему. Как проектировщик приложения, вы должны ответить на тысячи вопросов:

    ■ Какие стандартные или пользовательские элементы управления следует применять для отображения данных и организации взаимодействия с пользователем?

    ■ Имеет ли смысл создать собственные нестандартные элементы управления, или же лучше расширить свойства уже существующих элементов управления с тем, чтобы они соответствовали вашим конкретным нуждам?

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

    ■ На какие управляющие события необходимо реагировать? В каких случаях реакция на эти события должна подавляться?

    ■ Можно ли уменьшить количество событий, запускаемых при выполнении обычных или часто повторяющихся задач?

    ■ Существует ли необходимость в форсировании немедленного обновления или перерисовки элементов управления ради поддержания в пользователе уверенности в том, что его команды воспринимаются и исполняются, если при этом страдает общая производительность приложения?

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

    Ниже приведены некоторые из основных стратегий проектирования, ориентированных на создание высокопроизводительных пользовательских интерфейсов.

    Использование встроенных средств повышения производительности

    Если среда выполнения мобильного приложения, которую вы используете, предлагает для создания высокопроизводительных кодов пользовательских интерфейсов специальные возможности, вы обязательно должны ими воспользоваться. Более того, вы должны очень внимательно изучить перечень событий, свойств и методов, относящихся к используемым вами элементам управления, и попытаться найти среди них те, которые особенно подходят для повышения производительности интерфейса. Лучшим способом узнать как можно больше об этих возможностях каркаса пользовательского интерфейса является исследование членов его классов окон, форм и элементов управления. Кроме того, полезно внимательно изучить перегруженные версии методов, предназначенных для решения наиболее часто возникающих задач программирования, и выяснить, не найдутся ли методы, позволяющие выполнять операции в пакетном режиме вместо того, чтобы повторно вызывать один и тот же метод в цикле. Как несложно догадаться, выполнение операций в пакетном режиме обычно происходит гораздо быстрее. Для такого исследования вам, как правило, потребуется каких- нибудь несколько минут, но вы сами удивитесь, как много нового для себя вы сможетe обнаружить; очень часто плохое функционирование кода пользовательского интерфейса объясняется просто тем, что для достижения тех или иных целей используются далеко не самые эффективные свойства или методы, или тем, что вызываются методы, приостанавливающие операции с пользовательским интерфейсом на время выполнения его обновлений.

    Использование элементов управления TreeView и ListView среды .NET Compact Framework для повышения производительности приложений

    Элементы управления TreeView и ListView используются для отображения наборов взаимосвязанных данных в пользовательских интерфейсах. Поскольку эти элементы управления работают с наборами данных, часто возникает необходимость в добавлении к ним или удалении из них сразу целых групп элементов данных. Для обоих элементов управления предусмотрены высокопроизводительные методы, позволяющие эффективно решать подобные задачи. 

    • .BeginUpdate()/EndUpdate(). Оба эти метода присутствуют как в TreeView, так и в ListView, и предназначены для приостановки и возобновления автоматической перерисовки элемента управления на экране. Вызов метода BeginUpdate() указывает на то, что элемент управления не должен автоматически перерисовываться всякий раз, когда в него добавляются или из него удаляются элементы данных, тогда как вызов метода EndUpdate() восстанавливает режим автоматической перерисовки элемента управления. Выполнение необязательных операций перерисовки экрана может отрицательно сказываться на производительности приложения. Если в процессе работы вашего приложения возникает необходимость в помещении в элемент управления или исключении из него многочисленных данных, то соответствующий участок кода целесообразно окружить парой вызовов BeginUpdate() и EndUpdate(). 

    • .AddRange(). Для коллекции узлов элемента управления TreeView предусмотрен метод AddRange() (например, treeView1.Nodes.AddRange()), обеспечивающий групповое добавление узлов в TreeView. Такой "пакетный" режим обработки является гораздо более предпочтительным по сравнению с простым итеративным добавлением каждого узла по отдельности.

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

    Пример: различия в производительности, обусловленные использованием различных подходов при работе с элементами управления TreeView

    Приведенный в листинге 11.1 пример предназначен для количественной оценки эффективности трех различных методик работы с элементами управления TreeView среды .NET Compact Framework. Используя Visual Studio .NET, создайте новый проект C# для мобильного приложения, выбрав в качестве целевой платформы устройство Pocket PC. Убедившись в том, что вы находитесь в режиме конструктора, добавьте в пустую форму элемент управления TreeView и пять кнопок, как показано на рис. 11.1.

    Visual Studio .NET автоматически создаст и свяжет с кнопкой пустой обработчик событий

    Все, что вы должны для этого сделать — это дважды щелкнуть на кнопке формы. Имя добавленной функции будет состоять из имени элемента управления (например, button1) и суффикса _Click. Visual Studio выполнит следующее: 1) создаст для вас функцию обработчика событий, 2) запишет код в функцию InitializeComponent() формы, предназначенную для подключения только что созданного обработчика события щелчка, и 3) откроет окно редактора кода, чтобы вы могли ввести в нем код для обработчика события. При желании вы можете назвать кнопку по-другому, изменив свойство Name в окне Properties (окно справа на рис. 11.1). Целесообразно сделать это до двойного щелчка на кнопке с целью создания и подключения обработчика события, поскольку функция обработчика создается с использованием текущего имени элемента управления. Если имя элемента управления будет изменено уже после создания этой функции, обработчик по-прежнему останется связанным с ним должным образом, но его имя не будет согласовываться с новым именем элемента управления. Обеспечить совпадение имен в этом случае вам придется вручную; сделать это не составляет особого труда, но для этого вам придется выполнить лишнюю работу.

    Представленный в листинге 11.1 код состоит из набора обработчиков событий для различных кнопок, которые имеются на вашей форме. Фактические имена используемых функций будут происходить от имен, присвоенных соответствующим кнопкам. В своем коде я использовал для кнопок следующие имена: UnOptimizedFill, UnOptimizedClear, UseBeginEndUpdateForFill, UseBeginEndUpdateForClear и FillArrayBeforeAttachingToTree. Если вы используете заданные по умолчанию имена, которые предложит вам Visual Studio .NET, то у вас будут кнопки с именами button1, button2, button3, button4 и button5 и функции обработчиков событий с другими именами, которые надо будет соответственно изменить.

    Рис. 11.1. Конструктор форм среды Visual Studio .NET с размещенными на форме элементами управления TreeView и Button


    В любом случае, проще всего сначала создать и связать с кнопками пустые обработчики событий, выполняя для этого двойные щелчки на каждой из кнопок в конструкторе форм Visual Studio .NET, а затем вставить приведенный в листинге код обработчиков в созданные для вас определения функций.

    Листинг 11.1. Заполнение данными и очистка от них элементов управления TreeView с использованием альтернативных стратегий

    //---------------------------------------------------------------------

    //Примечание #1: В этом примере используется класс PerformanceSampling,

    // определенный ранее в данной книге. Убедитесь в том, что

    // вы включили этот класс в свой проект.

    //Примечание #2: Этот код необходимо включить в класс Form, содержащий

    // элемент управления TreeView и кнопки Button, к которым

    // подключены приведенные ниже функции xxx_Click.

    //---------------------------------------------------------------------


    //Количество элементов, которые необходимо поместить в элемент

    //управления TreeView

    const int NUMBER_ITEMS = 800;


    //-------------------------------------------------------------------------

    //Код для кнопки "Fill: Baseline"

    //

    //Использование неоптимизированного подхода для заполнения данными элемента

    //управления TreeView

    //-------------------------------------------------------------------------

    private void UnOptimizedFill_Click(object sender, System.EventArgs e) {

     //Очистить массив для создания одинаковых условий тестирования

     if (treeView1.Nodes.Count > 0) {

      treeView1.BeginUpdate();

      treeView1.Nodes.Clear();

      treeView1.EndUpdate();

      treeView1.Update();

     }


     //Для повышения корректности тестирования предварительно выполнить

     //операцию сборки мусора

     System.GC.Collect();


     //Запустить таймер

     PerformanceSampling.StartSample(0, "TreeViewPopulate");


     //Заполнить данными элемент управления TreeView

     for (int i = 0; i < NUMBER_ITEMS; i++) {

      treeView1.Nodes.Add("TreeItem" + i.ToString());

     }


     //Остановить таймер и отобразить результат

     PerformanceSampling.StopSample(0);

     System.Windows.Forms.MessageBox.Show(PerformanceSampling.GetSampleDurationText(0));

    }


    //-------------------------------------------------------------------------

    //Код для кнопки "Clear: Baseline"

    //

    //Использование неоптимизированного подхода для заполнения данными элемента

    //управления TreeView

    //-------------------------------------------------------------------------

    private void UnOptimizedClear_Click(object sender, System.EventArgs e) {

     //Для повышения корректности тестирования предварительно выполнить

     //операцию сборки мусора

     System.GC.Collect();


     //Запустить таймер

     PerformanceSampling.StartSample(1, "TreeViewClear");

     treeView1.Nodes.Clear();

     PerformanceSampling.StopSample(1);

     System.Windows.Forms.MessageBox.Show(PerformanceSampling.GetSampleDurationText(1));

    }


    //--------------------------------------------------

    //Код для кнопки "Fill: BeginUpdate"

    //

    //Подход, в котором используется метод BeginUpdate()

    //--------------------------------------------------

    private void UseBeginEndUpdateForFill_Click(object sender, System.EventArgs e) {

     //Очистить массив для создания одинаковых условий тестирования

     if (treeViewl.Nodes.Count > 0) {

      treeView1.BeginUpdate();

      treeView1.Nodes.Clear();

      treeView1.EndUpdate();

      treeView1.Update();

     }


     //Для повышения корректности тестирования предварительно выполнить

     //операцию сборки мусора

     System.GC.Collect();


     //Запустить таймер

     PerformanceSampling.StartSample(2, "Populate - Use BeginUpdate");


     //Заполнить данными элемент управления

     TreeView treeView1.BeginUpdate();

     for (int i = 0; i < NUMBER_ITEMS; i++) {

      treeView1.Nodes.Add("TreeItem" + i.ToString());

     }

     treeView1.EndUpdate();


     //Остановить таймер и отобразить результат

     PerformanceSampling.StopSample(2);

     System.Windows.Forms.MessageBox.Show(PerformanceSampling.GetSampleDurationText(2));

    }


    //--------------------------------------------------

    //Код для кнопки "Clear: BeginUpdate"

    //Подход, в котором используется метод BeginUpdate()

    //--------------------------------------------------

    private void UseBeginEndUpdateForClear_Click(object sender, System.EventArgs e) {

     //Для повышения корректности тестирования предварительно выполнить

     //операцию сборки мусора

     System.GC.Collect();


     //Запустить таймер

     PerformanceSampling.StartSample(3, "Clear - Use BeginUpdate");

     treeView1.BeginUpdate();

     treeView1.Nodes.Clear();

     treeView1.EndUpdate();


     //Остановить таймер и отобразить результат

     PerformanceSampling.StopSample(3);

     System.Windows.Forms.MessageBox.Show(PerformanceSampling.GetSampleDurationText(3));

    }


    //-------------------------------------

    //Код для кнопки "Fill: Use Array"

    //

    //Подход, в котором используется массив

    //-------------------------------------

    private void FillArrayBeforeAttachingToTree_Click(object sender, System.EventArgs e) {

     //Очистить массив для создания одинаковых условий тестирования

     if (treeView1.Nodes.Count > 0) {

      treeView1.BeginUpdate();

      treeView1.Nodes.Clear();

      treeView1.EndUpdate();

      treeView1.Update();

     }


     //Для повышения корректности тестирования предварительно выполнить

     //операцию сборки мусора

     System.GC.Collect();


     //Запустить таймер

     PerformanceSampling.StartSample(4, "Populate - Use Array");


     //Распределить память для нашего массива узлов дерева

     System.Windows.Forms.TreeNode [] newTreeNodes = new System.Windows.Forms.TreeNode[NUMBER_ITEMS];


     //Заполнить массив

     for(int i = 0; i < NUMBER_ITEMS; i++) {

      newTreeNodes[i] = newSystem.Windows.Forms.TreeNode("TreeItem" + i.ToString());

     }


     //Связать массив с элементом управления

     TreeView treeView1.BeginUpdate();

     treeView1.Nodes.AddRange(newTreeNodes);

     treeView1.EndUpdate();


     //Остановить таймер и отобразить результат

     PerformanceSampling.StopSample(4);

     System.Windows.Forms.MessageBox.Show(PerformanceSampling.GetSampleDurationText(4));

    }

    Результаты, полученные с использованием различных методик добавления данных в элемент управления TreeView и их исключения из него, приведены в таблицах 11.1 и 11.2.


    Таблица 11.1. Физическое устройство Pocket PC: добавление 800 элементов данных (время в секундах)

    Номер теста Не оптимизированный подход Использование методов BeginUpdate()/EndUpdate() Использование массива
    1 40,785 12,484 10,388
    2 40,533 12,322 10,419
    3 40,878 13,343 11,686
    Среднее 40,732 12,716 10,831
    Экономия времени по сравнению с базовым значением Базовое значение (0%) 68,78% 73,41%

    Из табл. 11.1 видно, что экономия времени, достигаемая за счет окружения кода, предназначенного для добавления в элемент управления TreeView и исключения из него данных, вызовами методов BeginUpdate() и EndUpdate(), составила примерно две трети (68,78%). Наряду с этим, благодаря тому, что мерцание элементов управления вследствие их обновления происходит реже, повышается и привлекательность интерфейса, оцениваемая с позиций конечного пользователя. Использование метода AddRange() (столбец "Использование массива") для заполнения данными элемента управления TreeView позволило уменьшить накладные расходы еще на 5%; это улучшение также является ощутимым.

    Было довольно-таки неожиданно обнаружить, что использование пары вызовов BeginUpdate() и EndUpdate() привело не только к значительному увеличению скорости добавления данных в элемент управления TreeView, но и оказало достаточно сильное влияние на скорость их удаления. Результаты, полученные с использованием двух различных подходов для удаления данных из элемента управления TreeView, сравниваются в табл. 11.2.

    Таблица 11.2. Очистка 800 элементов данных (время в секундах)

    Номер теста Не оптимизированный подход Использование методов BeginUpdate()/EndUpdate()
    1 18,791 8,656
    2 15,910 8,964
    3 16,821 8,516
    Среднее 17,174 8,712
    Экономия времени по сравнению с базовым значением Базовое значение (0 %) 49,27 %

    Как видно из табл. 11.2, одного лишь окружения кода, предназначенного для удаления данных из элемента управления TreeView, вызовами методов BeginUpdate() и EndUpdate() оказалось достаточным для того, чтобы достигнуть 50%-ной экономии времени.

    На основании полученных результатов можно сделать следующие выводы:

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

    2. Было бы неправильно предполагать, будто такие концептуально простые операции, как, например, очистка элемента управления от данных, будут сами по себе выполняться быстро и поэтому не следует прилагать никаких усилий к их ускорению.

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

    Выполняйте тесты с использованием реальных объемов данных, которые будут отображаться в вашем приложении 

    Обычная ошибка, которую допускают при написании кодов пользовательских интерфейсов. заключается в том, что в процессе проектирования и тестирования интерфейса используются данные меньшего объема, чем тот, с которым приходится сталкиваться при развертывании мобильного приложения. Алгоритм, который прекрасно справляется с извлечением из базы данных 20 элементов данных и их передачей пользовательскому интерфейсу, вовсе не обязательно сделает хорошо то же самое для 200 элементов данных. Если вы тестируете интерфейс, используя лишь небольшие наборы данных, то тем самым оставляете открытыми двери для множества неприятных сюрпризов, ожидающих вас на последующих стадиях разработки или при развертывании приложения, когда настанет черед работать с реальными объемами данных. Гораздо лучше выявлять все проблемы производительности еще на ранней стадии, когда для их преодоления могут быть найдены наиболее конструктивные решения; вносить изменения в проект впоследствии всегда труднее, это может нарушать стабильность работы приложения и почти всегда приводит к менее удовлетворительным результатам.

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

    В любом случае очень важно точно представлять себе емкость и местоположение источников данных, которые ваше приложение должно будет отображать в пользовательском интерфейсе, а также механизмы доступа к этим источникам. Определите эти характеристики источников данных уже на ранних стадиях процесса проектирования приложения и выполняйте тестирование приложения с использованием источников данных, удовлетворяющих этим требованиям. Если приложение предназначено для работы с данными различного объема, тестируйте его на максимальном объеме данных. Настоятельно рекомендуется организовать буфер данных и тестировать приложение при объеме данных, который в разумной степени превышает расчетный, чтобы проверить, как это повлияет на производительность. Если данные могут поступать из различных источников, тестируйте производительность с использованием каждого из возможных механизмов доступа к данным. Если для доступа к данным будет использоваться коммутируемая линия или линия с низкой пропускной способностью, тестируйте приложение в таких же условиях. Важно не только то, чтобы приложение в подобных ситуациях работало корректно, но и то, чтобы оно имело привлекательный внешний вид и не теряло своей интерактивности на время длительных периодов обновления пользовательского интерфейса. При соблюдении всех этих рекомендаций весь ваш процесс проектирования будет нацелен на достижение производительности, обеспечивающей прекрасные условия для работы в реальных условиях. Для этого не существует лучшего способа, чем работа с теми же данными и с использованием тех же методов доступа к ним, что и те, с которыми будут сталкиваться в реальной работе конечные пользователи.

    Отсроченный выбор — это благо! Откладывайте, откладывайте, откладывайте… 

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

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

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

    ■ Способ 1. Начните с просмотра данных, сгруппированных по районам (Neighbourhoods), а затем перейдите к просмотру данных, сгруппированных по ценам, чтобы определить, информация о домах какой стоимости должна быть отображена. Этому способу навигации соответствуют следующие переходы по иерархическим узлам TreeView: Neighbourhoods→Price→ListOfUnits. 

    ■ Способ 2. Начните с группировки по ценам (Price), а затем перейдите к просмотру данных, сгруппированных по типам, и, наконец, по районам, в результате чего будет отображен соответствующий список домов. Этому способу навигации соответствуют следующие переходы по иерархическим узлам TreeView: Price→HouseType→Neighbourhood→ListOfUnits.

    ■ Способ 3. Начните с типов домов (HouseType), а затем последовательно перейдите к просмотру данных, сгруппированных по интервалам цен и районам. Этому способу навигации соответствуют следующие переходы по иерархическим узлам TreeView: HouseType→Price→Neighbourhood→ListOfUnits.

    На рис. 11.2 показано, как может выглядеть навигация по узлам иерархии TreeView. Имеется три узла верхнего уровня: Neighbourhood, Price и House- Type У каждого родительского узла имеются подузлы, по которым пользователи могут переходить вглубь иерархии, получая, в конечном счете, множество объектов недвижимости, информацию о которых они хотят просмотреть.

    Несмотря на всю полезность метафоры дерева для отбора домов на основании иерархических категорий, при ее использовании могут возникать проблемы, обусловленные заметным ростом количества узлов дерева даже при средних объемах данных. Простая иерархия, включающая 10 узлов типа Neighbourhood, каждый из которых включает по 4 узла типа HouseType, включающие каждый по 4 узла типа Price, каждый из которых, в свою очередь, включает по 6 узлов типа House. в сумме дает для нашего дерева 960 концевых узлов, или листьев.

    Это число следовало бы дополнительно умножить на количество способов навигации, которые мы хотим использовать:

    960 для навигации по схеме Neighbourhoods→HouseType→Price→ListOfUnits

    960 для навигации по схеме Price→Neighbourhoods→HouseType→ListOfUnits

    960 для навигации по схеме HouseTypeOPrice→Neighbourhood→ListOfUnits

    Рис. 11.2. Простой пример того, как может выглядеть иерархия узлов элемента управления TreeView для задачи поиска объектов недвижимости


    Если мы решим заранее заполнить данными элемент управления TreeView, не дожидаясь, какую навигационную схему изберет пользователь, то нам, в конечном счете, придется задействовать огромное количество узлов дерева, большинство из которых не будут посещены пользователем во время сеансов работы с приложением. Создание таких членов TreeView занимает много времени, а каждый из отдельных узлов TreeView сам по себе потребляет системные ресурсы. Несомненно, хранение тысяч элементарных узлов иерархии TreeView приведет к ухудшению производительности приложения. Если исходить из того, что не все из описанных отдельных узлов будут посещены пользователем, то можно значительно повысить производительность, используя более разумный подход. Чего нам хотелось бы — так это сохранить метафору пользовательского интерфейса TreeView, но избежать создания сотен или тысяч узлов, которые так никогда и не будут посещены. Способ, позволяющий это реализовать, состоит в том, чтобы создавать нужные узлы TreeView лишь тогда, когда становится ясным, что пользователь собирается их посетить. Этого можно добиться, используя некий "умный" код, который 1) заполняет элемент управления TreeView информацией лишь до той точки, до которой он перед этим был развернут, и 2) обрабатывает событие, указывающее на ожидаемое развертывание определенного узла элемента управления TreeView, который, таким образом, должен быть заполнен достоверными данными. Это позволяет сэкономить время и ресурсы, которые иначе пришлось бы расходовать мобильному приложению для заполнения информацией огромного количества узлов, и, в конечном счете, делает приложение более привлекательным с точки зрения конечного пользователя.

    Возможны и более сложные подходы, например, заблаговременное продвижение на одну ступень вглубь иерархического дерева TreeView и предварительное заполнение узлов информацией; благодаря этому условия работы пользователя с приложением еще более улучшаются. Многообразие возможностей подхода, который вы решите применить для того, чтобы, используя разумные оценки, отложить выполнение части операций на более позднее время, ограничивается лишь объемом работы по проектированию и реализации ваших идей.

    .NET Compact Framework: не все поддерживаемые обработчики прерываний предоставляются конструктором форм Visual Studio .NET

    Платформа .NET Compact Framework поддерживает подмножество управляющих событий платформы .NET Compact Framework, ориентированной на настольные компьютеры. Следует отметить, что одно лишь наличие в .NET Compact Framework сигнатуры какого-либо события еще не является гарантией того, что это событие может быть возбуждено во время выполнения. Могут существовать специфические условия совместимости или причины, связанные со свойствами объектного наследования, в силу которых определение события необходимо было связать с элементом управления, но само событие запускаться во время выполнения в .NET Compact Framework не будет. Единственный способ прояснения подобных ситуаций состоит в том, чтобы подключить событие к элементу управления и посмотреть, будет ли оно запущено во время выполнения. Эта ситуация дополнительно осложняется тем фактом, что не все события, поддерживаемые платформой .NET Compact Framework, отражаются в графической среде проектирования Visual Studio .NET. Среди поддерживаемых событий имеются и такие, которые не фигурируют в списке событий C# в панели свойств или в выпадающем списке событий Visual Basic .NET в редакторе кода.

    События, отображаемые в среде проектирования, должны поддерживаться платформой .NET Compact Framework, но имеется ряд дополнительных событий, которые .NET Compact Framework поддерживает, но не отображает. Причиной такого поведения явилась недостаточная координация работ над этим проектом: группа, работающая над средствами проектирования, и группа, работающая над средой выполнения, не смогли идеально организовать взаимодействие между собой! (Надеюсь, что в процессе дальнейшего усовершенствования платформы подобная ситуация уже не повторится.) Большинство обычных событий отображаются в окне конструктора форм, но некоторые специфические события могут требовать подключения их к форме вручную. Сделать это несложно, но для этого надо хотя бы немного знать о том, как осуществляется подключение обработчиков событий.

    Если вы хотите использовать событие, которое поддерживается в .NET Compact Framework, но не предлагается в качестве доступного в среде проектирования Visual Studio .NET, вам придется вручную вставить одну строку кода в функцию InitializeComponent() формы, содержащей данный элемент управления. Функцию InitializeComponent() вы найдете в обычно скрытом или свернутом разделе "Windows Form Designer-Generated Code" редактора кода класса.

    Добавление обработчика событий BeforeExpand в элемент управления TreeView иллюстрирует приведенный ниже фрагмент кода:

    #region Windows Form Designer generated code

    private void InitializeComponent()

    … строки кода для других элементов управления …

    //

    // treeView1

    //

    this.treeView1.ImageIndex = -1;

    this.treeView1.Location = new System.Drawing.Point(72, 48);

    this.treeView1.Name = "treeView1";

    this.treeView1.SelectedImageIndex = -1;

    this.treeView1.Size = new System.Drawing.Size(168, 176);

    this.treeView1.TabIndex = 0;

    //ОДНОСТРОЧНЫЙ КОД ДЛЯ ПОДКЛЮЧЕНИЯ СОБЫТИЯ, КОТОРЫЙ ВЫ ДОЛЖНЫ ВСТАВИТЬ

    this.treeView1.BeforeExpand += new.System.Windows.Forms.TreeViewCancelEventHandler(this.TreeView1BeforeExpand);

    … строки кода для других элементов управления …

    Представленный выше код подключает обработчик события BeforeExpand элемента управления treeView1. Функция обработчика события должна иметь специальную сигнатуру. В данном случае она имеет следующий вид:

    private void TreeView1BeforeExpand(object sender, System.Windows.Forms.TreeViewCancelEventArgs e) {}

    Неплохим способом обеспечения автоматической генерации обоих вышеприведенных фрагментов кода является использование проекта Windows Application для настольных компьютеров. В Visual Studio .NET проекты для настольных компьютеров поддерживают графический способ создания и подключения всех поддерживаемых обработчиков прерываний. Сгенерированный при этом код вы далее можете скопировать и вставить в соответствующие части своего проекта .NET Compact Framework.

    Заслуживает рассмотрения и слегка видоизмененный вариант описанной выше стратегии. Поскольку конструктор форм автоматически вставляет и удаляет код внутри функции InitializeComponent(), существует вероятность того, что он добавит свой код поверх добавленного вами кода. Чтобы этого избежать, может оказаться целесообразным создать собственную функцию (например, MyInitializeComponent()), чтобы вставить в нее свой код, выполняющий все действия по инициализации, и вызвать эту функцию в коде конструктора формы сразу же после вызова InitializeComponent(). Благодаря этому вы сможете быть уверены в том, что ваш код не будет случайно удален конструктором форм.

    Пример: заполнение элемента управления TreeView данными по требованию

    На рис. 11.3 представлен пример простого приложения с элементами управления TreeView (treeview1) и Button (button1). Щелчок на кнопке во время выполнения приложения устанавливает или сбрасывает состояние элемента управления TreeView. После установки состояния элемента управления TreeView щелчком на кнопке он предоставляет три узла верхнего уровня, которые можно динамически заполнять данными. Этими узлами являются узлы Neighbourhoods, Price и HouseType.

    Рис. 11.3. Выполнение приложения, динамически заполняющего данными элемент управления TreeView, на эмуляторе Pocket PC


    Чтобы сократить размер кода и не усложнять пример, в листинг 11.2 включен лишь код, обеспечивающий динамическое заполнение данными узла Neighbourhoods. Щелчки на других узлах будут приводить при первом щелчке к отображению окна сообщений MessageBox, информирующего о том, что для динамического заполнения данного узла данными вы должны добавить собственный код.

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

    1. Начните новый проект Smart Device в Visual Studio .NET и выберите в качестве целевой платформы Pocket PC.

    2. Добавьте в форму Form в окне конструктора элементы управления TreeView и Button.

    3. Дважды щелкните на кнопке Button в окне конструктора форм; в результате этого будет создан и подключен к кнопке приведенный ниже обработчик событий button1_Click.

    4. Введите приведенный ниже код button1_Click, ответственный за заполнение данными элемента управления TreeView.

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

    6. Вручную подключите обработчик событий для события BeforeExpand элемента управления TreeView, о чем говорилось в разделе выше.

    7. Скомпилируйте пример и запустите его на выполнение.

    Листинг 11.2. Динамическое заполнение данными элемента управления TreeView

    //Фиктивный текст для размещения в заполнителях дочерних узлов

    const string dummy_node = "_dummynode";

    //Метка, которую мы будем использовать для обозначения узла

    const string node_needToBePopulated = "_populateMe";


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

    const string nodeText_Neighborhoods = "Neighborhoods";

    const string nodeText_Prices = "Prices";

    const string nodeText_HouseType = "HouseTypes";


    //--------------------------------------------------------------------

    //Обработчик события щелчка для кнопки

    //

    //Настраивает наш элемент управления TreeView для отображения процесса

    //последовательного заполнения дерева

    //--------------------------------------------------------------------


    private void button1_Click(object sender, System.EventArgs e) {

     TreeNode tnNewNode;


     //Отключить обновление интерфейса до тех пор, пока дерево

     //не будет заполнено

     treeView1.BeginUpdate();


     //Избавиться от устаревших данных

     treeView1.Nodes.Clear();


     //--------------------

     //Узел "Neighborhoods"

     //--------------------


     //Добавить узел "Neighborhoods" верхнего уровня.

     tnNewNode = treeView1.Nodes.Add("Neighborhoods");


     //Установить для узла метку, указывающую на то, что узел

     //будет заполняться динамически

     tnNewNode.Tag = node_needToBePopulated;

     //Этот фиктивный дочерний узел существует лишь для того, чтобы

     //узел имел, по крайней мере, один дочерний узел и поэтому

     //был расширяемым.

     tnNewNode.Nodes.Add(dummy_node);


     //------------

     //Узел "Price"

     //------------

     tnNewNode = treeView1.Nodes.Add("Price");


     //Установить для узла метку, указывающую на то, что узел

     //будет заполняться динамически

     tnNewNode.Tag = node_needToBePopulated;


     //Этот фиктивный дочерний узел существует лишь для того, чтобы

     //узел имел, по крайней мере, один дочерний узел и поэтому

     //был расширяемым.

     tnNewNode.Nodes.Add(dummy_node);


     //----------------

     //Узел "HouseType"

     //----------------

     tnNewNode = treeView1.Nodes.Add("HouseType");


     //Установить для узла метку, указывающую на то, что узел

     //будет заполняться динамически

     tnNewNode.Tag = node_needToBePopulated;


     //Этот фиктивный дочерний узел существует лишь для того, чтобы

     //узел имел, по крайней мере, один дочерний узел и поэтому

     //был расширяемым.

     tnNewNode.Nodes.Add(dummy_node);


     //Восстанавливаем обновление интерфейса

     treeView1.EndUpdate();

    }


    //-------------------------------------------------------------------------

    //Обработчик событий BeforeExpand для нашего элемента управления TreeView

    //ПРИМЕЧАНИЕ: Этот обработчик событий необходимо будет

    // вручную подключить к функции InitializeComponent()

    // формы.

    //

    //Вызывается при запросе пользователем расширения узла, у которого имеется,

    //по крайней мере, один дочерний узел. Этот вызов осуществляется до

    //отображения дочерних узлов данного узла и дает нам возможность

    //динамически заполнить данными элемент управления TreeView.

    //-------------------------------------------------------------------------
     

    private void TreeView1BeforeExpand (object sender, System.Windows.Forms.TreeViewCancelEventArgs e) {

     //Получить узел, который будет расширяться

     System.Windows.Forms.TreeNode tnExpanding;

     tnExpanding = e.Node;


     //Если узел не отмечен как "нуждающийся в заполнении данными",

     //то он устраивает нас в том виде, "как он есть".

     if (tnExpanding.Tag !=(object) node_needToBePopulated) {

      return; //Разрешить беспрепятственное продолжение выполнение

     }


     //------------------------------------------------------------

     //Требуется динамическое заполнение дерева данными.

     //Мы знаем, что узел должен быть заполнен данными; определить,

     //что это за узел

     //------------------------------------------------------------

     

    if (tnExpanding.Text == nodeText Neighborhoods) {

      PopulateTreeViewNeighborhoods(tnExpanding);

      return; //добавление элементов закончено!

     } else {

      //Проверить другие возможности для узлов дерева, которые мы должны

      //добавить.

      System.Windows.Forms.MessageBox.Show("НЕ СДЕЛАНО: Добавьте код для динамического заполнения этого узла");

      //Снять отметку с этого узла, чтобы мы не могли вновь выполнить

      //этот код

      tnExpanding.Tag = "";

     }

    }


    //------------------------------------------------------------------

    //Эта функция вызывается для динамического добавления дочерних узлов

    //в узел "Neighborhood"

    //------------------------------------------------------------------

    void PopulateTreeViewNeighborhoods(TreeNode tnAddTo) {

     TreeView tvControl;

     tvControl = tnAddTo.TreeView;

     tvControl.BeginUpdate();


     //Очистить имеющийся фиктивный узел

     tnAddTo.Nodes.Clear();


     //Объявить четыре узла, которые мы хотим сделать дочерними узлами

     //того узла, который был передан.

     TreeNode[] newNeighborhoodNodes;

     newNeighborhoodNodes = new TreeNode[4];

     newNeighborhoodNodes[0] = new TreeNode("Capitol Hill");

     newNeighborhoodNodes[1] = new TreeNode("Chelsea");

     newNeighborhoodNodes[2] = new TreeNode("Downtown");

     newNeighborhoodNodes[3] = new TreeNode("South Bay");


     //Добавить дочерние узлы в элемент управления

     TreeView tnAddTo.Nodes.AddRange(newNeighborhoodNodes);

     tvControl.EndUpdate();

    }

    Приведенный код демонстрирует, что, проявив немного изобретательности и написав совсем небольшой объем дополнительного кода, можно отложить заполнение сложного пользовательского интерфейса данными до тех пор, пока они не понадобятся пользователю. Независимо от того, работаете ли вы в .NET Compact Framework или в любой другой среде выполнения для мобильных устройств, откладывание заполнения данными расширяемых элементов управления пользовательских интерфейсов до того момента, когда возникнет действительная необходимость в них, является чрезвычайно мощным методом.

    Будьте внимательны, когда работаете с кодом, управляемым событиями

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

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

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

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

    1. Начните новый проект Smart Device в Visual Studio .NET, выбрав в качестве целевой платформы Pocket PC.

    2. Добавьте в форму Form элементы управления TextBox, Label, ListBox и Button.

    3. Дважды щелкните на кнопке Button в окне конструктора форм; в результате этого будет создан и подключен к кнопке приведенный ниже обработчик событий button1_Click. Введите соответствующий код из листинга 11.3, который будет реагировать на это событие.

    4. Дважды щелкните на элементе управления TextBox в окне конструктора форм; в результате этого будет создан и подключен к текстовому окну приведенный ниже обработчик событий textBoxl_TextChanged. Введите соответствующий код из листинга 11.3, который будет реагировать на это событие.

    5. Скомпилируйте пример и запустите его на выполнение.

    6. Введите текст в текстовое поле; обратите внимание, что каждое нажатие клавиши приводит к выполнению приведенного ниже кода обработки события textBox1_TextChanged.

    7. Щелкните на кнопке Button; обратите внимание, что это также приводит к выполнению приведенного ниже кода обработки события textBox1_TextChanged.

    НА ЗАМЕТКУ

    При выполнении приложения на платформе .NET Compact Framework 1.1 в случае установки значения свойства Text элемента управления TextBox программным путем событие TextChanged в действительности запускается дважды.

    Выполнение того же кода на настольном компьютере сопровождается только однократным запуском указанного события. Вероятно, поведение последующих версий NET Compact Framework будет изменено таким образом, чтобы оно совпадало с поведением .NET Framework (однократный запуск события). События — коварная вещь. Будьте очень внимательны в своих предположениях относительно того как и когда запускаются события.

    Листинг 11.3. Запуск обработчика событий при изменении содержимого элемента TextBox программным путем 

    int m_eventTriggerCount;


    private void textBox1_TextChanged(object sender, System.EventArgs e) {

     m_eventTriggerCount++;

     //Обновить надпись для отображения количества событий

     label1.Text = "Events: #" + m_eventTriggerCount.ToString();

     //Внести каждое событие в список

     listBox1.Items.Add(m_eventTriggerCount.ToString() + textBox1.Text);

    }


    private void button1_Click(object sender, System.EventArgs e) {

     //Запускает событие TextChanged так же,

     //как если бы текст был введен пользователем

     textBox1.Text = "Hello World";

    }

    Как видно из листинга 11.3, программная установка свойства Text элемента управления TextBox запускает тот же код обработки событий, который запускался бы при вводе текста пользователем. В зависимости от того, какие допущения вами сделаны, результаты могут как совпадать, так и не совпадать с ожидаемыми. Программисты часто пишут коды, предназначенные для заполнения пользовательских интерфейсов данными после их извлечения из внешнего источника. При этом устанавливаются свойства Checked переключателей RadioButton и флажков CheckBox, заполняются значения текстовых полей TextBox, заполняются элементами списки ListBox и ComboBox и так далее. Во многих случаях программисты предполагают, что выполнение всех этих установочных действий не приводит к запуску событий пользовательского интерфейса. Обычно в намерения программиста не входит, чтобы эти события запускались, поскольку пользовательский интерфейс всего лишь подготавливается к тому, чтобы пользователь мог им воспользоваться. Очень часто программисты, которые разрабатывают приложение, хотят, чтобы код обработки событий приложения запускался лишь тогда, когда происходит внешнее событие, например, поступает сигнал таймера, пользователь выполняет щелчок на кнопке или вводит текст в элемент управления и тому подобное.

    Не позволяйте событиям заставать вас врасплох

    Сложность работы с кодами, управляемыми событиями, связана с тем, что невозможно заранее знать, когда именно будет вызван код того или иного обработчика события. Просматривая исходный код приложения, вы можете точно сказать, когда вызываются библиотечные функции или другие части кода приложения, но ничто не укажет вам на то, когда именно среда выполнения запустит событие. По этой причине большинство разработчиков просто делают относительно этого некоторые предположения, которые даже не удосуживаются тщательно проверять. Кроме того, по мере увеличения объема приложения за счет дальнейшего добавления пользовательского кода и соответствующего усложнения его внутренней структуры в нем появляется все больше кода, предназначенного для генерации и обработки событий пользовательского интерфейса; из-за этого также могут возникать весьма тонкие взаимодействия, обнаружить которые простым просмотром исходного текста кода очень трудно. Так, код, реагирующий на выбор элемента в списке ListBox, может запускать код, инициирующий обновление свойств элементов управления CheckBox и TextBox, как показано в приведенном ниже примере:

    textboxUserName.Text = selectedRecord.Username;

    checkboxDeliverPackageToHomeAddress.Checked = selectedRecord.DeliverToHome;

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

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

    1. В процеcce разработки кода снабжайте его средствами контроля (мониторинга) выполнения приложения. Подсчет количества запусков событий относится к операциям с низкими накладными расходами; для этого требуется всего лишь последовательно увеличивать на единицу значение целой переменной. С точки зрения диагностики вам также может пригодиться ведение и периодическое изучение упорядоченного списка возникающих событий. Включение соответствующих средств в код можно организовать с помощью условной компиляции приложения. Целесообразно периодически анализировать поведение приложения на протяжении всего цикла разработки и изучать ту информацию, которую вам предоставляет его версия, в которой предусмотрены средства контроля выполнения приложения. Эти средства совсем не сложны в реализации, но являются чрезвычайно эффективным вспомогательным инструментом разработчика.

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

    3. Время от времени размещайте точки останова в каждом из обработчиков событий приложения и исследуйте цепочки обрабатываемых событий. Для выяснения того, какие виды обработки событий и в каком порядке выполняются, полезно периодически организовывать пошаговое отслеживание выполнения событий, запускаемых в пользовательском интерфейсе. Не исключено, что таким способом вам удастся обнаружить непредусмотренные каскады событий, инициируемых обновлением пользовательского интерфейса кодом приложения; может также оказаться, что выполняется излишняя обработка, требующая больших накладных расходов. Хорошим примером кода, который следует выявлять и исключать из своего приложения, является код обработки событий, инициирующий выполнение многочисленных операций считывания и записи данных с использованием базы данных.

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

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

    Пример приложения Pocket PC, демонстрирующий работу средств контроля запуска событий

    Рис. 11.4. Пример приложения Pocket PC, демонстрирующий работу средств контроля запуска событий


    1. Начните новый проект Smart Device в Visual Studio .NET, выбрав в качестве целевой платформы Pocket PC.

    2. Добавьте в форму Form элементы управления TextBox, RadioButton, ListBox и Button (на рис. 11.4 показано, как должна выглядеть форма).

    3. Дважды щелкните на кнопке Button в окне конструктора форм. В результате этого будет создан и подключен к кнопке приведенный ниже обработчик событий button1_Click. Введите соответствующий код из листинга 11.4, который будет реагировать на это событие.

    4. Дважды щелкните на элементе управления TextBox в окне конструктора форм. В результате этого будет создан и подключен к текстовому окну приведенный ниже обработчик событий textBox1_TextChanged. Введите соответствующий код из листинга 11.4, который будет реагировать на это событие.

    5. Дважды щелкните на элементе управления RadioButton1 в окне конструктора форм. В результате этого будет создан и подключен к переключателю приведенный ниже обработчик событий radioButton1_Click. Введите соответствующий код из листинга 11.4, который будет реагировать на это событие.

    6. Измените имя второй кнопки с button2 на buttonShowEventLog и дважды щелкните на кнопке Button в окне конструктора форм. В результате этого будет создан и подключен к кнопке приведенный ниже обработчик событий buttonShowEventLog_Click. Введите соответствующий код из листинга 11.4, который будет реагировать на это событие

    7. Введите оставшуюся часть приведенного ниже кода, включая операторы #if и #endif и переменные уровня класса.

    8. В самом начале файла класса формы Form добавьте оператор #define EVENTINSTRUMENTATION. Это позволит вам использовать условную компиляцию кода.

    9. Скомпилируйте пример и запустите его на выполнение. Щелкните на кнопке Button1, введите текст в текстовом поле TextBox и щелкните на кнопке ShowEventLog, чтобы увидеть список событий, которые были запущены.

    10. Завершив выполнение приложения, удалите символы комментария в строке m_userInterfaceUpdateOccuring = true; кода обработчика событий Button_Click и повторно запустите приложение на выполнение. Обратите внимание, что установка этого флага предотвратила нежелательное выполнение кода приложения при запуске обработчиков событий в результате программного доступа к свойствам элементов управления.

    Листинг 11.4. Использование модели состояний для обновления интерфейса и контроль запуска событий с целью более глубокого изучения процесса обработки событий и управления им

    //-------------------------------------------------------

    //Поместить данную директиву #define в начале определения

    //класса, если требуется регистрация событий

    //#define EVENTINSTRUMENTATION

    //-------------------------------------------------------


    //-----------------------------------------------------------------------

    //Флаг, указывающий обработчикам событий, должен ли из них осуществляться

    //выход без выполнения каких-либо действий

    //-----------------------------------------------------------------------

    bool m_userInterfaceUpdateOccuring;


    //Счетчики событий

    private int m_radioButton1ChangeEventCount;

    private int m_textBox1ChangeEventCount;


    //-------------------------------------------------------------------------

    //Код, который следует включать лишь в том случае, если приложение

    //выполняется в режиме контроля запуска событий. Этот код характеризуется

    //относительно высокими накладными расходами, и его следует компилировать и

    //выполнять только тогда, когда выполняется диагностика.

    //-------------------------------------------------------------------------

    #if EVENTINSTRUMENTATION

    private System.Collections.ArrayList m_instrumentedEventLog;


    //------------------------------------------------------------------------

    //Заносит записи о возникновении событий в массив, который мы

    //можем просмотреть

    //Примечание: Не делается никаких попыток ограничить размерность массива

    // регистрационных записей, поэтому, чем дольше выполняется

    // приложение, тем больше становится размер массива

    //------------------------------------------------------------------------

    private void instrumented_logEventOccurrence(string eventData) {

     //Создать журнал событий, если он еще не был создан

     if (m_instrumentedEventLog == null) {

      m_instrumentedEventLog = new System.Collections.ArrayList();

     }

     //Зарегистрировать событие

     m_instrumentedEventLog.Add(eventData);

    }


    //------------------------------------------------------------------------

    //Отобразить список возникших событий

    //Примечание: Этот вариант реализации довольно груб.

    // Целесообразнее отображать список событий

    // в отдельном диалоговом окне, которое специально выводится

    // для этого на экран.

    //------------------------------------------------------------------------

    private void instrumentation_ShowEventLog() {

     System.Windows.Forms.ListBox.ObjectCollection listItems;

     listItems = listBoxEventLog.Items;


     //Очистить список элементов

     listItems.Clear();


     //При отсутствии событий - выход

     if (m_instrumentedEventLog == null) {

      listItems.Add("0 Events");

      return;

     }


     //Отобразить поверх списка общее количество

     //подсчитанных нами событий

     listItems.Add(m_instrumentedEventLog.Count.ToString() + " Events");


     //Перечислить элементы списка в обратном порядке, чтобы первыми

     //отображались самые последние из них

     string logItem;

     for(int listIdx = m_instrumentedEventLog.Count - 1; listIdx >= 0; listIdx--) {

      logItem=(string) m_instrumentedEventLog[listIdx];

      listItems.Add(logItem);

     }

    }

    #endif


    //------------------------------------------------------

    //Событие изменения состояния переключателя RadioButton1

    //------------------------------------------------------

    private void radioButton1_CheckedChanged(object sender, System.EventArgs e) {

     //Если обновление данных в пользовательском интерфейсе осуществляется

     //приложением, то мы не хотим обрабатывать его так же, как если бы

     //это событие было запущено пользователем. Если это именно так,

     //то выйти из функции без выполнения каких-либо действий.

     if (m_userInterfaceUpdateOccuring == true) {

      return;

     }


     //Подсчитать, сколько раз выполнена обработка данного события

     m_radioButtonlChangeEventCount++;


    #if EVENTINSTRUMENTATION

     //Зарегистрировать наступление события

     instrumented_logEventOccurrence("radioButton1.Change:" + //Событие

      m_radioButton1ChangeEventCount.ToString() + ":" +       //Количество раз

      radioButton1.Checked.ToString());                       //Значение

    #endif


    //-------------------------------------------------------------

    //Событие щелчка на кнопке Button1

    //Имитирует обновление пользовательского интерфейса программным

    //кодом, что может приводить к запуску обработчика события

    //-------------------------------------------------------------

    private void button1_Click(object sender, System.EventArgs e) {

     //Указать на то, что мы не хотим, чтобы обработчики сразу же

     //обрабатывали события, поскольку мы обновляем

     //пользовательский интерфейс.

     //m_userInterfaceUpdateOccuring = true;

     radioButton1.Checked = true;

     textBox1.Text = "Hello World";


     //Обновление пользовательского интерфейса завершено

     m_userInterfaceUpdateOccuring = false;

    }


    //------------------------------------------------------------------

    //Обработчик события изменения состояния элемента управления TextBox

    //------------------------------------------------------------------

    private void textBox1_TextChanged(object sender, System.EventArgs e) {

     //Если обновление данных в пользовательском интерфейсе осуществляется

     //приложением, то мы не хотим обрабатывать его так же, как если бы

     //это событие было запущено пользователем. Если это именно так,

     //то выйти из функции без выполнения каких-либо действий.

     if (m_userInterfaceUpdateOccuring == true) {

      return;

     }


     //Подсчитать, сколько раз выполнена обработка данного события

     m_textBox1ChangeEventCount++;


    #if EVENTINSTRUMENTATION

     //Занести событие в журнал

     instrumented_logEventOccurrence("textBox1.Change:" + //Событие

      m_textBox1ChangeEventCount.ToString() + ":" +       //Количество раз

      textBox1.Text.ToString());                          //Значение

    #endif

    }


    private void buttonShowEventLog_Click(object sender, System.EventArgs e) {

    #if EVENTINSTRUMENTATION

     instrumentation_ShowEventLog();

    #endif

    }

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

    Важным аспектом производительности приложения является его интерактивность. Наилучшим вариантом было бы полное отсутствие каких-либо задержек во взаимодействии пользователя с пользовательским интерфейсом, однако в некоторых случаях это оказывается просто невозможным. Если необходимо выполнить работу, результатов которой пользователь должен дожидаться в течение длительного времени, то следует приложить некоторые усилия к тому, чтобы сделать условия такого ожидания настолько комфортными, насколько это возможно. Считайте, что эта задача столь же важна, как и проектирование помещения, в котором пациенты вынуждены дожидаться своей очереди при посещении врача. Было бы замечательно, если бы к врачу можно было зайти вообще безо всякой очереди, но это не представляется возможным (Если вам известны исключения, дайте мне об этом знать!) Условия ожидания были бы значительно худшими, если бы не было человека в регистратуре, который делает отметку о вашем визите, если бы вам не сообщали, сколько примерно времени вам придется ждать, если бы окружающая обстановка не было комфортной или если бы вам не предоставляли возможность полистать или почитать какой-нибудь журнал, дабы скоротать время ожидания. Прекрасным и эстетически привлекательным элементом интерьера, помогающим занять ваше внимание, служит, в частности, аквариум. Многое дает и регистрация вашего прихода. Условия визита еще более улучшатся, если регистратор сообщит вам, как долго продлится ваше ожидание, а также предложит газету и прохладительный напиток, чтобы вы чувствовали себя более комфортно. (Опять-таки, если вам известна клиника, где именно так все и происходит, обязательно сообщите мне об этом.)

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

    ■ Наихудшие условия: полное отсутствие связи с пользователем. Картина приложения, которое не реагирует на запросы и не дает пользователю ни малейшего намека на то, что работа для него выполняется, очень напоминает зависание или блокирование приложения. Раздражение пользователя быстро нарастает и он начинает лихорадочно щелкать кнопками или давить на экран, чтобы добиться от приложения хоть какого-нибудь ответа. В результате этого остается ряд случайных непреднамеренных событий, которые приложению придется обрабатывать, когда управление будет вновь передано пользовательскому интерфейсу. Если предыдущие попытки успеха не принесли, то спустя короткое время, пользователь, вероятнее всего, попытается закрыть приложение, переключиться на другое приложение или извлечь из устройства батарею для перезарядки. (В случае мобильных телефонов это не составляет никакого труда.) Ситуация еще более усугубляется, когда мобильное приложение не предоставляет пользователю никакой визуальной индикации того, что работа завершена. Если предположить, что в течение всего времени, пока приложение было занято вычислениями, пользователь терпеливо ждал, не нажимая никаких кнопок и не вынимая батарею, то как он узнает о том, что к приложению вернулась способность реагировать на запросы? Старайтесь всегда избегать ситуаций, в которых приложение выглядит вполне нормально и, казалось бы, должно отвечать на запросы, но на самом деле этого не происходит. 

    ■ Улучшенные условия: отображение курсора ожидания в течение того времени, пока приложение не в состоянии отвечать на запросы. Отображение курсора ожидания преследует две важные цели. Во-первых, он извещает конечных пользователей о том, что работа для них выполняется, а во-вторых, его исчезновение указывает на то, что работа завершена и приложением вновь можно пользоваться. Если учесть, что отображение курсора ожидания обычно не составляет никакого труда, то нежеланию приложить даже столь незначительные усилия к тому, чтобы улучшить интерактивные свойства приложения, не может быть никаких оправданий. Любая операция, для выполнения которой требуется более нескольких десятых долей секунды, тормозит пользовательский интерфейс до такой степени, что пользователь сможет это почувствовать, и поэтому нет ни малейших оснований для того, чтобы в этой ситуации не дать пользователю понять, что приложение выполняет нужную для него работу. Используя курсор ожидания, вы на несколько секунд "покупаете" понимание и терпение со стороны пользователя устройства, но если выполнение работы занимает более нескольких секунд, то вам следует прибегнуть к более информативному индикатору выполнения, нежели простой курсор ожидания. Кроме того, если почти любое действие пользователя сопровождается появлением курсора ожидания, вам следует позаботиться об оптимизации мобильного приложения и выполнении части обработки в фоновом режиме; задержки могут быть приемлемыми в некоторых ситуациях, но далеко не во всех. 

    ■ Лучшие условия; отображение индикатора выполнения или сопроводительного текста, поясняющего, что именно происходит в данный момент. Если операция, завершения которой вынужден дожидаться пользователь, длится более нескольких секунд, следует подумать об использовании индикатора выполнения или текстовых пояснений, которые информируют пользователя о том, что происходит в данный момент, отображают процентную долю оставшейся работы или оценку времени, оставшегося до завершения операции. Отображение курсора ожидания не препятствует отображению текста, информирующего пользователя о том, как продвигается работа. Пояснительный текст, который говорит пользователю о том, какая именно работа для него выполняется, можно отображать позади курсора ожидания. Полезной будет любая информация, указывающая на степень выполнения работы. Информативный текст полезен, в частности, тем, что помогает поддерживать в пользователе чувство причастности к вычислительному процессу. Так, алгоритм, загружающий информацию с сервера, может отображать такую последовательность обновляющихся сообщений: "Поиск сервера", "Сервер найден. Выполняется регистрация", "Загружается информация (10%)", "Загружается информация (60%)", "Готово!" Информация такого рода порождает в пользователе ощущение участия в процессе загрузки. Обратите внимание на важность последнего сообщения, говорящего о том, что выполнение операции завершилось успешно. Кроме того, если в процессе загрузки возникают какие-либо трудности, то текстовая информация, которая содержится в указанных обновляемых сообщениях, может помочь пользователю в преодолении возникших проблем.

    При написании кода, предоставляющего пользователю периодически обновляемую информацию о степени завершения задач, которые выполняются в течение длительного времени, может потребоваться явная перерисовка элемента управления. Если не предусмотрено явное обновление форм и элементов управления, то они, как правило, лишь помещают сообщения, требующие перерисовки экрана, в очередь, непринудительное обновление сообщений на экране происходит только тогда, когда у системы находится время для их обработки. Поскольку ваш вычислительный процесс может выполняться в том же потоке, что и код пользовательского интерфейса, выполнение полезной работы будет препятствовать обработке сообщений, помещенных в очередь, до тех пор, пока управление не будет возвращено пользовательскому интерфейсу. Для разрешения этой проблемы в .NET Compact Framework для каждого элемента управления предусмотрен метод Update(). Вызов этого метода приводит к немедленной перерисовке элемента управления. В листинге 11.5 представлен простой рабочий алгоритм, который периодически обновляет в элементе управления Label текст, информирующий о степени выполнения приложения. Если только не осуществить вызов label1.Update() при обновлении текста в Label, то новый текст будет отображен лишь после завершения работы. 

    ■ Еще более лучшие условия: отображение информации о ходе выполнения задачи с одновременным предоставлением пользователю возможности отменить операцию. Для выполнения некоторых задач может требоваться значительное время, и пользователь может захотеть отменить такую задачу, не дожидаясь ее завершения. Может так оказаться, что синхронизация задачи с сервером будет осуществляться в течение нескольких минут, но пользователю мобильного устройства в это время надо будет войти в лифт или спуститься в метро, где доступ к сети невозможен. Возможно и такое, что конечному пользователю мобильного устройства надо срочно получить информацию из другой части приложения и ждать, пока завершится текущая задача, он не может. Если выполнение задачи затягивается, и она блокирует пользовательский интерфейс, то неплохим решением будет предусмотреть для пользователя возможность отмены операции, которая выполняется для него. Это можно обеспечить, организовав в алгоритме периодический опрос состояния флажка, который устанавливается, если пользователь нажимает кнопку отмены выполнения операции. Как объяснялось в главе, посвященной многопоточному выполнению, это удобно реализовать с помощью конечного автомата, управляющего состоянием фоновой задачи.

    Если длительная задача выполняется в высокоприоритетном потоке (том же, в котором выполняется код пользовательского интерфейса), то предоставление пользователю возможности отменить ее выполнение, хотя и осложняется, по-прежнему остается осуществимым. Как и в случае обновления пользовательского интерфейса в процессе выполнения интенсивных вычислений, реакция на пользовательские запросы, в том числе и на щелчок на кнопке отмены выполнения, возможна лишь при условии обработки сообщений потока пользовательского интерфейса. В .NET Compact Framework предусмотрено решение для подобных ситуаций, но аналогичные концепции используются, вероятно, и в каркасах приложений для других устройств. Вызов статического метода System.Windows.Forms.Application.DoEvents() приводит к принудительной обработке всех сообщений, находящихся в очереди данного потока, до того, как выполнение сможет быть продолжено. Это означает, что будут обработаны и сообщения, требующие перерисовки пользовательского интерфейса, в том числе ожидающие обработки щелчки на кнопках и нажатия клавиш. Если вызывать метод DoEvents() достаточно часто (несколько раз в секунду), то можно обеспечить сохранение интерактивности пользовательского интерфейса без малейшего ущерба для выполнения текущей задачи.

    Может показаться, что метод DoEvents() является панацеей от всех бед, но это не так. Вызывая метод DoEvents(), необходимо проявлять осторожность, поскольку при этом могут порождаться в высшей степени нежелательные тонкие эффекты. Так как вызов метода DoEvents() приводит к обработке всех сообщений, прежде чем управление будет возвращено вызвавшему его коду, то это может сопровождаться вызовом обработчиков событий таймера и повторными вхождениями в выполняющиеся в данный момент обработчики событий. В результате этого может сложиться такая ситуация, при которой мы будем иметь множество вложенных вызовов обработчиков событий, связанных с элементами пользовательского интерфейса, и таймеров, если метод DoEvents() вызывается внутри обработчика события и новое сообщение о событии помещается в очередь еще до завершения обработки первого события. Собираясь использовать метод DoEvents(), вы должны использовать при написании кода обработчиков событий своего приложения технологию "безопасного программирования" (defensive programming), которая предполагает тщательную проверку выполнения всех допустимых условий. Настоятельно рекомендуется помещать все подобные проверки в самом начале кода обработчиков событий, чтобы осуществить немедленный выход из функции в случае, если вхождение в нее является повторным. Прежде чем осуществить вхождение в блок кода, в котором выполняется задача, требующая длительного времени, и в процессе этого вызывается метод DoEvents(), убедитесь в том, что для всех элементов управления, генерация которыми событий должна быть запрещена, значение свойства Enabled установлено в false; благодаря этой мере пользователи будут лишены возможности выполнять щелчки на указанных элементах управления, тем самым загромождая очередь событий. Ситуации, в которых возможность повторного вхождения в обработчики событий является желательной, встречаются крайне редко, ибо в этом случае код становится чрезвычайно запутанным и трудно поддается отладке. Если вы можете выбирать между использованием метода DoEvents() и созданием фонового потока для выполнения основных вычислений, то я бы рекомендовал почти всегда отдавать предпочтение хорошо продуманному последнему решению. Каким бы сложным с концептуальной точки зрения ни казалось использование нескольких потоков, модель их выполнения обычно оказывается более простой и предсказуемой, чем смесь кодов каркаса и приложения, вызываемая при использовании метода DoEvents(). Ничто из вышесказанного не является чем-то специфическим для мобильных устройств; те же самые проблемы возникают и в случае настольных компьютеров, однако мы акцентируем на них внимание, исходя из повышенных потребностей конечных пользователей в отношении интерактивного взаимодействия с приложением в случае мобильных устройств. Тем не менее, иногда применение метода DoEvents() может оказаться полезным, и я описываю здесь этот подход, одновременно предостерегая вас: "Caveat emptor!" ("Пусть покупатель будет бдителен!"), то есть действуйте на свой страх и риск. Если вы покупаете билет на DoEvents(), то должны быть готовы ко всем подъемам и спускам на этом маршруте. 

    ■ Наилучшие условия: найдите такой способ выполнения задачи, которые никоим образом не приводят к блокированию действий пользователя. Если ваше мобильное приложение способно вытолкнуть выполнение работы в фоновый поток, предоставляя пользователю возможность продолжить работу с другими задачами и своевременно уведомляя его о завершении первой задачи, то можете считать, что Чаша Грааля интерактивных возможностей приложения — в ваших руках. Действуя подобным образом, вы должны тщательно продумать модель выталкивания выполнения работы в фоновый поток. Вам необходимо решить, каким образом следует информировать пользователя о ходе выполнения работы, сохранив для него возможность управления выполнением фоновой задачи, но при этом не создавать никаких ощутимых неудобств в отношении взаимодействия пользователя с высокоприоритетным потоком пользовательского интерфейса. В качестве примера удачного применения этой стратегии в случае приложений для настольных компьютеров можно привести поддержку в Microsoft Word печати документов в фоновом режиме с одновременным предоставлением пользователю возможности продолжения работы по редактированию активного документа в высокоприоритетном потоке. Это средство очень полезно и кажется простым, но, чтобы все это работало, проектировщикам пришлось хорошенько потрудиться

    Листинг 11.5. Вызов метода Update() элемента управления для отображения пояснительного текста, информирующего о ходе выполнения задачи

    //--------------------------------------------------------------------

    //Этот код принадлежит форме, содержащей по одному элементу управления

    //Button (button1) и Label (label1)

    //--------------------------------------------------------------------


    private void button1_Click(object sender, System.EventArgs e) {

     //Отобразить курсор ожидания

     System.Windows.Forms.Cursor.Current = System.Windows.Forms.Cursors.WaitCursor;

     string testString;

     for (int loop3 = 0; loop3 < 100; loop3 = loop3 + 10) {

      label1.Text = loop3.ToString() + "% выполнено...";

      //!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

      //Чтобы отобразить информацию о процессе обновления,!

      //удалите символы комментария в строке ниже         !

      //!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

      //label1.Update();

      testString = "";


      for(int loop2 = 0; loop2 < 1000; loop2++) {

       testString = testString + "тест";

      }

     }

     label1.Text = "Готово!";

     //Удалить курсор ожидания

     System.Windows.Forms.Cursor.Current = System.Windows.Forms.Cursors.Default;

    }

    Выбор подходящих форматов и размеров растровых изображений

    С растровыми изображениями приходится иметь дело как при низкоуровневой работе с графикой, так и при высокоуровневом проектировании пользовательского интерфейса. В описаниях большинства каркасов пользовательских интерфейсов упоминается элемент управления PictureBox, способный отображать битовые образы, а низкоуровневые графические библиотеки имеют дело главным образом с операциями на битовом уровне. Растровые изображения используются во многих разновидностях приложений, представляющих значительный интерес; достаточно привести в качестве примера приложения, в которых используются карты городских улиц, изображения медицинского назначения, фотографии объектов недвижимости или изображения игровых полей. В процессе работы с данными растровых изображений важно заботиться как о способах файлового представления изображений, так и об объемах памяти, занимаемых данными изображений. Для хранения и потоковой передачи данных существуют различные файловые форматы, у каждого из которых характеризуется своими достоинствами и недостатками. Независимо от того, файлы какого формата вы используете, в памяти растровые изображения обычно представляются в виде матрицы, соответствующей пиксельным размерам изображения; этот несжатый формат удобен тем, что обеспечивает быстрое использование изображений как графическим оборудованием, так и базовой операционной системой. 

    Размеры изображения имеют существенное значение

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

    Для упрощения расчетов предположим, что мы имеем дело с изображениями в форме квадрата. Тогда 1-мегапиксельному изображению будет соответствовать матрица размером 1000×1000 пикселей. Такое разрешение значительно превышает возможности экранов большинства мобильных устройств, и до недавнего времени было слишком высоким даже для экранов настольных компьютеров (размеры которых обычно составляет 1024×768 = 786432 пикселя). Вместе с тем, даже недорогие современные цифровые камеры позволяют получать фотографии, состоящие из более чем 2-мегапикселей, однако не столь уж большой редкостью являются ныне 4- и 6-мегапиксельные камеры, причем этот пиксельный показатель с каждым годом возрастает.

    Физические дисплеи с большим количеством пикселей потребляют больше электроэнергии по сравнению с экранами, обладающими низкой разрешающей способностью, их стоимость выше, они занимают больше места и являются более хрупкими. Кроме того, с увеличением размеров экрана возрастают требования к памяти устройства и его вычислительным возможностям в отношении графики. Несомненно, с течением времени количество пикселей на экранах мобильных устройств будет только расти, но есть все основания полагать, что это будет происходить с запаздыванием по отношению к тем величинам разрешений, которые будут становиться доступными для цифровых камер и настольных дисплеев. Если допустить, что размеры дисплея мобильного устройства составляют 500×500 пикселей, то экран в состоянии отображать в общей сложности 250000 пикселей. Это составляет всего лишь ¼ объема 1-мегапиксельного изображения и 1/16 объема 4-мегапиксельного. Если подойти к рассмотрению этого вопроса с другой стороны, то можно заметить, что для размещения 4- мегапиксельного изображения на дисплее Pocket PC с размерами 240×320 (76800 пикселей) потребуется отбросить 98% пикселей изображения, не попадающих в область экрана. Аналогичным образом, в случае 2-мегапиксельного изображения для размещения его на том же экране Pocket PC потребуется отбросить 96% пикселей, тогда как в 1-мегапиксельном изображении лишними окажутся 92% пикселей.

    Почему мы об этом говорим? На то имеется несколько причин: 

    ■ Размер загружаемого файла. Если избыточные пиксели вашему приложению не нужны, то имеет ли смысл их загружать? Загрузка большего файла потребует больше времени, да и в деньгах это часто будет стоить дороже. Уменьшенные размеры экранов специально учитываются Web-приложениями для мобильных устройств, которые с этой целью используют изображения с низким разрешением. To же самое имеет смысл делать и в случае мобильных приложений с развитыми функциональными возможностями, развернутых на устройствах. 

    ■ Объем памяти, необходимый для храпения изображения на устройстве. Если ваше мобильное приложение загружает крупное изображение, то его необходимо где-то хранить. В типичных случаях для этого используется либо виртуальная файловая система в ОЗУ, либо флэш-память. В любом случае для чтения и записи изображений, размеры которых превышают необходимые, требуется дополнительное время, и они зря занимают память, которую иначе можно было бы использовать для хранения большего количества изображений или других данных. Что бы вы выбрали: иметь на своем устройстве только одно 4-мегапиксельное изображение или десяток изображений по размерам экрана? 

    ■ Внутреннее представление изображения в памяти. Пожалуй, это наиболее значимый из рассматриваемых нами аспектов. Если ваше мобильное приложение загружает в память 4-мегапиксельное изображение, то оно создает в памяти битовый образ, состоящий из 4 миллионов пикселей. Скажите, часто ли вам приходилось объявлять и использовать в своих приложениях целочисленные массивы, содержащие по 4 миллиона элементов? Объем памяти, необходимый для хранения такого массива, настолько огромен, что почти во всех случаях оказывается значительно больше объема загруженного кода и всей остальной памяти, относящейся к приложению. Независимо от эффективности используемого метода сжатия файлов, хранящийся в памяти битовый образ остается битовым образом и представляет собой отображение битов изображения в памяти устройства. Кроме того, если используемый вами файловый формат изображения предполагает привлечение сложных математических алгоритмов, обеспечивающих высокую степень сжатия данных, то эти же алгоритмы должны будут применяться и для распаковки изображений, что довольно-таки ощутимо скажется на производительности вашего приложения. Загрузка крупных растровых изображений в память — это прямая дорога к исчерпанию всей свободной памяти, доступной на устройстве, результатом чего будет либо полное падение производительности вашего приложения, либо его аварийное завершение вследствие нехватки памяти.

    Мораль сей басни такова: не имеет никакого смысла использовать изображения с числом пикселей, превышающим размер экрана. Эти изображения будут медленно переноситься на устройство, потребуют использования больших объемов памяти для их загрузки и сохранения, и в любом случае должны будут урезáться до размеров, соответствующих размерам экрана мобильного устройства. Лучше всего согласовываться с размерами экранов доступных устройств и выбирать такие размеры изображений, которые соответствуют размерам рабочего пространства. Если в приложении предусмотрен элемент управления PictureBox, размеры которого должны составлять 120×120 пикселей, то использовать следует изображения с такими же размерами.

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

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

    Уменьшение размера файла: достижение баланса между степенью сжатия и разрешением изображения

    Для уменьшения размеров файлов цифровых изображений используют три способа:

    1. Снижение разрешения. Суть этого способа заключается в уменьшении фактического количества пикселей, составляющих изображение. Если в Windows XP войти в программу Paint, выбрать в меню Рисунок пункт Растянуть/наклонить и уменьшить изображение до 50% его первоначальной ширины, то количество пикселей в изображении уменьшится наполовину. Уменьшив далее изображение до 50% его первоначальной высоты, вы еще раз уменьшите его разрешение в два раза. Теперь количество пикселей в полученном изображении будет составлять 25% от первоначального (0,5×0,5 = 0,25). Уменьшение количества пиксельных данных, подлежащих сохранению, означает уменьшение общего размера изображения.

    2. Выбор наиболее подходящего формата файлов. Различные разновидности распространенных файловых форматов отличаются друг от друга своей способностью сжимать различные изображения. Одни форматы хорошо подходят для фотографий (множество цветовых оттенков, мало резких границ), другие больше всего пригодны для сохранения компьютерной графики (резкие границы изображений, небольшое количество цветовых оттенков). В некоторых форматах сжатие изображений вообще не применяется. Выбор наиболее подходящего формата позволит обеспечить наилучшее качество изображения при минимальном размере.

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

    3. Увеличение степени сжатия. Во многих случаях алгоритмы сжатия с потерей качества предоставляют возможность выбора приемлемой степени сжатия, обеспечивающей получение изображений желаемого качества.

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

    Так много файловых форматов и так мало времени…

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

    Формат JPG/JPEG

    В полном соответствии со своим названием файлы JРЕG (Joint Photographic Expert Group — Объединенная группа экспертов в области фотографии) являются непревзойденными при хранении фотографических и реальных изображений. JPEG — это формат сжатия, допускающий регулирование потери качества изображений, что в общем случае позволяет обеспечивать отличное сжатие файлов за счет лишь незначительного ухудшения качества изображения. Во всех JPEG-фотографиях, полученных обычными цифровыми камерами, используется сжатие с потерями. Именно благодаря этому удается сжимать потрясающие 3-мегапиксельные изображения до размера 600 Кбайт. В более сложных программах рисования можно регулировать степень сжатия JPEG-файлов, что позволяет добиваться наилучшего баланса между качеством и размером изображения. При использовании формата JPEG фотографии, адаптированные к разрешению экрана Pocket PC, могут быть сжаты до размера менее 20 Кбайт, но, несмотря на это, будут отлично выглядеть.

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

    Формат PNG

    Формат PNG (Portable Network Graphics — переносимая сетевая графика) является, можно сказать, новичком среди файловых форматов, однако этот новичок быстро завоевывает признание в качестве превосходного формата файлов, предназначенных для хранения изображений. Попытки создания формата PNG были реакцией на возникновение некоторых проблем, связанных с защитой прав на интеллектуальную собственность в отношении формата GIF. Файлы формата PNG обеспечивают отличное сжатие без потерь в случае цифровых изображений. Если ваша платформа поддерживает изображения PNG и вам необходимо сжатие без потерь, то это именно тот формат, который вам нужен. Формат PNG во многих случаях обеспечивает значительно лучшее сжатие, чем формат GIF, и при доступности обоих форматов предпочтение следует отдавать именно ему.

    Формат GIF

    Формат GIF (Graphics Interchange Format — формат графического обмена) можно считать предшественником формата PNG. GIF-файлы также предлагают сжатие без потерь, но ограничены использованием 256 цветов. Из-за этого ограничения формат GIF не годится для хранения фотографических изображений. GIF-файлы интенсивно использовались при создании Web-страниц в Internet в первые годы своей популярности и по этой причине продолжают находить широкое применение и в наши дни.

    Формат BMP

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

    В том, что касается разработки приложений для .NET Compact Framework, JPG- файлы больше всего подходят для фотографических изображений, а PNG-файлы — для битовых образов, требующих максимально достоверной передачи изображений. Ситуации, в которых требуется полная, с точностью до пикселя, достоверность передачи изображений, обычно встречаются тогда, когда некоторое изображение будет использоваться с определением одного цвета пикселей в качестве прозрачного. Области прозрачности могут применяться при создании сложных рисунков, когда одни рисунки "просматриваются" через другие, о чем будет более подробно говориться далее в главе 13.

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

    Многие современные мобильные телефоны выпускаются с установленными в них цифровыми камерами, позволяющими получать фотографии. При этом размеры некоторых изображений могут превышать 1 мегапиксель (1000×1000 пикселей), и можно не сомневаться, что эта тенденция будет только усиливаться. Как и фотографии, получаемые с помощью цифровых фотокамер, разрешение таких изображений значительно превосходит те пределы, выше которых нормальный вывод изображений на экраны устройств становится невозможным; эти изображения предназначены для просмотра на экранах с большими размерами (настольные компьютеры) или для вывода на печать. Загружать и удерживать в памяти битовые образы таких размеров слишком расточительно, однако из-за проблем задержек и устойчивости связи бессмысленно пытаться отсылать картинку на сервер для ее уменьшения, чтобы получить ее обратно на устройстве в уменьшенном виде. Решение, которое позволяет работать с изображениями, характеризующимися высоким разрешением, и вместе с тем сохраняет возможность эффективного управления памятью устройства, состоит в следующем:

    1. Загрузите изображение с высоким разрешением в память. Примечание. Если изображение достаточно велико, то такая загрузка на деле может оказаться невозможной; в этом случае должно быть каким-то образом получено изображение с более низким разрешением, причем это может делаться даже на стадии получения самого изображения. Изображение с более низким разрешением может храниться вместе с полномасштабным изображением.

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

    3. При первой же возможности удалите исходное изображение, имеющее высокое разрешение, и освободите занимаемую им память.

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

    Различные подходы к реализации пользовательских интерфейсов в управляемых средах выполнения

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

    .NET Compact Framework, выполняющаяся на устройствах, использующих операционные системы Windows СЕ, Pocket PC и Smartphone, предоставляет визуализацию большинства своих элементов управления пользовательского интерфейса и управление ими операционной системе. Под этим подразумевается что элементу управления Window платформы .NET Compact Framework соответствует элемент управления Window операционной системы Windows СЕ, элементу управления ListView платформы .NET Compact Framework — элемент управления ListView операционной системы Windows СЕ, элементу управления Button платформы .NET Compact Framework — элемент управления Button операционной системы Windows СЕ и так далее. Преимущества такого подхода обусловлены несколькими причинами:

    1 Производительность. Специалисты по разработке операционных систем затратили много времени на проектирование и настройку элементов управления своих пользовательских интерфейсов. Добиться аналогичной производительности при перерисовке всех окон и элементов управления и обеспечить надлежащее управление их взаимодействием с пользователем путем самостоятельной реализации этих возможностей очень трудно.

    2. Внешний вид и нюансы поведения. Элементы управления .NET Compact Framework обладают теми же поведенческими характеристиками, что и элементы управления базовой системы Windows. Реализация пользовательского интерфейса сопряжена с необходимостью принятия множества мелких решений наподобие: "Как именно должны прорисовываться пиксели при визуализации элемента управления? Что должно происходить, если выделить блок текста и нажать клавишу забоя, ввести букву, выполнить двойной щелчок на слове?" Добиться точного воспроизведения внешнего вида и нюансов поведения пользовательского интерфейса очень трудно, а вместе с тем люди очень чувствительны к малейшим отклонениям от того, к чему они привыкли.

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

    Несмотря на то что в .NET Compact Framework, выполняющейся на устройствах с операционными системами Pocket PC, Smartphone или Windows СЕ, реализация большинства элементов пользовательского интерфейса делегируется операционной системе, это вовсе не означает, что вы не имеете возможности создать совершенно новый элемент управления средствами каркаса приложения. В .NET Compact Framework можно создать графический элемент управления с нуля, и это делается в тех случаях, когда в базовой операционной системе аналогичный элемент управления отсутствует. Эту работу может выполнить конечный разработчик, но для таких элементов управления, как GridControl, она была выполнена и в .NET Compact Framework.

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

    Если .NET Compact Framework переносится на другую платформу (в процессе ее проектирования такая возможность заранее предполагалась), то она должна будет либо использовать возможности пользовательского интерфейса базовой операционной системы, либо предоставить свою реализацию этих элементов управления в виде библиотек на основе собственного или управляемого кода. По своим возможностям встроенной поддержки элементов управления пользовательского интерфейса операционные системы различных устройств существенно отличаются друг от друга, так что объем работы, которую при этом придется выполнить, и выбор стратегии проектирования оказываются в высшей степени зависящими от характеристик конкретной целевой операционной системы.

    Стратегии проектирования высокопроизводительного графического кода

    Код, предназначенный для обработки графики, целесообразно рассматривать отдельно от кода высокоуровневого пользовательского интерфейса. Обычно в коде пользовательского интерфейса используются высокоуровневые абстракции, предлагаемые операционной системой или средой программирования. Примерами подобных абстракций могут служить такие понятия, как элементы управления Button, ListBox, ListView, Panel или Grid. На самом нижнем уровне элементы управления реализуются в виде последовательностей инструкций рисования и низкоуровневого взаимодействия с операционной системой и пользователями. Разработчикам приложений почти никогда не приходится иметь дело с элементами управления на этом самом низком уровне абстракции. Обычно, используя элементы управления пользовательского интерфейса, разработчик полагается на уже организованное для него оптимизированное выполнение базовых операций, связанных с рисованием и поддержкой элементов управления; задача разработчика заключается лишь в том, чтобы наиболее эффективным способом использовать эти элементы управления на более высоком уровне абстракции. В отличие от этого, при работе с графикой разработчик должен специально продумать, как должны решаться низкоуровневые задачи рисования и каким образом можно обеспечить их наиболее эффективное выполнение.

    С позиций абстрагирования код высокоуровневого пользовательского интерфейса аналогичен организации связи в сети с использованием запросов и ответов по протоколу HTTP; в то же время, графический код аналогичен организации связи на уровне сокетов или же пакетов. Как и в случае сетевых коммуникаций, во всех возможных случаях лучше использовать высокоуровневые абстракции. Высокоуровневые абстракции легко использовать, они эффективны для большинства задач и надежно протестированы. Эффективная работа с низкоуровневыми программными средствами, например, графическими библиотеками, требует более глубокого понимания того, что при этом происходит, и соответствующего активного вмешательства в ход событий, и должна сопровождаться тщательным анализом того, какую пользу с точки зрения эффективности это может принести приложению. Графический код выглядит и работает иначе, чем код пользовательского интерфейса.

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

    Четырехминутный практикум по работе с графикой в .NET Compact Framework

    В документации к .NET Framework и .NET Compact Framework содержится обширная информация по работе с графикой, с которой вам обязательно стоит ознакомиться. Ниже приведены лишь самые основные сведения.

    Рабочей лошадкой графики в NET является объект System.Drawing.Graphics. Если вы знакомы с методами разработки приложений в собственных кодах Windows, считайте, что это — объектно-ориентированная версия hDC (Device Context, контекст устройства). Чтобы нарисовать что-либо (например, фигуру, линию, текст, растровое изображение) на поверхности битовой карты, ваш код должен вызвать соответствующий метод объекта Graphics. Не менее важно знать о том, что после того как вы закончите работать с созданным перед этим объектом Graphics, необходимо освободить память от этого объекта при помощи вызова функции Dispose(), иначе он будет занимать драгоценные системные ресурсы. Для того чтобы постоянно держать под рукой какой-либо из объектов Graphics, у вас должны быть веские причины.

    Если объект Graphics не создается, а передается, то обычно этот объект вам предоставляет вызывающая функция. В подобных случаях забота о том, чтобы освободить память от этого объекта с помощью метода Dispose(), обычно лежит не на вашем коде; при вызове метода Dispose() объекта, который все еще используется другим кодом, возникают проблемы. Наиболее распространенные ситуации, в которых объект Graphics передается другой функции, возникают, когда вы пишете обработчик Paint для нестандартного элемента управления или подключаетесь к обработчику Paint для формы или стандартного элемента управления. В этих случаях вы выполняете нужные операции рисования при помощи передаваемого вам объекта Graphics.

    Перья (Pen), кисти (Brush), шрифты (Font), растровые изображения (Bitmap) также должны создаваться. Эти объекты не принадлежат какому-то одному объекту Graphics; одни и те же объекты могут использоваться различными объектами Graphics. Когда работа с этими объектами в вашем коде закончена, для них также необходимо вызывать метод Dispose(). Тем не менее, в случае мобильных приложений использование глобальных объектов Pen, Brush, Font или Bitmap, если они часто используются, может оказаться полезным и эффективным. Производительность приложения можно значительно повысить, применяя тщательно продуманную стратегию кэширования объектов.

    Для указания атрибутов рисования визуализируемых объектов используется класс ImageAttributes. Он передается при помощи перегруженных методов Graphics.DrawImage() и позволяет вашему приложению устанавливать прозрачный цвет для битовых образов, копируемых на другую поверхность. Использование класса ImageAttributes на платформе .NET Compact Framework позволяет вашему приложению назначать один из цветов битового образа в качестве прозрачного; благодаря этому ваш код получает возможность визуализировать на битовых образах объекты, форма которых отличается от прямоугольной. Такая возможность оказывается очень полезной при написании игр. Как и в случае других классов, объекты ImageAttributes по окончании работы должны вызывать метод Dispose(). 

    Способы интеграции графики с кодом пользовательского интерфейса

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

    1. Отображение растровых изображений в элементе управления PictureBox.

    2. Рисование непосредственно в форме.

    3. Реализация пользовательских элементов управления.

    Перечисленные способы описываются ниже с использованием иллюстративных примеров их применения. 

    Отображение растровых изображений в элементе управления PictureBox

    Рисование графики на внеэкранных битовых картах и отображение полученных битовых образов в элементе PictureBox на форме является самым простым и наиболее мощным способом работы с пользовательской графикой. В силу своей простоты такой подход великолепно подходит для работы почти с любыми видами деловой графики и даже для многих игр.

    Чтобы использовать этот подход, вашему приложению достаточно создать объект Bitmap и соответствующий ему объект Graphics, а затем использовать этот объект Graphics для рисования всего, что необходимо, на битовой карте, прежде чем назначить ее свойству Image элемента управления PictureBox. Элемент управления PictureBox позаботится обо всем остальном. Он проверяет, обновлено ли изображение на экране, и при необходимости перерисовывает его; если элемент управления PictureBox временно перекрывался, частично или полностью, другим элементом управления или окном, PictureBox самостоятельно перерисует его, когда он вновь окажется открытым. PictureBox осуществляет проверку того, что выводимое изображение идентично тому, которое хранится в битовой карте; предусматривать в приложении какую-либо дополнительную логику для этого не требуется.

    В силу своей простоты такая методика прекрасно подходит для отображения графических данных, которые не претерпевают ежесекундных изменений. Он также обеспечивает достаточную эффективность, поскольку элементу управления PictureBox известно, когда он должен сам себя перерисовать. Элементы управления PictureBox могут предоставлять также некоторые события (например, Click), к которым ваше приложение может подключаться, что позволяет наделять их некоторой дополнительной интерактивностью, что делает этот подход еще более полезным

    На рис. 11.5 представлено приложение, в котором имеются элементы управления Button и PictureBox. В результате нажатия кнопки Button создается внеэкранная битовая карта, на ней рисуется эллипс и некоторый текст к нему, после чего эта карта назначается элементу управления PictureBox. В листинге 11.6 приведен выполняющий описанные операции рисования код, который необходимо вставить в код обработчика события щелчка на кнопке. Для создания и запуска данного приложения необходимо выполнить следующие действия:

    Рис. 11.5. Рисование на внеэкранной битовой карте и передача ее в элемент управления PiсtureBох


    1. Начните новый проект Smart Device в Visual Studio .NET и выберите в качестве целевой платформы Pocket PC.

    2. Добавьте в форму Form в окне конструктора форм элементы управления PictureBox и Button. 

    3. Дважды щелкните на кнопке Button1 в окне конструктора форм; в результате этого будет создан и подключен к кнопке приведенный ниже обработчик событий button1_Click. Введите приведенный в листинге 11.6 код, который реагирует на это событие. 

    4. Скомпилируйте пример и запустите его на выполнение. Щелкните на кнопке Button1.

    С применения подхода, основанного на элементе управления PictureBox, рекомендуется начинать в тех случаях, когда существует необходимость в отображении графических данных. Если такой подход в состоянии удовлетворить нужды вашего приложения — отлично! Пытаться использовать другие, более сложные подходы имеет смысл лишь в тех случаях, когда только что описанный подход вас не устраивает.

    Листинг 11.6. Создание изображения на внеэкранной растровой поверхности и передача его в элемент управления PictureBox

    //--------------------------------------------------------------------

    //Создать рисунок на растровой поверхности. Переслать его в PictureBox

    //--------------------------------------------------------------------

    private void button1_Click(object sender, System.EventArgs e) {

     //Создать новую битовую карту

     System.Drawing.Bitmap myBitmap;

     myBitmap = new System.Drawing.Bitmap(pictureBox1.Width, pictureBox1.Height);


     //--------------------------------------------------------------------------

     //Создать объект Graphics, чтобы иметь возможность рисовать на битовой карте

     //--------------------------------------------------------------------------

     System.Drawing.Graphics myGfx;

     myGfx = System.Drawing.Graphics.FromImage(myBitmap);


     //Закрасить нашу битовую карту желтым цветом

     myGfx.Clear(System.Drawing.Color.Yellow);


     //Создать перо

     System.Drawing.Pen myPen;

     myPen = new System.Drawing.Pen(System.Drawing.Color.Blue);


     //-----------------

     //Нарисовать эллипс

     //-----------------

     myGfx.DrawEllipse(myPen, 0, 0, myBitmap.Width - 1, myBitmap.Height -1);


     //Создать сплошную кисть

     System.Drawing.Brush myBrush;


     //-----------------------

     //Нарисовать текст кистью

     //-----------------------

     myBrush = new System.Drawing.SolidBrush(System.Drawing.Color.Black);


     //Примечание: Мы используем объект Font из формы

     myGfx.DrawString("Hello!",this.Font, myBrush, 2, 10);


     //------------------------------

     //Важно! Очистить все после себя

     //------------------------------

     myGfx.Dispose();

     myPen.Dispose();

     myBrush.Dispose();

     //-------------------------------------------------------------------

     //Указать объекту pictureBox, на необходимость отображения растрового

     //изображения, которое мы только что создали и нарисовали.

     //-------------------------------------------------------------------

     pictureBox1.Image = myBitmap;

    }

    Рисование непосредственно в форме

    Для рисования в форме существует два способа:

    1. Создайте объект Graphics для формы и используйте этот объект для создания нужных рисунков.

    2. Подключитесь к функции OnPaint() для формы и получите объект Graphics, который можно будет использовать для рисования.

    Создание объекта Graphics для формы и рисование на нем обычно применяются лишь изредка, поскольку в этом случае любой созданный вами рисунок не существует постоянно. Форма с удовольствием предоставит вашему приложению свою поверхность для рисования всего, что вам заблагорассудится, но не будет запоминать нарисованное. Это означает, что если форма по какой-либо причине будет перерисована, то все, что нарисовал ваш код, исчезнет. Поэтому данный метод не годится для графики, которую вы хотите сохранять на экране в течение длительных периодов времени. Так, описанный способ плохо подходит для вычерчивания диаграмм, которые пользователь будет просматривать на своем мобильном устройстве, поскольку ваше приложение не в состоянии контролировать, когда именно отображаемая картинка будет частично или полностью перекрыта другим изображением. Вместе с тем, данный метод может оказаться полезным в тех случаях, когда все экранное изображение все время полностью перерисовывается, как это происходит, например, в играх. Если изображение перерисовывается несколько раз в секунду, то не имеет никакого значения, является ли оно постоянно существующим или нет.

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

    1. Начните новый проект Smart Device в Visual Studio .NET и выберите в качестве целевой платформы Pocket PC.

    2. Добавьте в форму Form кнопку Button.

    3. Дважды щелкните на кнопке Button1 в окне конструктора форм; в результате этого будет создан и подключен к кнопке приведенный ниже обработчик событий button1_Click. Введите приведенный в листинге 11.7 код, который реагирует на это событие.

    4. Скомпилируйте пример и запустите его на выполнение. Щелкните на кнопке Button1.

    Листинг 11.7. Создание объекта Graphics для формы

    //----------------------------------------------------------

    //Создает объект Graphics для формы и осуществляет рисование

    //----------------------------------------------------------


    private void button1_Click(object sender, System.EventArgs e) {

     //Создать объект Graphics для формы

     System.Drawing.Graphics myGfx;

     myGfx = this.CreateGraphics();


     //Создать кисть

     System.Drawing.Brush myBrush;

     myBrush = new System.Drawing.SolidBrush(System.Drawing.Color.DarkGreen);


     //Заполнить прямоугольник

     myGfx.FillRectangle(myBrush, 4, 2, 60, 20);

     //-------------------------

     //Важно: Выполнить очистку!

     //-------------------------

     myBrush.Dispose();

     myGfx.Dispose();

    }

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

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

    Функции перерисовки вызываются операционной системой тогда, когда ваше приложение нуждается в "ретушировании". Если у вас имеется более или менее статическое графическое изображение, которое вы хотите перерисовывать только тогда, когда в этом возникает необходимость, то это очень удобно реализовать, подключаясь к функции Paint. В то же время, вы должны проанализировать, не удастся ли вам решить те же задачи, используя элемент управления PictureBox; в этом случае вам вообще не приходится иметь дело с какими бы то ни было низкоуровневыми запросами перерисовки.

    В листинге 11.8 приведен пример реализации функции перерисовки экрана, которая при поступлении запроса на перерисовку формы рисует прямоугольник и фрагмент текста. Эту функцию следует поместить в код формы. Выполните следующие три эксперимента с использованием этого кода, которые помогут вам лучше понять, как и когда именно функция рисования вызывается операционной системой: 

    ■ Поместите на форму кнопку, которая выполняет вызов this.Update(). Заметьте, что, как правило, это не приводит к немедленному вызову функции OnPaint. Вызов Update() лишь требует, чтобы форма или элемент управления перерисовывались, если имеются области, ставшие недействительными. 

    ■ Поместите не форму кнопку, которая выполняет вызов this.Invalidate(). В результате этого вызова запросы OnPaint перемещаются в начало очереди и обрабатываются сразу же. как только у операционной системы появляется такая возможность, что обычно наступает очень быстро. Выполнение один за другим вызовов this.Invalidate(); this.Update(); приводит к немедленной перерисовке формы. 

    ■ Поместите на форму кнопку, которая отображает элемент управления MessageBox. Начните перемещать окно сообщений (MessageBox) в разные места формы, наблюдая за тем, когда именно инициируются вызовы OnPaint(). В ходе моих экспериментов помещение элемента управления MessageBox над цветным прямоугольником не приводило к вызову OnPaint() (вероятно, в этом случае операционная система сама заботилась о перерисовке формы, не нуждаясь в помощи со стороны приложения), однако такой вызов осуществлялся, когда окно сообщений помещалось над текстом. Таким образом, можно сделать вывод, что операционная система иногда способна самостоятельно выполнять для вас элементарную перерисовку экрана, не нуждаясь в вызовах OnPaint().

    Ключевым обстоятельством, которое должно быть вам абсолютно понятным, если вы используете подключение к функциям Paint, является то, что вы не в состоянии контролировать, когда именно будут вызываться эти функции. Вы можете сделать это принудительно, объявив область экрана недействительной, но не имеете никакой возможности контролировать внешние факторы, которые могут инициировать запросы перерисовки экрана. Поэтому крайне важно, чтобы операции перерисовки осуществлялись как можно быстрее и с малыми накладными расходами. Если перерисовка экрана осуществляется очень медленно, то будет создаваться впечатление, что ваше приложение работает в замедленном режиме.

    Листинг 11.8. Подключение к функции Paint формы

    //Кисти, которые мы хотим кэшировать, чтобы избавить себя

    //от необходимости все время создавать их и уничтожать

    System.Drawing.Brush m_brushBlue;

    System.Drawing.Brush m_brushYellow;


    //Ради интереса подсчитаем, сколько раз осуществлялся вызов

    int m_paintCount;


    //-----------------------------------------------------------------------------

    //Мы перекрываем обработчики событий Paint наших базовых классов. Это означает,

    //что каждый раз, когда форма вызывается для перерисовки самой себя, будет

    //вызываться эта функция.

    //-----------------------------------------------------------------------------

    protected override void OnPaint(PaintEventArgs e) {

     //ВАЖНО: Вызвать базовый класс и дать ему возможность

     //выполнить всю необходимую работу по рисованию

     base.OnPaint(e);


     //Увеличить на 1 значение счетчика вызовов

     m_paintCount = m_paintCount + 1;


     //------------------------------------------------------------------------

     //Важно:

     //Вместо того чтобы создавать объект Graphics, мы получаем его на время

     //данного вызова. Это означает, что освобождать память путем вызова

     //метода .Dispose() объекта - не наша забота

     //------------------------------------------------------------------------

     System.Drawing.Graphics myGfx;

     myGfx = e.Graphics;


     //-------------------------------------------------------------------

     //Поскольку эту операцию рисования необходимо выполнить быстро,

     //кэшируем кисти, чтобы избавить себя от необходимости создавать их и

     //уничтожать при каждом вызове

     //-------------------------------------------------------------------

     if (m_brushBlue == null) {

      m_brushBlue = new System.Drawing.SolidBrush(System.Drawing.Color.Blue);

     }

     if (m_brushYellow == null) {

      m_brushYellow = new System.Drawing.SolidBrush(System.Drawing.Color.Yellow);

     }

     //-------------------

     //Выполнить рисование

     //-------------------

     myGfx.FillRectangle(m_brushBlue, 2, 2, 100, 100);

     myGfx.DrawString("PaintCount: " + m_paintCount.ToString(), this.Font, mbrushYellow, 3, 3);

     //Выход: Объекты, для которых мы должны были бы вызывать метод

     //.Dispose(), отсутствуют.

    }

    Обработчики событий или перекрытые функции?

    Существует еще один способ подключения к запросам Paint. В приведенном выше примере для этого использовался подход, основанный на механизме наследования. Однако обработку событий Paint можно осуществлять и при помощи обработчика событий. Это выполняется точно так же, как и подключение к обработчику событий Click; для получения запросов событий необходимо зарегистрировать функцию. Если вместо перекрытия метода OnPaint() использовать обработчик событий, то в приведенный выше код необходимо внести три вида изменений:

    1. Следует изменить имя и сигнатуру метода таким образом, чтобы они соответствовали тому, что требуется для обработчика событий Paint (например, protected void PaintEventHandler(object sender, PaintEventArgs e) вместо protected override void OnPaint(PaintEventArgs e)).

    2. Вызов base.OnPaint (e) необходимо удалить, поскольку осуществление этого вызова не входит в обязанности обработчика событий. В действительности, именно базовая реализация base.OnPaint(e) вызывает любой зарегистрированный обработчик событий.

    3. В функцию InitializeComponent формы необходимо добавить код для подключения нового обработчика событий. Например, добавьте такой код:

    this.Paint += new System.Windows.Forms.PaintEventHandler(this.PaintEventHandler);

    InitializeComponent();

    Выбирая между обработчиком событий и механизмом наследования, вы можете руководствоваться собственными соображениями. Что касается меня, то для такой низкоуровневой работы, как подключение к обработке запросов перерисовки экрана (Paint), я предпочитаю использовать подход, основанный на механизме наследования, который мне более понятен. Для высокоуровневых событий, например щелчков (Click), я предпочитаю прибегать к подходу, основанному на использовании обработчиков событий, поскольку этот код автоматически генерируется для меня после двойного щелчка на элементе управления в окне конструктора форм.

    Реализация пользовательских элементов управления

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

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

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

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

    Пользовательские элементы управления являются мощной абстракцией, которую вы можете использовать в своих приложениях, но важно не пытаться применять их искусственно, без всякой на то необходимости. Если требуется обеспечить высокую степень интерактивности при взаимодействии конечного пользователя с приложением и/или для разработчика важно иметь возможность работать с детализированными событиями, то решение, основанное на пользовательских элементах управления, заслуживает рассмотрения. В листингах 11.9 и 11.10 представлена очень простая реализация пользовательского элемента управления в .NET Compact Framework. В листинге 11.9 содержится код, предназначенный для самого элемента управления, тогда как в листинге 11.10 — код, который используется для динамического создания экземпляра элемента управления, помещения его в форму и подключения к нему обработчика события. На рис. 11.6 показано выполняющееся на устройстве Pocket PC приложение, в котором присутствует данный нестандартный элемент управления.

    НА ЗАМЕТКУ

    Существует возможность сделать пользовательский элемент управления видимым на стадии проектирования!

    В данном примере пользовательский элемент управления для простоты создается во время выполнения. Чтобы отобразить его для разработчика на стадии проектирования, в результате чего этот элемент управления будет виден в окне конструктора форм подобно любому другому стандартному элементу управления, в приложение следует кое-что добавить. Обсуждение этого вопроса выходит за рамки данной книги, однако соответствующая информация содержится в документации по .NET Compact Framework.Окно сообщения, появляющееся в ответ на запуск события щелчка на пользовательском элементе управления

    Рис. 11.6. Окно сообщения, появляющееся в ответ на запуск события щелчка на пользовательском элементе управления


    Для создания и запуска данного приложения потребуется выполнить следующие действия:

    1. Начните новый проект Smart Device в Visual Studio NET и выберите в качестве целевой платформы Pocket PC.

    2. Добавьте в проект новый класс и назовите его myButton. Перейдя в окно редактора кода класса введите код, представленный в листинге 11.9.

    3. Перейдите в окно конструктора форм для формы Form1. Добавьте в форму кнопку. Дважды щелкните на кнопке для перехода в окно редактора кода и введите код, представленный в обработчике события button1_Click в листинге 11.10.

    4. Введите за кодом обработчика события button1_Click оставшуюся часть кода, представленного в листинге 11.10.

    5. Скомпилируйте пример и запустите его на выполнение. Щелкните на кнопке Button1.

    Листинг 11.9. Простой пользовательский элемент управления, который изменяет цвета и запускает событие, определяемое пользователем

    //Простейший пользовательский элемент управления

    public class myButton : System.Windows.Forms.Control {

     //--------------------------------------

     //Объекты, необходимые нам для рисования

     //--------------------------------------

     System.Drawing.Brush m_RectangleBrush;

     System.Drawing.Brush m_TextBrush;

     System.Drawing.Color m_RectangleColor;

     //----------------------------------------------------

     //Событие, которое мы хотим предоставить на обработку.

     //Это - общедоступный делегат.

     //----------------------------------------------------

     public event System.EventHandler EventButtonTurningBlue;


     //Конструктор

     public myButton() : base() {

      //ПРИМЕЧАНИЕ: Мы должны написать функцию Dispose() и

      //деструктор, который освобождает память от этих объектов

      //Создать необходимые кисти

      m_RectangleColor = System.Drawing.Color.Black;

      m_RectangleBrush = new System.Drawing.SolidBrush(m_RectangleColor);

      m_TextBrush = new System.Drawing.SolidBrush(System.Drawing.Color.White);

     }


     //-----------------------------------------------

     //Внутренним откликом на щелчок является

     //повторение трех различных цветов кнопки в цикле

     //-----------------------------------------------

     protected override void OnClick(System.EventArgs e) {

      //--------------------------------------------------------

      //Важно: Вызвать базовую реализацию. Это

      //обеспечит возможность вызова любого обработчика событий,

      //подключенного к данному элементу управления

      //--------------------------------------------------------

      base.OnClick(e);


      //------------------------------------------------------

      //Выбрать цвет новой кисти, исходя из цвета старой кисти

      //------------------------------------------------------

      if (m_RectangleColor == System.Drawing.Color.Black) {

       m_RectangleColor = System.Drawing.Color.Blue;

       //!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

       //Запустить событие                                   !

       //!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

       if (EventButtonTurningBlue!= null) {

        //Возбудить событие без передачи аргумента

        EventButtonTurningBlue(this, null);

       }

      }

      else

       if (m_RectangleColor == System.Drawing.Color.Blue) m_RectangleColor = System.Drawing.Color.Red;

       else m_RectangleColor = System.Drawing.Color.Black;


      //-----------------------

      //Освободить старую кисть

      //-----------------------

      m_RectangleBrush.Dispose();


      //----------------------------------------------------------------

      //Создать новую кисть, которую мы собираемся использовать для фона

      //----------------------------------------------------------------

      m_RectangleBrush = new System.Drawing.SolidBrush(m_RectangleColor);


      //------------------------------------------------------------

      //Сообщить операционной системе, что наш элемент управления

      //должен быть перерисован, как только представится возможность

      //------------------------------------------------------------

      this.Invalidate();

     }


     //----------------------------------------------------------------

     //Ради интереса подсчитаем, сколько раз осуществлялась перерисовка

     //----------------------------------------------------------------

     int m_paintCount;

     protected override void OnPaint(System.Windows.Forms.PaintEventArgs e) {

      //--------------------------------------------

      //ВАЖНО: Вызвать базовый класс и позволить ему

      //выполнить работу по рисованию

      //--------------------------------------------

      base.OnPaint(e);


      //Увеличить на единицу значение счетчика вызовов

      m_paintCount = m_paintCount + 1;


      //-------------------------------------------------------------------

      //Важно:

      //Вместо того чтобы создавать объект Graphics, мы получаем его

      //на время данного вызова. Это означает, что освобождать память путем

      //вызова метода .Dispose() объекта - не наша забота

      //-------------------------------------------------------------------

      System.Drawing.Graphics myGfx;

      myGfx = e.Graphics;


      //Нарисовать прямоугольник

      myGfx.FillRectangle(m_RectangleBrush, 0, 0, this.Width,this.Height);


      //Нарисовать текст

      myGfx.DrawString("Button! Paint: " + m_paintCount.ToString(), this.Parent.Font, m_TextBrush, 0, 0);

     } //конец функции

    } //конец класса

    Листинг 11.10. Код, который должен быть помещен в форму для создания экземпляра пользовательского элемента управления

    //--------------------------------------------------------------

    //Этот код будет подключен в качестве нашего обработчика событий

    //--------------------------------------------------------------

    private void CallWhenButtonTurningBlue(object sender, System.EventArgs e) {

     System.Windows.Forms.MessageBox.Show("Button is about to turn blue!");

    }


    //Наша новая кнопка

    myButton m_newControl;


    //----------------------------------------------

    //Эта функция подключается для обработки событий

    //щелчка на кнопке Button1

    //----------------------------------------------

    private void button1_Click(object sender, System.EventArgs e) {

     //----------------------------------------------

     //Для простоты мы допускаем существование только

     //одного экземпляра элемента управления.

     //----------------------------------------------

     if (m_newControl != null) {

      return;

     }


     //Создать экземпляр нашей кнопки

     m_newControl = new myButton();


     //Указать ему его местоположение внутри родительского объекта

     m_newControl.Bounds = new Rectangle(10, 10, 150, 40);


     //-------------------------------

     //Присоединить обработчик событий

     //-------------------------------


     m_newControl.EventButtonTurningBlue += new System.EventHandler(this.CallWhenButtonTurningBlue);


     //Добавить его в список элементов управления данной формы.

     //Это сделает его видимым

     this.Controls.Add(m_newControl);

    }

    Где рисовать — на экране или вне экрана?

    Создание привлекательной графики — в равной степени и искусство, и ремесло; под этим подразумевается, что одинаково важная роль принадлежит как используемым методикам, так и планированию. Для того чтобы нарисовать на экране одиночный прямоугольник или одиночный фрагмент текста, требуется очень мало времени. Рисование же сложной диаграммы или игрового поля на лету в видимой области экрана, скорее всего, заставят пользователя дожидаться появления результатов в течение некоторого времени. Рассмотрим случай сложной диаграммы, на рисование которой уходит 1,5 секунды. Эта длительность заметно превышает порог человеческого восприятия, и глаз человека в состоянии различить довольно много событий, которые могут произойти на протяжении такого временного промежутка. Если рисование диаграммы состоит из стадий рисования фонового изображения, вычерчивания и сопровождения надписями осей диаграммы, размещения точек, соответствующих фактическим данным, и генерации таблицы ключей, в которой каждый набор данных идентифицируется определенным цветом, то процесс построения диаграммы, происходящий на глазах у пользователя, будет представлять собой весьма неприглядную картину. Результат будет еще более плачевным, если получаемая графика предназначена для динамической игры; в этом случае пользователь столкнется с мерцанием экрана, сопровождающим перерисовку элементов игры при их перемещениях на экране.

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

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

    Как уже обсуждалось в предыдущих разделах, в .NET Compact Framework имеются два неплохих способа переноса внеэкранных рисунков в пользовательский интерфейс. Во-первых, это использование элемента управления PictureBox и задание посредством его свойства Image того битового образа, рисование которого вы только что завершили (например, pictureBox1.Image = myNewBitmap).

    Второй способ заключается в получении объекта Graphics для того элемента интерфейса, в котором он будет прорисовываться (обычно таковым является объект Form) и вызове метода Graphics.DrawImage() для переноса изображения:

    //Получить объект Graphics для формы

    System.Drawing.Graphics gfx;

    gfx = this.CreateGraphics();


    //Нарисовать изображение в объекте с использованием

    //начальной точки с координатами x=10, y=15);

    gfx.DrawImage(myBitmap, 10, 15);

    Следует подчеркнуть, что для метода Graphics.DrawImage() предусмотрено несколько перегруженных версий. Та, которая фигурировала выше, является самой простой и работает быстрее остальных; она просто копирует биты из одного битового образа в другой. Другие перегруженные версии обладают более развитыми возможностями, что позволяет им выполнять такие, например, операции, как перенос в целевое изображение строго определенной области исходного изображения, растягивание или сжатие копируемых изображений или использование масок прозрачности, позволяющих делать прозрачными отдельные области изображения, через которые могут "просматриваться'' другие изображения. Каждая из этих разновидностей метода выполняет определенное преобразование при копировании изображения из объекта-источника в объект назначения. В общем случае, чем сложнее преобразование изображения, которое требует выполнить ваш код, тем дороже оно обходится с точки зрения производительности.

    Для поддержания производительности приложения на приемлемом уровне лучше всего стремиться к тому, чтобы исходное и целевое растровые изображения в максимальной степени совпадали друг с другом. По возможности стремитесь к тому, чтобы при переносе изображений создавались их точные битовые копии, а пиксели не растягивались и не сжимались. Маски прозрачности следует использовать лишь тогда, когда в этом есть необходимость. Хотя к .NET Compact Framework это и не относится, но другие среды выполнения могут предлагать вам выбрать количество цветов, которые должны использоваться в изображении, и указать битовую глубину цвета, используемую для поддержания этой информации. Совпадение этой информации для исходного и целевого изображений способно существенно улучшить производительность. Общая задача заключается в сведении к минимуму любых расхождений между исходным и конечным изображениями, чтобы операция переноса изображения была максимально приближена к операции простого копирования битов в память. Оптимизация таких операций для достижения высокой производительности обычно обеспечивается на всех платформах. 

    Определите собственный процесс визуализации

    Нетривиальным рисункам свойственна сложная структура. Сначала создают прототип, позволяющий строить диаграммы. В этот прототип добавляются средства, позволяющие не только строить диаграммы, но и снабжать надписями оси. Далее добавляются метки делений, позволяющие задавать, где располагаются точки данных и соединяющие их линии. Затем добавляется поддержка одновременного отображения нескольких наборов данных. После этого добавляется таблица цветовых ключей, идентифицирующих различные наборы данных. Далее добавляется поддержка нескольких цветов. Затем добавляется возможность рисования изображений поверх фонового изображения. Наконец, добавляется возможность присвоения диаграммам названий. Код, предназначенный для построения диаграмм, совершенно неожиданно для вас разрастается до огромных размеров, причем каждая отдельная его часть предусматривает выполнение узкоспециализированной задачи, рассматриваемой в отрыве от контекста обеспечения высокой производительности приложения в целом. Тем же недостатком "нашествия возможностей" страдает и код игр, когда в список того, что должно быть сделано для визуализации игрового поля, постоянно добавляются все новые и новые члены.

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

    Рассмотрим следующий пример.

    Не рационализированный процесс построения графиков

    1. Создать пустую битовую карту (150×150 пикселей).

    2. Скопировать фоновое изображение на пустую битовую карту. (Фоновое изображение имеет размеры 150×150 пикселей, непосредственное копирование.)

    3. Вычертить координатные оси. (Создаются и уничтожаются перья Red Pen, Blue Pen и Yellow Pen.)

    4. Рассчитать количество делений на каждой из осей.

    5. Вычертить все линии делений.

    6. Вывести текст для каждого деления. (Создается и уничтожается кисть White Brush, создается и уничтожается шрифт 8 пунктов.)

    7. Нарисовать данные для каждого из наборов данных, для которых строятся графики. (Создаются и уничтожаются перья Red Pen, Orange Pen, Yellow Pen и Green Pen.)

    8. Нарисовать линии между точками.

    9. Вычертить квадраты вокруг каждой точки.

    10. Нарисовать название графика. (Создается и уничтожается кисть White Brush.)

    11. Нарисовать таблицу цветовых ключей, идентифицирующих наборы данных.

    12. Нарисовать рамку. (Создается и уничтожается перо White Pen.)

    13. Нарисовать шаблонные линии для каждого набора данных. (Создаются и уничтожаются перья Red Pen, Orange Pen, Yellow Pen и Green Pen.)

    14. Нарисовать текстовые подписи для каждого набора данных. (Создаются и уничтожаются кисти Red Brush, Orange Brush, Yellow Brush и Green Brush, создается и уничтожается шрифт 8 пунктов.)

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

    Рассмотрим более рациональный вариант организации процесса построения графиков.

    Создание ресурсов

    1. Создать битовую карту (150×150 пикселей), если до этого память для нее не была распределена.

    2. Создать необходимые перья: Red Pen, Orange Pen, Yellow Pen, Green Pen и Blue Pen.

    3. Создать необходимые кисти: White Brush, Red Brush, Orange Brush, Yellow Brush и Green Brush.

    4. Создать необходимые шрифты: шрифт 8 пунктов.

    5. Очистить битовую карту.

    6. Скопировать фоновое изображение на битовую карту.

    7. Вычертить координатные оси.

    8. Рассчитать количество делений на каждой из осей.

    9. Вычертить все линии делений.

    10. Нарисовать текст для каждого деления.

    11. Нарисовать данные для каждого из наборов данных, для которых строятся графики.

    12. Нарисовать линии между точками.

    13. Вычертить квадраты вокруг каждой точки.

    14. Нарисовать название графика.

    15. Нарисовать таблицу цветовых ключей, идентифицирующих наборы данных.

    16. Нарисовать рамку.

    17. Нарисовать шаблонные линии для каждого набора данных.

    18. Нарисовать текстовые подписи для каждого набора данных.

    19. Освободить память, занимаемую перьями, кистями и шрифтами. 

    Отсрочка — зло, используйте предварительные вычисления

    При проектировании высокоуровневого кода пользовательских интерфейсов целесообразно попытаться отложить выполнение некоторых операций на как можно более поздний срок; заполнение элементов управления большими объемами данных, которые, возможно, пользователь никогда не будет просматривать, является бессмысленной тратой процессорного времени. В данном случае стратегия откладывания работы на более поздний срок удачна по той причине, что данные, которыми заполняется интерфейс, являются динамическими, а их возможные объемы часто могут оказываться настолько большими, что заполнять ими интерфейс было бы слишком расточительно. Если же объем информации ограничен и наперед известен, то имеет смысл попытаться заранее вычислить или инициализировать эти данные. Именно с такой ситуацией приходится часто сталкиваться в случае графических данных.

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

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

    Чтобы проиллюстрировать это положение рассмотрим два типичных случая.

    Пример 1: предварительная визуализация объектов бизнес-диаграммы

    На рис. 11.7 приведена довольно красочная картинка, на которой некие (фиктивные) данные отображаются в виде гистограммы. Гистограмма представляет относительные темпы роста экономики различных стран за определенный период времени. Чтобы облегчить визуальное восприятие данных пользователем, было решено представить данные, относящиеся к каждой из стран, в виде столбцов, раскрашенных символами валюты данной страны. Кроме того, для диаграммы предусмотрено фоновое изображение; в данном случае таковым является простой градиентный фоновый цвет, плавно меняющийся от темного к светлому оттенку, но вместо него может быть использовано любое изображение

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

    Рис. 11.7. Гистограмма, представляющая темпы роста экономики различных стран


    Первое, что мы можем сделать в плане оптимизации, — это определить все то, что будет оставаться неизменным в любой диаграмме, и превратить эти объекты в готовый фон. Сказанное иллюстрируется на рис. 11.8. Мы решили заранее нарисовать на фоновой битовой карте градиентную заливку фоновым цветом, вертикальные и горизонтальные линии и часть названия диаграммы. Было решено не включать в фоновое изображение следующие данные: собственно столбцы гистограммы, названия стран и их позиции на гистограмме, числовые метки для каждого деления на вертикальной шкале, текст над каждым из столбцов гистограммы и заполнение самих столбцов. Все эти объекты должны быть динамически сгенерированы во время выполнения приложения.

    Рис. 11.8. Заблаговременно подготовленное фоновое изображение для гистограммы


    Теперь нам предстоит принять очень важное решение относительно того, какому варианту отдать предпочтение: однократно прорисовывать фоновый битовый образ при каждом запуске приложения или же создать это изображение на стадии проектирования, скомпилировать в виде ресурса приложения и только загружать его на стадии выполнения. Если такое растровое изображение является статическим или необходимо иметь всего лишь несколько его различных вариантов, то, вероятно, имеет смысл подготовить их на настольном компьютере и включить в приложение как двоичный ресурс. Дополнительным преимуществом проведения такой подготовительной работы на стадии проектирования является то, что мы можем поручить ее профессиональному художнику-графику, который выполнит работу на высоком эстетическом уровне. Как бы то ни было, в любом случае мы сэкономим массу процессорного времени, поскольку каждый раз, когда будет требоваться повторная визуализация диаграммы, мы уже будем располагать ее заблаговременно подготовленной сложной частью, кэшированной для нужд приложения.

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

    В соответствии с нашими планами относительно исходного варианта реализации мы могли бы нарисовать прямоугольники для каждого столбца и вывести над каждым из них соответствующий текст, но это было бы нерациональной тратой времени. Существует также проблема визуализации лишь части линий текста. Задача визуализации символов $$$ на 38 процентов высоты или символов €€€ на 76 процентов высоты средствами каркаса приложения может оказаться не столь уж простой. Учитывая сложность реализации этого подхода, от использования символов валюты можно было бы вообще отказаться, но это был бы позорный поступок с нашей стороны, поскольку наличие обозначений валюты на столбцах упрощает установление соответствия между странами и столбцами и поэтому снижает вероятность неправильного истолкования данных пользователем. Поэтому от всех этих "украшений" мы не отказываемся, но и платить за них лишнее не собираемся.

    В качестве оптимизации первого порядка мы могли бы создать битовые образы строк символов валюты для каждой страны, то есть растровое изображение 30×10 пикселей для строки $$$ и аналогичные изображения для других видов валют. Вероятно, такие битовые образы можно было бы очень быстро копировать поверх нашего фонового изображения, предусмотрев для этого цикл, который выполнялся бы столько раз, сколько необходимо, с последующим отсечением верхушки символов в соответствующих точках, чтобы обеспечить нужную высоту столбцов. В действительности, мы можем поступить еще лучше. Почему бы не заготовить заранее битовые образы, представляющие для каждой валюты столбцы максимальной высоты? Всего нам нужно четыре таких заготовки: одна для США (US), одна для Японии (Japan), одна для Великобритании (UK) и одна для стран Европейского Союза (EU). Когда нашему приложению необходимо нарисовать любой из этих столбцов, оно просто использует соответствующий битовый образ и копирует его часть на поверхность нашего рисунка. Как и в случае фонового изображения, если имеется возможность создавать изображения на стадии проектирования, то можно прибегнуть к услугам дизайнеров, которые придадут изображениям максимально привлекательный внешний вид. Пример таких заранее заготовленных столбцов представлен на рис. 11.9.

    Рис. 11.9. Заранее заготовленные декорированные столбцы гистограмм


    Какой дополнительный объем памяти потребуется для реализации такого подхода? Предположим, что максимальная высота столбцов составляет 180 пикселей, а их ширина — 32 пикселя. Пусть, далее, для хранения информации о цвете каждого пикселя требуется 4 байта. Тогда для хранения в памяти битового образа одного столбца диаграммы потребуется 180×32×4 = 23040 байт, или 22,5 Кбайт. В зависимости от ситуации такой объем может быть для нас как приемлемым, так и неприемлемым. Насколько велик этот объем по сравнению с теми объемами памяти, которые требуются для хранения других объектов? Если предположить, что размеры фонового изображения составляют 200×200 пикселей, то для него потребуется 200×200×4 = 156,25 Кбайт памяти. 

    Для построения окончательного изображения диаграмм нам потребуется еще одна битовая карта такого же размера, на которую будут копироваться графические данные заднего и переднего планов; в результате этого суммарный необходимый объем памяти достигает 312,5 Кбайт, что не так уж мало, но вполне приемлемо для большинства мобильных устройств. Битовые образы всех четырех столбцов суммарно потребуют 22,5×4=90 Кбайт, что значительно меньше того, что требуется для фонового изображения и пустой битовой карты, на которой все наши изображения будут объединены в одно целое. Учитывая тот факт, что, затрачивая 90 Кбайт дополнительной памяти, мы избавляемся от необходимости многократно создавать в памяти копии небольших изображений или каждый раз рисовать текст поверх столбцов, такую оптимизацию, по всей видимости, можно считать эффективной.

    Пример 2: предварительная визуализация объектов для сюжетной игры

    Любопытно отметить, что оптимизация визуализации изображений для сюжетных игр имеет много общего с оптимизацией этого процесса, которую мы рассмотрели на примере диаграмм. На рис. 11.10 представлено изображение для сюжетной игры, которую я написал для .NET Compact Framework, чтобы продемонстрировать концепции графики.

    HA ЗАМЕТКУ

    Те, кого интересует исходный текст этого приложения, могут найти его на Web-сайте. Приложение было написано на языке VB.NET и названо "HankTheCaveMan" ("Пещерный человек Хэнк"). Оно позволяет продемонстрировать многие из концепций графики, которые обсуждаются в этой главе. Полный исходный код приложения доступен для загрузки на сайте обмена исходными кодами www.gotdotnet.com вместе с бесчисленным множеством других примеров и информации.

    Рис. 11.10. Сюжетная игра, написанная для .NET Compact Framework


    Существует три слоя изображений для игр, которые должны визуализироваться на экране:

    1. Фоновая картинка. В играх предусматривается статическая фотография, используемая в качестве фонового изображения.

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

    3. Динамический передний план. К этой категории относятся активные экранные изображения, которые могут изменяться от кадра к кадру. В нашей игре они представляют пещерного человека, пещерную женщину, два валуна, птицу и четыре факела. Эту разновидность графических элементов обычно называют "спрайтами". Кроме того, к динамическому переднему плану относятся индикатор запаса энергии, который находится в верхней части игрового поля слева, и индикатор Score/Bonus (Счет/Бонус), расположенный в верхней части игрового поля справа. В программе все эти элементы представлены объектами одной коллекции

    Циклу визуализации предшествует загрузка фонового изображения и создание на нем рисунка настилов и лестниц. Эти изображения комбинируются в памяти и xpaнятся как наше фоновое изображение. Настилы и лестницы не изменяются при переходе от кадра к кадру, поэтому нет смысла визуализировать их вместе с каждым кадром. Поскольку фоновое изображение уже и так должно загружаться, мы не занимаем лишнюю память, рисуя на нем статическое игровое поле.

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

    Когда выполняется визуализация кадра, то сначала в целевую битовую карту копируется фоновое изображение. После этого каждому из объектов визуализируемой коллекции направляется запрос на то, чтобы он визуализировал себя на целевой битовой карте. Некоторые из этих объектов являются текстовыми; такие объекты для прорисовки своего текста на целевой битовой карте вызывают метод Graphics.DrawString(). Одним из нетекстовых объектов является "энергетическая шкала", которая состоит из двух прямоугольников — внешней рамки и внутреннего закрашенного прямоугольника. Остальные объекты являются многокадровыми растровыми изображениями, то есть несколько таких изображений представляют различные возможные состояния спрайта; это обеспечивает возможность анимации объектов путем быстрого переключения между их различными изображениями.

    С Хэнком (пещерным человеком) ассоциированы 8 изображений; 2 — для бега влево, 2 — для бега вправо, 2 — для подъема и 2 — для спуска. Для каждого спрайта предусмотрена внутренний конечный автомат, который следит за тем, какое изображение должно отображаться в процессе визуализации. Изображения имеют небольшие размеры; размеры изображения Хэнка составляют 21×35 пикселей, или, округляя в большую сторону до ближайшего кратного 4 (чтобы учесть возможное выравнивание границ размещения объектов в памяти), 24×36 = 864 пикселя. Всего для одного изображения требуется 864×4 байт/пиксель, что составляет примерно 3,5 Кбайт. Для загрузки всех 8 изображений Хэнка в память потребуется порядка 28 Кбайт, что вполне приемлемо.

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

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

    Кэшируйте часто используемые ресурсы

    При написании кода для обработки графики часто оказывается так, что приложению необходимо многократно использовать одни и те же ресурсы. К числу распространенных классов, допускающих повторное использование, относятся битовые изображения (Bitmap), перья (Pen), кисти (Brush) и шрифты (Font). Было бы слишком расточительно многократно загружать или создавать одни и те же ресурсы; на повторную загрузку ресурса из хранилища или его повторный запрос у операционной системы затрачивается дополнительное процессорное время. Не меньшим расточительством является и одновременная загрузка эквивалентных ресурсов в память; например, хранить в памяти несколько экземпляров одного и того же битового изображения — напрасная трата ресурсов, если вместо этого можно обойтись совместным использованием единственного экземпляра. Наконец, освобождение памяти, занимаемой ресурсами, также ложится бременем на систему и снижает ее производительность. Чтобы избежать подобного снижения производительности, иногда оказывается полезным применять в приложении глобальный диспетчер ресурсов, который берет на себя все заботы, связанные с загрузкой, кэшированием и удалением часто используемых ресурсов.

    В листинге 11.11 представлен пример, демонстрирующий три подхода к загрузке и обслуживанию ресурсов в памяти. 

    ■ Подход 1, основанный на принципе "открытия задвижки " (Litch-based approach). Любой код, которому требуется общий ресурс, обращается к статическому свойству класса GraphicsGlobals для получения его значения. Если ресурс уже загружен, он возвращается запросившему его объекту. Если ресурс еще не был загружен, то он создается, кэшируется и после этого возвращается пользователю. Такой подход обладает двумя преимуществами: 1) запрашивающий объект не должен заботиться ни о каком инициализирующем коде; в результате запроса всегда будет возвращен действительный ресурс, и 2) управляемый ресурс может быть освобожден, если приложение считает, что в течение некоторого времени в нем не будет надобности; при необходимости он будет заново создан в результате следующего запроса. Единственным недостатком такого подхода являются незначительные дополнительные накладные расходы, связанные с необходимостью вызова функции для доступа к свойству всякий раз, когда требуется ресурс; обычно этими накладными расходами можно пренебречь. 

    ■ Подход 2, основанный на групповой обработке ресурсов (batch-based approach). Если некоторые ресурсы используются одинаковым образом и имеют сравнимые времена существования, то они могут быть инициализированы сразу все вместе. Код, в котором должны будут использоваться эти глобальные ресурсы, может непосредственно получить доступ к переменным, но предварительно он должен удостовериться в том, что они уже были инициализированы. Когда приложение перейдет в состояние, в котором эти ресурсы в течение некоторого времени использоваться не будут, эти ресурсы должны быть освобождены, и для них должен быть вызван метод Dispose(). 

    ■ Подход 3, основанный на использовании коллекций (collection-based approach). Если некоторые ресурсы всегда используются в виде группы, как, например, в случае битовых изображений, образующих анимационную последовательность, то имеет смысл загружать их вместе и возвращать в виде массива или коллекции ресурсов. Если загрузка ресурсов обходится слишком дорого или требует использования значительных объемов памяти, то может оказаться целесообразным хранить их экземпляры, кэшированные централизованным образом, чтобы исключить повторное создание одних и тех же ресурсов. Как и в рассмотренном выше случае, когда необходимость в этих ресурсах отпадает, их необходимо освобождать и вызывать для них метод Dispose(), следуя предварительно намеченной стратегии

    Листинг 11.11. Три полезных способа кэширования графических ресурсов

    using System;

    using System.Drawing;

    internal class GraphicsGlobals {

     //==========================================================================

     //Подход 1: Создать ресурс по требованию

     // и кэшировать его для последующего использования.

     //

     //Внешний код получает доступ к общедоступным свойствам для их просмотра, но

     //сами переменные остаются внутренними переменными класса

     //==========================================================================

     private static Pen s_bluePen;

     public static Pen globalBluePen {

      get {

       //Если перо еще не было создано

       if (s_bluePen == null) {

        s_bluePen =new System.Drawing.Pen(System.Drawing.Color.Blue);

       }

       return s bluePen;

      }

     } //Конец свойства

     //=========================================================

     //Подход 2:

     //Загрузить глобально и кэшировать все

     //используемые объекты Pen, ImageAttribute, Font и Brush.

     //

     //Внешний код получает доступ ко всем общедоступным членам,

     //так что никакие функции доступа не нужны.

     //=========================================================

     public static Pen g_blackPen;

     public static Pen g_whitePen;

     public static System.Drawing.Imaging.ImageAttributes g_ImageAttribute;

     private static bool s_alreadyInitialized;

     public static Font g_boldFont;

     public static Font g_smallTextFont;

     public static Brush g_greenBrush;

     public static Brush g_yellowBrush;

     public static Brush g_redBrush;

     public static Brush g_blackBrush;


     //==============================================================

     //Эта функция должна быть вызвана до попыток доступа к любому из

     //вышеперечисленных глобальных объектов

     //==============================================================

     public static void InitializeGlobals() {

      if (s_alreadyInitialized == true) {

       return;

      }

      g_blackPen = new System.Drawing.Pen(Color.Black);

      g_whitePen = new System.Drawing.Pen(Color.White);

      g_ImageAttribute = new  System.Drawing.Imaging.ImageAttributes();

      g_ImageAttribute.SetColorKey(Color.White, Color.White);

      g_boldFont = new Font(FontFamily.GenericSerif, 10, FontStyle.Bold);

      g_smallTextFont = new Font(FontFamily.GenericSansSerif, 8, FontStyle.Regular);

      g_blackBrush = new SolidBrush(System.Drawing.Color.Black);

      g_greenBrush = new SolidBrush(System.Drawing.Color.LightGreen);

      g_yellowBrush = new SolidBrush(System.Drawing.Color.Yellow);

      g_redBrush = new SolidBrush(System.Drawing.Color.Red);

      s_alreadyInitialized = true;

     }


     //=========================================================

     //Подход 3: Возвратить массив связанных ресурсов.

     // Кэшировать ресурсы локально, чтобы при многократных

     // запросах не загружались (понапрасну) их дубликаты.

     //=========================================================

     private static Bitmap m_CaveMan_Bitmap1;

     private static Bitmap m_CaveMan_Bitmap2;

     private static Bitmap m_CaveMan_Bitmap3;

     private static Bitmap m_CaveMan_Bitmap4;

     private static System.Collections.ArrayList m_colCaveManBitmaps;


     //--------------------------------------------------

     //Создать и загрузить массив изображений для спрайта

     //--------------------------------------------------

     public static System.Collections.ArrayList g_CaveManPictureCollection() {

      //Изображения загружаются лишь в том случае, если мы их еще не загрузили

      if (m_CaveMan_Bitmap1 == null) {

       //-----------------------------------------------------------------

       //Загрузить изображения. Эти изображения хранятся в виде

       //встроенных ресурсов в нашем двоичном приложении.

       //

       //Загрузка изображений из внешних файлов осуществляется аналогичным

       //образом, но выполнить ее проще (нам достаточно лишь указать

       //имя файла в конструкторе растровых изображений).

       //------------------------------------------------------------------


       //Получить ссылку на нашу двоичную сборку

       System.Reflection.Assembly thisAssembly = System.Reflection.Assembly.GetExecutingAssembly();

       //Получить имя сборки

       System.Reflection.AssemblyName thisAssemblyName = thisAssembly.GetName();

       string assemblyName = thisAssemblyName.Name;


       //Загрузить изображения в виде двоичных потоков из нашей сборки

       m_CaveMan_Bitmap1 = new System.Drawing.Bitmap(

        thisAssembly.GetManifestResourceStream(

        assemblyName + ".Hank RightRunl.bmp"));

       m_CaveMan_Bitmap2 = new System.Drawing.Bitmap(

        thisAssembly.GetManifestResourceStream(

        assemblyName + ".Hank_RightRun2.bmp"));

       m_CaveMan_Bitmap3 = new System.Drawing.Bitmap(

        thisAssembly.GetManifestResourceStream(

        assemblyName + ".Hank_LeftRun1.bmp"));

       m_CaveMan_Bitmap4 = new System.Drawing.Bitmap(

        thisAssembly.GetManifestResourceStream(

        assemblyName + ".Hank_LeftRun2.bmp"));


       //Добавить их в коллекцию

       m_colCaveManBitmaps = new System.Collections.ArrayList();

       m_colCaveManBitmaps.Add(m_CaveMan_Bitmap1);

       m_colCaveManBitmaps.Add(m_CaveMan_Bitmap2);

       m_colCaveManBitmaps.Add(m_CaveMan_Bitmap3);

       m_colCaveManBitmaps.Add(m_CaveMan_Bitmap4);

      }

      //Возвратить коллекцию

      return m_colCaveManBitmaps;

     }

    } //Конец класса

    Старайтесь избегать распределения памяти для объектов при выполнении повторяющихся или непрерывно продолжающихся операций рисования

    Выполнение лишних операций размещения и уничтожения объектов в памяти является одной из наиболее распространенных причин ухудшения производительности графического кода. Графический код часто выполняется внутри циклов или многократно вызывается. Кроме того, поскольку графические объекты используют значительные объемы памяти, а также системные ресурсы, с их обслуживанием связаны сравнительно большие накладные расходы. Если ваше мобильное приложение распределяет и освобождает память всякий раз, когда необходимо визуализировать изображения, то вы неминуемо столкнетесь с проблемами нехватки памяти, что обусловит необходимость частого выполнения операций по сборке мусора, замедляющих работу приложения. В связи с этим необходимо предельно внимательно анализировать любые участки кода, требующие распределения памяти для объектов в процессе визуализации графики. Особенно это касается объектов, связанных с графическими ресурсами, но остается справедливым и по отношению к объектам другой природы (например, коллекциям, массивам, строкам), которые могут фигурировать в таком коде.

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

    ■ Объекты Bitmap. Часто возникают ситуации, в которых в качестве временного хранилища для изображения или части изображения, создаваемых вашим приложением, удобно использовать объект Bitmap. Если вашему приложению требуется пространство для рисования, то в этом нет ничего необычного; единственное, чего следует избегать — так это создания и уничтожения многих битовых изображений в циклах рисования. Обычно гораздо лучше иметь одно рабочее пространство, многократно используемое в качестве общего ресурса, чем нести дополнительные затраты, связанные с непрерывным созданием и уничтожением временных изображений. Размер такой временной памяти должен быть равен размеру наибольшего временного изображения, которое может понадобиться вашим процедурам (но не более того). Если временную память необходимо очищать, прежде чем использовать ее для рисования, то это можно сделать легко и быстро при помощи вызова Graphics.Clear(). Кроме того, как продемонстрировано в приведенном выше примере кода, часто используемые растровые изображения имеет смысл загружать и кэшировать в памяти. Следите за тем, чтобы в каждый момент времени загружался только один экземпляр растрового изображения. Загрузка нескольких экземпляров идентичных изображений приведет к напрасному расходованию больших объемов памяти. 

    ■ Объекты Graphics. Если требуется постоянно перерисовывать растровое изображение, как это, например, приходится делать при перерисовке кадров игрового поля, то, вероятно, наряду с целевым экранным изображением целесообразно кэшировать также объекты Graphics внеэкранных изображений. Поступая таким образом, вы избавитесь от необходимости непрерывного размещения этих объектов в памяти и их удаления. Не распределяйте в циклах рисования память для объектов Graphics растровых изображений или форм более одного раза; в идеальном случае следует вообще избегать размещения и удаления таких объектов внутри циклов. To же самое касается и любых других битовых карт, в которых вы должны выполнять рисование; кэшируйте их объекты Graphics, а не создавайте их каждый раз заново. Учтите, что объекты Graphics нужны только для тех битовых карт, которые вы используете для создания изображений; если битовая карта используется лишь в качестве источника информации об изображении, то объект Graphics для нее вам вообще не нужен. 

    ■ Объекты Font, Brush, Pen, ImageAttribute. Распространенной ошибкой разработчиков, причины которой вполне объяснимы, является их чрезмерное увлечение управлением временем жизни ресурсов на микроуровне, не сопровождающееся глубоким анализом их использования на макроуровне. В процессе создания графического изображения вам может потребоваться пройти, скажем, через 30 отдельных этапов, в ходе которых рисуются линии, закрашиваются эллипсы, создается текст и копируются битовые образы. Выполнение каждой из этих операций требует использования некоторой комбинации перьев, кистей, шрифтов, то есть объектов Pen, Brush, Font, а если в дело включаются еще и маски прозрачности, то и объектов ImageAttribute. В случае мобильных устройств ситуация усугубляется тем, что в .NET Compact Framework, в отличие от .NET Framework, статические версии базовых кистей и перьев не предусмотрены. Так, в версии NET Framework, ориентированной на настольные компьютеры, существуют, например, объекты System.Drawing.Pens.Blue и System.Drawing.Brushes.DarkOrange, но в NET Compact Framework эти объекты приходится распределять. Решение этой проблемы заключается в создании собственного глобального набора объектов Pen и Brush, который вы будете использовать для нужд рисования во всем приложении. Вы должны тщательно просмотреть все циклы рисования в приложении, обращая особое внимание на случаи повторного создания идентичных ресурсов и избавляясь от подобной избыточности в коде. Если в вашем приложении непрерывно выполняются операции рисования, то такие объекты, как шрифты, перья, кисти и маски прозрачности, должны либо однажды распределяться в цикле рисования, либо глобально кэшироваться. 

    ■ Типы, которые преобразуются (или, как еще говорят, "'упаковываются") в объекты. Наиболее распространенным типом значений, используемым в графических кодах, является структура System.Drawing.Rectangle. Обычно работа с типами, соответствующими значениям, отличается высокой эффективностью, поскольку их можно размещать в стеке (а не в глобальном пуле свободной памяти, или куче, как объекты). В то же время, значения также могут рассматриваться как объекты и размещаться в массивах или коллекциях или передаваться всюду, где допускается использование объектного типа. Тщательно следите за тем, чтобы не происходило неявное постоянное распределение и освобождение памяти, обусловленное "упаковкой" значений в объекты и их обратной "распаковкой".

    Заботясь о том, чтобы в графическом коде приложения не выполнялось непрерывное создание и уничтожение часто используемых объектов, вы не должны забывать о необходимости следить за использованием глобальной памяти своего мобильного приложения. Если в графических функциональных возможностях нуждаются лишь отдельные части приложения, то следует подумать об освобождении занимаемых ими ресурсов, как только приложение входит в состояние, в котором в течение некоторого времени графические ресурсы ему не будут нужны. Хорошей практикой проектирования считается использование конечного автомата и определение тех графических ресурсов, от которых при выходе приложения из определенных состояний можно освободиться.

    Резюме 

    От того, что вы напишете код, обеспечивающий отличную реакцию пользовательского интерфейса, алгоритмы вашего приложения работать быстрее не станут. Это никоим образом не ускорит победный марш разработки приложения "до полного завершения". Более того, код приложения может даже усложниться. Единственная причина, по которой следует заботиться о высокой реактивности интерфейса и стремиться к высокой производительности его кода, заключается в том, чтобы создать для пользователя более комфортные условия, но разве ради этого не стоит потрудиться? Если вы хотите, чтобы пользователь поставил самую высокую оценку вашему приложению, вы не должны жалеть времени на доводку и отшлифовку таких вещей, как способность интерфейса к отклику. Как и в случае других аспектов производительности, выполнение этой работы нельзя откладывать до завершающих этапов процесса разработки. В конце разработки вы еще смогли бы добавить что-то наподобие курсоров ожидания в тех точках, где приложение может тормозиться, но это все равно, что красиво покрасить фасад плохо построенного дома — это лучше, чем вообще ничего, но, в сущности, не так уж и много. Никогда не забывайте о необходимости обеспечения высокой интерактивности пользовательского интерфейса и всегда в явном виде включайте это требование в качестве критерия прохождения каждой контрольной точки разработки. Лучше всего решать подобные проблемы сразу же после их выявления, пока они не успели накрепко зацементироваться в структуру приложения.

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

    Изучайте свойства и методы элементов управления, которые вы используете. Для элементов управления часто предусматриваются специальные возможности оптимизации производительности. Внимательно анализируйте события, на которые реагирует ваше приложение. В частности, вы должны постараться хорошо представлять себе и экспериментально проверять, когда именно запускаются события, на которые приложению приходится реагировать. В процессе просмотра кода события могут казаться вам простыми и понятными, но они представляют ту область, в которой вы, как разработчик, вынуждены передавать управление выполнением приложения операционной системе. Именно операционная система и каркас пользовательского интерфейса определяют, когда именно будет выполняться код вашего обработчика события; это может случаться гораздо чаще, чем вы думаете, или происходить неожиданным для вас образом при обработке сообщений операционной системой. Между событиями, свойствами и методами элементов управления могут существовать такие тонкие виды взаимодействия, о которых вы даже не догадываетесь. Непредусмотренная и не являющаяся необходимой обработка событий пользовательского интерфейса порождает проблемы производительности и усложняет работу с приложением. Будьте особенно внимательны в тех случаях, когда существует возможность повторного входа в обработчик событий.

    При работе с растровыми изображениями существенное значение имеют их размеры. Современные цифровые изображения с высоким разрешением занимают очень много места как на диске, так и в памяти. Цифровые фотографии, а также битовые образы, генерируемые на настольных компьютерах, представляют собой изображения, разрешение которых намного превышает возможности мобильных устройств. Никогда не забывайте о размерах экранов устройств, с которыми вы работаете, и размерах переносимых на них битовых образов; они должны соответствовать друг к другу. Для изображений, размеры которых довольно велики, неплохо использовать сжатые форматы хранения данных. В случае изображений, допускающих применение сжатие с потерями, рекомендуется использовать формат JPEG, тогда как в случае изображений, требующих полной достоверности, более подходящим является формат PNG. Помните, что, независимо от эффективности используемого алгоритма сжатия, занимаемый загруженным изображением объем памяти определяется размерами самого изображения.

    Обработку графики следует продумывать независимо от кодирования высокоуровневого пользовательского интерфейса и оптимизировать отдельно. Оптимизация графического кода, предназначенного для рисования изображений, требует применения иных методик, нежели настройка кода высокоуровневого пользовательского интерфейса, и требует особого искусства. Поскольку графические изображения, генерируемые вашим мобильным приложением, обычно должны сосуществовать с элементами высокоуровневого пользовательского интерфейса, следует продумать, каким образом следует организовать взаимодействие графического кода с пользовательским интерфейсом; для этого существует несколько различных моделей. Для такого взаимодействия целесообразно использовать самый высокий из возможных уровней абстракции. Если вы генерируете статические изображения, то одним из приемлемых и концептуально несложных способов реализации указанного подхода может быть создание битового образа в памяти и его назначение свойству Image элемента управления PictureBox. Если требуется непрерывное обновление изображения, то приложение может создавать изображение во внеэкранном буфере и периодически копировать кадры на поверхность формы подобно кинопроектору. Пользовательские элементы управления особенно удобно использовать в тех случаях, когда требуется высокая степень интерактивности взаимодействия пользователя с создаваемой графикой, например, диаграммой, позволяющей переходить с одного уровня детализации данных, которые она представляет, на другой. Кроме того, пользовательские элементы управления могут генерировать пользовательские события, которые ваше приложение может перехватывать. Каждая из вышеперечисленных стратегий интеграции пользовательского интерфейса характеризуется своими видами взаимодействий, для которых она больше всего приспособлена; при этом очень важно явно сформулировать, какие именно задачи должна решать графика в мобильном приложении, и выбрать наиболее подходящую стратегию решения этих задач.

    Работая с графическим кодом, вы должны хорошо представлять себе систематическую картину того, каким образом в приложении осуществляется визуализация изображений. При этом очень важно "не оказаться крохобором в мелочах, бездумно транжирящим крупные суммы" в обращении с ресурсами. Если имеются такие объекты Bitmap, Graphics, Pen, Brush, Font или ImageAttribute, которые будут использоваться во всех операциях рисования в приложении, то вы должны разработать систему, в задачи которой входит создание и кэширование указанных ресурсов. Применение предварительно создаваемых изображений сопряжено с дополнительным расходом ценной памяти, однако оно чрезвычайно ускоряет операции рисования; при разумном подходе это позволяет создавать приложения с привлекательным интерфейсом, сохраняющие высокую производительность. Исключайте случаи одновременной загрузки двух идентичных ресурсов — подобная расточительность никому не нужна.

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

    Создание многофункциональных графических интерфейсов для мобильных устройств — вполне осуществимая задача, решение которой способствует повышению комфортности условий работы пользователя с вашим приложением. При построении графических пользовательских интерфейсов очень важно придерживаться подхода, являющегося одновременно творческим, аналитическим и систематическим. Сочетание творческого воображения художника, умеющего представлять себе то, чего еще не существует, аналитических способностей инженера, умеющего решать технические задачи, и проницательного взора бухгалтера, который не допустит неоправданных затрат, — вот что необходимо для того, чтобы ваше мобильное приложение было оценено конечными пользователями как высокопроизводительное, а взаимодействие с его привлекательным интерфейсом доставляло им одно удовольствие. Работа с графикой потребует от вас напряжения всех сил, но одновременно доставит огромное удовольствие; никогда не забывайте о производительности, и тогда выше вас — только небо!







     


    Главная | В избранное | Наш E-MAIL | Добавить материал | Нашёл ошибку | Наверх