company_banner

UWP beginner: Адаптивный дизайн (VB.NET + C#)

  • Tutorial
Мы продолжаем историю по разработке под универсальную платформу Windows (UWP). Тема этой статьи родилась из-за большого количества вопросов по ней к автору от независимых разработчиков UWP-приложений. Для кого-то она может показаться вполне очевидной, но мы надеемся, что вы найдете в статье полезный лайфхак по адаптивному дизайну в UWP.

Статья подготовлена совместно с активным участником сообщества Microsoft Developer, Алексеем Плотниковым, и менеджером по работе с техническими аудиториями, Стасом Павловым.



Первая истина, которую должен запомнить любой разработчик UWP-приложений – «мысли как пользователь». Вторая истина – «без адаптивного дизайна приложение обречено на медленную и мучительную смерть», так как количество устройств и расширений экранов просто огромно. Первое и самое частое решение для создания адаптивного дизайна, которое сразу приходит в голову – это адаптивные триггеры (AdaptiveTrigger).

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

<Grid x:Name="LayoutRoot">
    <VisualStateManager.VisualStateGroups>
        <VisualStateGroup>
            <VisualState x:Name="WideState">
                <VisualState.StateTriggers>
                    <AdaptiveTrigger MinWindowWidth="600" />
                </VisualState.StateTriggers>
                <VisualState.Setters>
                    <Setter Target="LayoutRoot.Background" Value="Green" />
                </VisualState.Setters>
            </VisualState>
            <VisualState x:Name="NarrowState">
                <VisualState.StateTriggers>
                    <AdaptiveTrigger MinWindowWidth="0" />
                </VisualState.StateTriggers>
                <VisualState.Setters>
                    <Setter Target="LayoutRoot.Background" Value="Red" />
                </VisualState.Setters>
            </VisualState>
        </VisualStateGroup>
    </VisualStateManager.VisualStateGroups>
</Grid>

Запустите проект и посмотрите, как при изменении ширины окна будет меняться цвет: с зеленого на красный и обратно.



Если вы не понимаете, что тут происходит, то для начала вам нужно познакомиться с понятием адаптивных триггеров. Хороший курс по адаптивному дизайну и адаптивным триггерами доступен в Microsoft Virtual Academy. К слову, не лишним будет пройти его весь – это существенно расширит ваши знания в вопросе разработки UWP-приложений.

Приятная особенность адаптивных триггеров заключается в том, что нам не нужно выполнять никаких действий в коде и это упрощает разделение труда между дизайнером и разработчиком, и в дополнении делает проект независимым от используемого языка программирования (C# или VB.Net).

С первого взгляда все может показаться крайне простым и очевидным, но начиная применять полученные выше знания, вы столкнетесь с целым рядом вытекающих вопросов и сложностей. Первое, что приходит в голову – «А какую ширину окна выбрать для, того, чтобы на экране телефона видеть подходящее построение макета?» Этот вопрос может показаться глупым, а ответ вполне простым – выберем наибольшее значение из минимальных. Например, в коде выше задана ширина 600 px, а в других примерах и подобных статьях часто встречается 720 px. Давайте зададим 720 px, и тогда все устройства ширина экрана которых будет либо меньше, либо равна 720 px, будут иметь нужное нам представление. И вы будете правы ровно до того момента, пока не повернете ваше устройство в горизонтальное положение (помните первую истину?). Для кого-то, возможно, это будет открытием, но существует устройства, в которых высота экрана меньше 720 px. Повернув устройство в горизонтальное положение, высота превратится в ширину, триггер выдаст нам представление для узкого по ширине окна, что полностью сломает наш макет.

Для полного понимания картины рассмотрим пример выше со следующей модификацией:

   <Grid x:Name="GlobalGrid">
        <VisualStateManager.VisualStateGroups>
            <VisualStateGroup>
                <VisualState x:Name="WideState">
                    <VisualState.StateTriggers>
                        <AdaptiveTrigger MinWindowWidth="720" />
                    </VisualState.StateTriggers>
                    <VisualState.Setters>
                        <Setter Target="MyStackPanel.Orientation" Value="Horizontal" />
                    </VisualState.Setters>
                </VisualState>
                <VisualState x:Name="NarrowState">
                    <VisualState.StateTriggers>
                        <AdaptiveTrigger MinWindowWidth="0" />
                    </VisualState.StateTriggers>
                    <VisualState.Setters>
                        <Setter Target="MyStackPanel.Orientation" Value="Vertical" />
                    </VisualState.Setters>
                </VisualState>
            </VisualStateGroup>
        </VisualStateManager.VisualStateGroups>
        <StackPanel x:Name="MyStackPanel" HorizontalAlignment="Center" VerticalAlignment="Center">
            <TextBlock Text="77" FontSize="100"/>
            <TextBlock Text="77" FontSize="100"/>
            <TextBlock Text="77" FontSize="100"/>
        </StackPanel>
    </Grid>

Запустите проект на самом первом эмуляторе из списка отладки «Mobile Emulator 10.xxx. WVGA 4 inch 512MB» и переведите его в горизонтальный режим. Как видите StackPanel остался в вертикальной ориентации, хотя более логичным было бы видеть горизонтальную. Триггер не отработал, потому что и ширина, и высота устройства в нашем случае меньше 720 px.

Хорошо, тогда давайте выберем меньшее значение для триггера, например, 500. Но и тут мы получаем ту же проблему, но уже на современных устройствах, где ширина экрана в вертикальной ориентации больше 500. На самом деле, чтобы воспроизвести проблему, нам даже не нужен эмулятор устройства с маленьким разрешением. Достаточно запустить приложение на ПК и сделать размер окна минимальным по высоте, а затем ширину уменьшить до достижения значения из триггера. StackPanel снова выглядит не так как нам хотелось бы. Кстати, если эта проблема вам кажется не значительной, то вспомните, что в Windows 10 на ПК мы получили возможность закреплять сразу четыре окна на экране, что дает им как раз такие пропорции.



Перейдем к решению. Показанный выше пример со StackPanel – это не просто пример ради примера, а главная часть рабочего проекта, в котором обозначенная проблема была краеугольным камнем. Для понимания, о чем идет речь, рассмотрим приложение «Рубль Life» от соавтора статьи, в котором отображаются курсы валют в реальном времени.

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



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

VB.NET

Private Sub GlobalGrid_SizeChanged(sender As Object, e As SizeChangedEventArgs)
        If e.NewSize.Height > e.NewSize.Width Then
            MyApplicationData.Orientation = Vertical
        Else
            MyApplicationData.Orientation = Horizontal
        End If
End Sub

C#

  private void GlobalGrid_SizeChanged(object sender, SizeChangedEventArgs e)
        {
      
            if (e.NewSize.Height > e.NewSize.Width)
                MyApplicationData.Orientation = Orientation.Vertical;
            else
                MyApplicationData.Orientation = Orientation.Horizontal;
        }

В данном примере соответствующее значение присваивается свойству собственного класса. Далее все что нужно, это осуществить привязку свойства Orientation у StackPanel к соответствующему свойству в нашем классе. Вы так же можете сохранить преимущества задания значений в XAML и вместо установки свойств класса, осуществить переход к нужному визуальному состоянию с помощью VisualStateManager.GoToState().

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

Кстати при изучении приложения «Рубль Life», можно так же заметить, что при изменении соотношения сторон, происходит и другие изменения в интерфейсе. Например, если запустить приложение на ПК и развернуть на полный экран, то кнопки приложения будут располагаться сверху и снизу, занимая свободное пространство, а если сжать окно, то кнопки скроются и отобразится CommandBar с набором тех же действий. Напротив, если запустить приложение на телефоне, то мы сначала увидим представление с CommandBar, а повернув телефон, представление с кнопками. Такой подход является воистину универсальным и позволяет пользователю получить предпочтительную версию интерфейса, просто изменив размер окна или повернув устройство.



Что же со StackPanel это действительно работает, скажете вы, а как быть с более сложными интерфейсами? Вы удивитесь, но такой подход можно применять и на более сложных интерфейсах комбинированием реакции на изменение соотношения сторон и использованием адаптивных триггеров. Например, у вас есть группа объектов, которая формирует высоту в 600 px. Как бы вы не реагировали на изменения соотношения сторон, это не поможет вам сделать эту группу объектов, удобной для восприятия, если высота окна или высота экрана телефона в горизонтальном положении меньше 600 px. Как раз для таких ситуаций и нужны адаптивные триггеры. Вы можете перестроить группу так, чтобы объекты стали занимать меньшую высоту, либо скрыть/переместить менее значимые объекты.

В завершении стоит добавить, что главная идея построения интерфейса в ответ на изменение соотношения сторон основана на том, что нам не всегда нужно кардинально изменять интерфейс для разных типов устройств и порой достаточно создать два варианта представления для ситуаций, которые мы назвали «горизонтальным» и «вертикальным» расположением окна/устройства. Например, стандартное приложение для Windows 10 «Почта» имеет разные XAML-файлы под версию для ПК и телефонов, тогда как описанная выше концепция также смотрелась бы вполне логично. В частности, в версии для телефонов, повернув устройство в горизонтально положение, интерфейс не изменится, тогда как места вполне достаточно для отображения как списка писем, так и содержания письма по типу версии для ПК. В дальнейших статьях, мы будем постоянно прибегать к примерам использования такой стратегии, что позволит вам закрепить данный материал, либо напротив выработать собственную методику построения универсальных и адаптивных интерфейсов.

Первая статья из серии «UWP beginner»: Заголовок окна в приложениях.
Microsoft
Microsoft — мировой лидер в области ПО и ИТ-услуг

Комментарии 21

    0
    Картинок не хватает. А те что есть непропорционально большие.
    … вы будете видеть приятную глазу картину.
    можно так же заметить… изменения в интерфейсе.
      0
      Каких именно?
        0
        Именно тех, на которые ссылаетесь.

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

          Насчет рисунков, оставлен специально такой масштаб, чтобы было максимально наглядно: маленькое и большое окно.
      +1
      А как же способ с использованием DeviceFamily для указания различных платформ, такой способ более подходящий (https://habrahabr.ru/post/268695/), учитывая разный масштаб на устройствах. Не вижу тут никакого лайфхака.
        +1
        Идея лайфхака в том, что не всегда нужно знать на каком устройстве выполняется приложение, что бы получить одинаковое поведение, о чем сказано в заключении. Да есть масса сценариев, когда DeviceFamily предпочтительней, но есть более простые сценарии, в которых DeviceFamily это усложнение. Прежде чем сделать Рубль Life я изучил статью, которую вы привели в качестве примера и счел ее усложнением для моего сценария. Мое решение проще и так же решает поставленную задачу. Приложение тестировалось на всех размерах от маленьких смартфонов, до больших телевизоров и везде смотрится приятно.
          0
          Ну для столь простого, то да. В более сложном приложении, это костыль по сути, усложняет View.
            +1
            Так таких простых проектов полным полно. Изучая их я и решился на написание данной статьи.
            Простой пример — программа со стихами некоего автора. Разработчик решил, что на мобильных устройствах он будет показывать список стихов в ListView, а на ПК и GridView. Все хорошо, пока не начинаются описанные в статье манипуляции. Описанное решение стоило бы ему пару лишних строчек и в корне изменило бы ситуацию, а решение с разделением по девайсам слишком больших усилий на которое он скорее всего не готов пойти.
        0
        Есть несколько идей для следующих статей из этой рубрики, что было бы интереснее для вас?

        1. SettingsFlyout в UWP.
        2. Стилизация и разработка элементов управления UWP.
        3. Продвижение: как получить положительный отзыв.
          +2
          Второе.
            0
            Вот вот, даже если использовать MVVM, то все равно не получается отдельно от кода работать.
            +1
            Пожалуйста раскройте эту тему:
            2. Стилизация и разработка собственных элементов управления UWP.

            У меня есть потребность разобраться в создании нескольких элементов:
            1. Первый: Графиков, диаграмм, как элемента управления, который можно было бы использовать в других проектах. Сейчас диаграмму реализовал в виде используя несколько элементов ItemControl и TextBlock, но это творение не является единым элементом управления.
            2. Второй: Элемент для вывода отчетов с возможностью указания настраиваемых фильтров, группировок, периода. Например, нужно вывести отчет о клиентах и заказах, товарах в количестве и сумме. Подумываю вывести в виде таблицы используя Grid или в другом виде используя gridView, а для задания группировок и фильтров использовать ContentDialog. Хочется, чтобы это был некий единый элемент, который связываешь с данными, указываешь некую схему отображения, а он формирует и выводит.
            3. ListView в виде иерархического дерева. Возможность раскрывать ветки и видеть содержимое.
            4. Элемент, подобно ComboBox, но с векторными картинками (svg), без надписей и возможно с надписями. Картинки при раскрытии должны быть расположены по горизонтали и по вертикали.
            Может это уже все есть готовое, тогда подскажите где?

            Позже раскройте такие темы:
            3.
            Продвижение: как получить положительный отзыв

            4. Синхронизация локальных данных базы SQLite и некой общей базы данных в Azure. Например, пользователь использует одно и тоже приложение на телефоне (в основном вводит данные) и на компьютере (больше анализирует собранные данные, обрабатывает).
            5. Использование векторных картинок в виде пользовательских данных.
            6. Голосовой ввод и использование Cortana.
              0
              Большое спасибо за фидбэк!
            0
            >упрощает разделение труда между дизайнером и разработчиком

            Неужели есть конторы, где кто-то пишет только xaml, а кто-то только логику? Мне кажется в 99.9% верстку делает тот же разработчик.
            Спрашиваю как uwp-dev.
              0
              >В частности, в версии для телефонов, повернув устройство в горизонтально положение, интерфейс не изменится, тогда как места вполне достаточно для отображения как списка писем, так и содержания письма по типу версии для ПК.
              На 1520 при повороте в горизонтальное положение именно так и происходит — список слева, открытое письмо справа.
                0
                У меня консультировалась студентка, которая не написала не строчки кода, при этом защищалась по теме UWP дизайна. Для старичков все это кажется странным, а вот молодежь уже взращивается на идеях такого разделения труда.
                  +1
                  Мы так работаем – обучили графического дизайнера xaml'у (абсолютно без навыков программирования — ни строчки на js даже не написал никогда), очень удачно втянулся в тонкости UX/UI дизайна и работает только со вьювами, без вмешательства программистов. Конечно, бывают случаи, когда помощь нужна – конвертер там написать, или особое поведение прописать в code-behind'е, но 99.99% случаев справляется сам.
                  Так удается обеспечить одинаковую логику и единый дизайн-язык всего UI. Плюс, у программистов меньше искушений писать логику под конкретный вью.
                    +1
                    Я хочу у вас работать!
                      +1
                      Интересно. Он умеет темплейты, стили, триггеры, бихевиоры?
                      Как он пишет байндинги? Вы даёте ему контракт вью-модели?
                        +1
                        Да, темплейты, стили, триггеры, вижуал-стайлы, you name it.
                        Бихевиоры мы, правда, не ипользуем. Однажды пробовали в проекте под Windows Phone 7.1, если не ошибаюсь, но как-то не зашло. Блэнд, кстати, тоже не используется — все делает в Студии, напрямую в xaml'е.

                        Байдингам помогает ИнтеллиСенс, да и вью-модел ему доступен на просмотр.
                        Частенько, когда что-то по-сложнее, программисты ему накидывают прибайндженных контролей подряд во вью (например, в стакпанель, не заморачиваясь с тем, как это будет выглядеть в итоге), а дальше он уже сам их двигает-стилизует-анимирует.
                          0
                          Круто! Продвинутый у вам дизайнер.

                  Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                  Самое читаемое