Нашли ошибку или опечатку? Выделите текст и нажмите

Поменять цветовую

гамму сайта?

Поменять
Обновления сайта
и новые разделы

Рекомендовать в Google +1

Приложение для часовых поясов через Win API в WinRT

73

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

Пример приложения, отслеживающего время в разных часовых поясах

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

Разработчик с радостью обнаруживает класс TimeZoneInfo в пространстве имен System и замечает, что статический метод GetSystemTimeZones возвращает коллекцию объектов TimeZoneInfo для всех часовых поясов в мире. Однако при попытке использовать этот метод выясняется, что он недоступен для приложений Windows 8. В приложениях Windows 8 можно получить либо объект TimeZoneInfo для текущего часового пояса, либо тривиальный объект TimeZoneInfo для времени UTC (Universal Coordinated Time).

Однако приложениям Windows 8 доступны функции Win32, способные предоставить большую часть нужной информации. Функция Win32 EnumDynamicTimeZoneInformation перебирает все часовые пояса в мире в формате структур DYNAMIC_TIME_ZONE_INFORMATION:

typedef struct _TIME_DYNAMIC_ZONE_INFORMATION {
  LONG       Bias;
  WCHAR      StandardName[32];
  SYSTEMTIME StandardDate;
  LONG       StandardBias;
  WCHAR      DaylightName[32];
  SYSTEMTIME DaylightDate;
  LONG       DaylightBias;
  WCHAR      TimeZoneKeyName[128];
  BOOLEAN    DynamicDaylightTimeDisabled;
} DYNAMIC_TIME_ZONE_INFORMATION, *PDYNAMIC_TIME_ZONE_INFORMATION;

Это расширенная версия структуры TIME_ZONE_INFORMATION:

typedef struct _TIME_ZONE_INFORMATION {
  LONG       Bias;
  WCHAR      StandardName[32];
  SYSTEMTIME StandardDate;
  LONG       StandardBias;
  WCHAR      DaylightName[32];
  SYSTEMTIME DaylightDate;
  LONG       DaylightBias;
} TIME_ZONE_INFORMATION, *PTIME_ZONE_INFORMATION;

Тип WCHAR представляет 16-разрядный символ Юникода, а массивы таких символов обычно представляют строки, завершаемые нуль-символом. Поле StandardName содержит строку вида «Eastern Standard Time», а поле DaylightName - строку вида «Eastern Daylight Time». Поле TimeZoneKeyName в структуре DYNAMIC_TIME_ZONE_INFORMATION содержит ключ реестра Windows. В Windows 8 эти записи реестра находятся в ветке HKEY_LOCAL_MACHINE/SOFTWARE/Microsoft/Windows NT/CurrentVersion/Time Zones, а ключ реестра совпадает с StandardName.

Поле Bias содержит количество минут, вычитаемое из времени UTC для получения местного времени. Для восточного побережья США смещение составляет 300 минут. Поле StandardBias всегда равно нулю, тогда как DaylightBias содержит количество минут, вычитаемое из стандартного времени для перехода к летнему времени (обычно -60).

Поля DaylightDate и StandardDate определяют, когда должен происходить переход на летнее время и обратно; информация хранится в формате SYSTEMTIME:

typedef struct _SYSTEMTIME {
  WORD wYear;
  WORD wMonth;
  WORD wDayOfWeek;
  WORD wDay;
  WORD wHour;
  WORD wMinute;
  WORD wSecond;
  WORD wMilliseconds;
} SYSTEMTIME, *PSYSTEMTIME;

Структура SYSTEMTIME в основном используется функциями Win32 GetLocalTime и GetSystemTime для получения текущего местного времени и времени UTC соответственно. Значения SYSTEMTIME в структуре TIME_ZONE_INFORMATlON специально закодированы для обозначения даты перехода: поля wHour и wMinute обозначают время перехода, поле wMonth обозначает месяц перехода (например, 3 - март), поле wDayOfWeek обозначает день недели перехода (например, 1 - воскресенье), а поле wDay - конкретное вхождение заданного дня недели в заданном месяце (например, 2 - второе воскресенье месяца, или 5 - последнее воскресенье).

Windows различает локальные контексты (locales), которые переключаются между стандартным и летним временем в дни, обозначенные подобным образом, и теми, которые динамически переключают время каждый год. Говорят, что последние используют «динамическое летнее время», но информация по годам напрямую недоступна. Функция с именем GetTimeZoneInformationForYear получает год и указатель на структуру DYNAMIC_TIME_ZONE_INFORMATION, а возвращает указатель на структуру TIME_ZONE_INFORMATION с информацией для указанного года. Функция SystemTimeToTzSpecificLocalTime получает указатель на структуру TIME_ZONE_INFORMATION и указатель на структуру SYSTEMTIME (вероятно, полученный от функции GetSystemTime) и возвращает структуру SYSTEMTIME для местного времени в этом часовом поясе. Таким образом, программам не приходится самостоятельно выполнять преобразование времени.

Такая программа, как ClockRack, должна поддерживать возможность выбора часового пояса для конкретного локального контекста. В идеале она должна напоминать средства выбора часового пояса в Windows 8. Вызовите чудо-кнопки Windows 8, выберите кнопку "Параметры" и нажмите нижнюю кнопку "Изменение параметров компьютера". Кнопка запускает программу со списком настроек. Выберите категорию Общие; в верхней части приложения располагается поле со списком часовых поясов. Каждый часовой пояс определяется смещением от UTC; иногда приводится название часового пояса и во многих случаях - примеры городов.

Например, для «романского часового пояса» в списке отображается строка:

(UTC+01:00) Брюссель, Копенгаген, Мадрид, Париж

В разделе реестра Windows, содержащем список часовых поясов, метки хранятся под именем «Display», но эта информация не предоставляется функциями Win32. Ее придется извлекать из реестра, а для приложений Windows 8 недоступны функции Win32 для чтения данных из реестра.

Конечно, ничего не мешает вам написать маленькую программу .NET, которая будет обращаться к классу TimeZoneInfo, форматировать полученные строки и определять объект Dictionary, включаемый в программу Windows 8. Код программы .NET, которую я использовал для построения этого списка, приведен в комментарии над определением Dictionary:

using System;
using System.Collections.Generic;

namespace WinRTTestApp
{
    public partial class TimeZoneManager
    {
        // Сгенерировано следующей консольной программой .NET:
        // foreach (TimeZoneInfo info in TimeZoneInfo.GetSystemTimeZones())
        //     Console.WriteLine("{{ \"{0}\", \"{1}\" }},", info.StandardName, info.DisplayName);
        static Dictionary<string, string> displayStrings = new Dictionary<string, string>
        {
            { "Линия перемены дат (зима)", "(UTC-12:00) Линия перемены дат" },
            { "UTC-11", "(UTC-11:00) Время в формате UTC -11" },
            { "Гавайское время (зима)", "(UTC-10:00) Гавайи" },
            { "Аляскинское время (зима)", "(UTC-09:00) Аляска" },
            { "Тихоокеанская Мексика (зима)", "(UTC-08:00) Нижняя Калифорния" },
            { "Тихоокеанское время США (зима)", "(UTC-08:00) Тихоокеанское время (США и Канада)" },
            { "США - горное время (зима)", "(UTC-07:00) Аризона" },
            { "Горное время США (зима)", "(UTC-07:00) Горное время (США и Канада)" },
            { "Горное время (Мексика) (зима)", "(UTC-07:00) Ла-Пас, Мазатлан, Чихуахуа" },
            { "Центральное время Мехико (зима)", "(UTC-06:00) Гвадалахара, Мехико, Монтеррей" },
            { "Центральная Канада (зима)", "(UTC-06:00) Саскачеван" },
            { "Центральная Америка (зима)", "(UTC-06:00) Центральная Америка" },
            { "Центральное время США (зима)", "(UTC-06:00) Центральное время (США и Канада)" },
            { "Ю-Ам. тихоокеанское вр. (зима)", "(UTC-05:00) Богота, Кито, Лима, Рио-Бранко" },
            { "Восточное время США (зима)", "(UTC-05:00) Восточное время (США и Канада)" },
            { "США - восточное время (зима)", "(UTC-05:00) Индиана (восток)" },
            { "Венесуэла (зима)", "(UTC-04:30) Каракас" },
            { "Парагвай (зима)", "(UTC-04:00) Асунсьон" },
            { "Атлантическое время (зима)", "(UTC-04:00) Атлантическое время (Канада)" },
            { "Ю-Ам. западное время (зима)", "(UTC-04:00) Джорджтаун, Ла-Пас, Манаус, Сан-Хуан" },
            { "Центральная Бразилия (зима)", "(UTC-04:00) Куяба" },
            { "Тихоокеанское Ю-Ам. вр. (зима)", "(UTC-04:00) Сантьяго" },
            { "Ньюфаундлендское время (зима)", "(UTC-03:30) Ньюфаундленд" },
            { "Восточное Ю-Ам. время (зима)", "(UTC-03:00) Бразилия" },
            { "Аргентина (зима)", "(UTC-03:00) Буэнос-Айрес" },
            { "Гренландское время (зима)", "(UTC-03:00) Гренландия" },
            { "Ю-Ам. восточное время (зима)", "(UTC-03:00) Кайенна, Форталеза" },
            { "Монтевидео (зима)", "(UTC-03:00) Монтевидео" },
            { "Баия (зима)", "(UTC-03:00) Сальвадор" },
            { "UTC-02", "(UTC-02:00) Время в формате UTC -02" },
            { "Средняя Атлантика (зима)", "(UTC-02:00) Среднеатлантическое время - старое" },
            { "Азорское время (зима)", "(UTC-01:00) Азорские о-ва" },
            { "Кабо-Верде (зима)", "(UTC-01:00) Кабо-Верде" },
            { "Время в формате UTC", "(UTC) Время в формате UTC" },
            { "GMT - время по Гринвичу (зима)", "(UTC) Дублин, Лиссабон, Лондон, Эдинбург" },
            { "Марокко (зима)", "(UTC) Касабланка" },
            { "Время по Гринвичу (зима)", "(UTC) Монровия, Рейкьявик" },
            { "Западная Европа (зима)", "(UTC+01:00) Амстердам, Берлин, Берн, Вена, Рим, Стокгольм" },
            { "Центральная Европа (зима)", "(UTC+01:00) Белград, Братислава, Будапешт, Любляна, Прага" },
            { "Романское время (зима)", "(UTC+01:00) Брюссель, Копенгаген, Мадрид, Париж" },
            { "Центральноевропейский (зима)", "(UTC+01:00) Варшава, Загреб, Сараево, Скопье" },
            { "Намибийское время (зима)", "(UTC+01:00) Виндхук" },
            { "Западная Центр. Африка (зима)", "(UTC+01:00) Западная Центральная Африка" },
            { "Иорданское время (зима)", "(UTC+02:00) Амман" },
            { "Греция, Турция (зима)", "(UTC+02:00) Афины, Бухарест" },
            { "Ливанское время (зима)", "(UTC+02:00) Бейрут" },
            { "Финляндия (зима)", "(UTC+02:00) Вильнюс, Киев, Рига, София, Таллин, Хельсинки" },
            { "Восточная Европа (зима)", "(UTC+02:00) Восточная Европа" },
            { "Сирия (зима)", "(UTC+02:00) Дамаск" },
            { "Иерусалимское время (зима)", "(UTC+02:00) Иерусалим" },
            { "Египетское время (зима)", "(UTC+02:00) Каир" },
            { "RTZ 1 (зима)", "(UTC+02:00) Калининград (RTZ 1)" },
            { "Турция (зима)", "(UTC+02:00) Стамбул" },
            { "Ливия (зима)", "(UTC+02:00) Триполи" },
            { "Южная Африка (зима)", "(UTC+02:00) Хараре, Претория" },
            { "Багдадское время (зима)", "(UTC+03:00) Багдад" },
            { "RTZ 2 (зима)", "(UTC+03:00) Волгоград, Москва, Санкт-Петербург (RTZ 2)" },
            { "Саудовское время (зима)", "(UTC+03:00) Кувейт, Эр-Рияд" },
            { "Беларусь (зима)", "(UTC+03:00) Минск" },
            { "Восточная Африка (зима)", "(UTC+03:00) Найроби" },
            { "Иранское время (зима)", "(UTC+03:30) Тегеран" },
            { "Арабское время (зима)", "(UTC+04:00) Абу-Даби, Мускат" },
            { "Азербайджанское время (зима)", "(UTC+04:00) Баку" },
            { "Кавказское время (зима)", "(UTC+04:00) Ереван" },
            { "RTZ 3 (зима)", "(UTC+04:00) Ижевск, Самара (RTZ 3)" },
            { "Маврикий (зима)", "(UTC+04:00) Порт-Луи" },
            { "Грузинское время (зима)", "(UTC+04:00) Тбилиси" },
            { "Афганское время (зима)", "(UTC+04:30) Кабул" },
            { "Западная Азия (зима)", "(UTC+05:00) Ашхабад, Ташкент" },
            { "RTZ 4 (зима)", "(UTC+05:00) Екатеринбург (RTZ 4)" },
            { "Пакистан (зима)", "(UTC+05:00) Исламабад, Карачи" },
            { "Индийское время (зима)", "(UTC+05:30) Колката, Мумбаи, Нью-Дели, Ченнай" },
            { "Шри-Ланка (зима)", "(UTC+05:30) Шри-Джаявардене-пура-Котте" },
            { "Непальское время (зима)", "(UTC+05:45) Катманду" },
            { "Центральная Азия (зима)", "(UTC+06:00) Астана" },
            { "Бангладеш (зима)", "(UTC+06:00) Дакка" },
            { "RTZ 5 (зима)", "(UTC+06:00) Новосибирск (RTZ 5)" },
            { "Мьянмарское время (зима)", "(UTC+06:30) Янгон" },
            { "Юго-Восточная Азия (зима)", "(UTC+07:00) Бангкок, Джакарта, Ханой" },
            { "RTZ 6 (зима)", "(UTC+07:00) Красноярск (RTZ 6)" },
            { "Китайское время (зима)", "(UTC+08:00) Гонконг, Пекин, Урумчи, Чунцин" },
            { "RTZ 7 (зима)", "(UTC+08:00) Иркутск (RTZ 7)" },
            { "Малайское время (зима)", "(UTC+08:00) Куала-Лумпур, Сингапур" },
            { "Западная Австралия (зима)", "(UTC+08:00) Перт" },
            { "Тайваньское время (зима)", "(UTC+08:00) Тайбэй" },
            { "Улан-Батор (зима)", "(UTC+08:00) Улан-Батор" },
            { "Токийское время (зима)", "(UTC+09:00) Осака, Саппоро, Токио" },
            { "Корейское время (зима)", "(UTC+09:00) Сеул" },
            { "RTZ 8 (зима)", "(UTC+09:00) Якутск (RTZ 8)" },
            { "Центрально-Австралийский (зима)", "(UTC+09:30) Аделаида" },
            { "Центральная Австралия (зима)", "(UTC+09:30) Дарвин" },
            { "Восточная Австралия (зима)", "(UTC+10:00) Брисбен" },
            { "RTZ 9 (зима)", "(UTC+10:00) Владивосток, Магадан (RTZ 9)" },
            { "Западно-тихоокеанский (зима)", "(UTC+10:00) Гуам, Порт-Морсби" },
            { "Сиднейское время (зима)", "(UTC+10:00) Канберра, Мельбурн, Сидней" },
            { "Магадан (зима)", "(UTC+10:00) Магадан" },
            { "Тасманийское время (зима)", "(UTC+10:00) Хобарт" },
            { "Центрально-тихоокеанский (зима)", "(UTC+11:00) Соломоновы о-ва, Нов. Каледония" },
            { "RTZ 10 (зима)", "(UTC+11:00) Чокурдах (RTZ 10)" },
            { "RTZ 11 (зима)", "(UTC+12:00) Анадырь, Петропавловск-Камчатский (RTZ 11)" },
            { "Новозеландское время (зима)", "(UTC+12:00) Веллингтон, Окленд" },
            { "UTC+12", "(UTC+12:00) Время в формате UTC +12" },
            { "Камчатка (зима)", "(UTC+12:00) Петропавловск-Камчатский — устаревшее" },
            { "Фиджи (зима)", "(UTC+12:00) Фиджи" },
            { "Тонга (зима)", "(UTC+13:00) Нукуалофа" },
            { "Самоанское время (зима)", "(UTC+13:00) Самоа" },
            { "О-ва Лайн (зима)", "(UTC+14:00) О-в Киритимати" },
        };
    }
}

Объект Dictionary является частью класса TimeZoneManager из проекта ClockRack. В этом классе я объединил всю логику P/Invoke. Код за пределами TimeZoneManager не обращается ни к каким функциям или структурам Win32.

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

namespace WinRTTestApp
{
    public struct TimeZoneDisplayInfo
    {
        public int Bias { set; get; }
        public string Display { set; get; }
        public string TimeZoneKey { set; get; }
    }
}

Свойство Bias используется только для сортировки. Поле TimeZoneKey содержит ту же строку, что и поле TimeZoneKeyName в структуре DYNAMIC_TIME_Z0NE_INFORMATION, а свойство Display берется из словаря displayStrings.

Часть класса TimeZoneManager в основном файле TimeZoneManager.cs начинается с определения структур Win32 и объявления трех функций Win32, необходимых для работы класса:

using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;

namespace WinRTTestApp
{
    public partial class TimeZoneManager
    {
        [StructLayout(LayoutKind.Sequential)]
        struct SYSTEMTIME
        {
            public ushort wYear;
            public ushort wMonth;
            public ushort wDayOfWeek;
            public ushort wDay;
            public ushort wHour;
            public ushort wMinute;
            public ushort wSecond;
            public ushort wMilliseconds;
        }

        [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
        struct TIME_ZONE_INFORMATION
        {
            public int Bias;

            [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)]
            public string StandardName;
            public SYSTEMTIME StandardDate;
            public int StandardBias;

            [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)]
            public string DaylightName;
            public SYSTEMTIME DaylightDate;
            public int DaylightBias;
        }

        [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
        struct DYNAMIC_TIME_ZONE_INFORMATION
        {
            public int Bias;

            [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)]
            public string StandardName;
            public SYSTEMTIME StandardDate;
            public int StandardBias;

            [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)]
            public string DaylightName;
            public SYSTEMTIME DaylightDate;
            public int DaylightBias;

            [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)]
            public string TimeZoneKeyName;
            public byte DynamicDaylightTimeDisabled;
        }

        [DllImport("kernel32.dll")]
        static extern byte SystemTimeToTzSpecificLocalTime(ref TIME_ZONE_INFORMATION tzi,
                                            ref SYSTEMTIME utc, out SYSTEMTIME local);

        [DllImport("Advapi32.dll")]
        static extern uint EnumDynamicTimeZoneInformation(uint index,
                                            ref DYNAMIC_TIME_ZONE_INFORMATION dynamicTzi);

        [DllImport("kernel32.dll")]
        static extern byte GetTimeZoneInformationForYear(ushort year,
                                            ref DYNAMIC_TIME_ZONE_INFORMATION dtzi,
                                            out TIME_ZONE_INFORMATION tzi);
        

        // ...
    }
}

Вспомните, что некоторые поля структур DYNAMIC_TIME_ZONE_INFORMATION и TIME_ZONE_INFORMATlON определяются как массивы WCHAR. В C# это решение не сработает! потому что массив C# всегда представляет собой указатель на блок памяти, выделенной из кучи. Вместо этого атрибут MarshalAs позволяет указать, что эти поля должны интерпретироваться как строки C# с конкретной максимальной длиной.

Конструктор класса TimeZoneManager многократно вызывает EnumDynamicTimeZoneInformation до тех пор, пока не будет получено ненулевое значение - признак конца списка. Количество значений в списке зависит от версии Windows (101 в моей версии). Каждое значение преобразуется к типу TimeZoneDisplayInfo и добавляется в открытую коллекцию с именем DisplayInformation:

// ...

namespace WinRTTestApp
{
    public partial class TimeZoneManager
    {
        // ...        

        // Внутренний словарь для выборки значений 
        // DYNAMIC_TIME_ZONE_INFORMATION по ключам
        Dictionary<string, DYNAMIC_TIME_ZONE_INFORMATION> dynamicTzis =
                                        new Dictionary<string, DYNAMIC_TIME_ZONE_INFORMATION>();

        public TimeZoneManager()
        {
            uint index = 0;
            DYNAMIC_TIME_ZONE_INFORMATION tzi = new DYNAMIC_TIME_ZONE_INFORMATION();
            List<TimeZoneDisplayInfo> displayInformation = new List<TimeZoneDisplayInfo>();

            // Перебор часовых поясов
            while (0 == EnumDynamicTimeZoneInformation(index, ref tzi))
            {
                dynamicTzis.Add(tzi.TimeZoneKeyName, tzi);

                // Создание объекта TimeZoneDisplayInfo для открытого свойства
                TimeZoneDisplayInfo displayInfo = new TimeZoneDisplayInfo
                {
                    Bias = tzi.Bias,
                    TimeZoneKey = tzi.TimeZoneKeyName
                };

                // Поиск отображаемой строки
                if (displayStrings.ContainsKey(tzi.TimeZoneKeyName))
                {
                    displayInfo.Display = displayStrings[tzi.TimeZoneKeyName];
                }
                else if (displayStrings.ContainsKey(tzi.StandardName))
                {
                    displayInfo.Display = displayStrings[tzi.StandardName];
                }
                // Или построение
                else
                {
                    if (tzi.Bias == 0)
                        displayInfo.Display = "(UTC) ";
                    else
                        displayInfo.Display = String.Format("(UTC{0}{1:D2}:{2:D2}) ",
                                                            tzi.Bias > 0 ? '-' : '+',
                                                            Math.Abs(tzi.Bias) / 60,
                                                            Math.Abs(tzi.Bias) % 60);
                    displayInfo.Display += tzi.TimeZoneKeyName;
                }

                // Добавить в коллекцию
                displayInformation.Add(displayInfo);

                // Подготовка к следующей итерации
                index += 1;
                tzi = new DYNAMIC_TIME_ZONE_INFORMATION();
            }

            // Сортировка отображаемых данных
            displayInformation.Sort((TimeZoneDisplayInfo info1, TimeZoneDisplayInfo info2) =>
            {
                return info2.Bias.CompareTo(info1.Bias);
            });

            // Инициализация открытого свойства
            this.DisplayInformation = displayInformation;
        }

        // Открытый интерфейс
        public IList<TimeZoneDisplayInfo> DisplayInformation { protected set; get; }

        // ...
    }
}

Как вы вскоре увидите, это свойство DisplayInformation используется как источник данных (ItemsSource) для ComboBox.

Последний метод TimeZoneManager преобразует время UTC в местное время по ключу часового пояса. Эта же строка используется в поле TimeZoneKeyName структуры DYNAMIC_TIME_ZONE_INFORMATION и в свойстве TimeZoneKey структуры TimeZoneDisplayInfo:

// ...

namespace WinRTTestApp
{
    public partial class TimeZoneManager
    {
        // ...

        public DateTime GetLocalTime(string timeZoneKey, DateTime utc)
        {
            // Преобразование в структуру Win32 SYSTEMTIME
            SYSTEMTIME utcSysTime = new SYSTEMTIME
            {
                wYear = (ushort)utc.Year,
                wMonth = (ushort)utc.Month,
                wDay = (ushort)utc.Day,
                wHour = (ushort)utc.Hour,
                wMinute = (ushort)utc.Minute,
                wSecond = (ushort)utc.Second,
                wMilliseconds = (ushort)utc.Millisecond
            };

            // Преобразование к местному времени
            DYNAMIC_TIME_ZONE_INFORMATION dtzi = dynamicTzis[timeZoneKey];
            TIME_ZONE_INFORMATION tzi = new TIME_ZONE_INFORMATION();
            GetTimeZoneInformationForYear((ushort)utc.Year, ref dtzi, out tzi);

            SYSTEMTIME localSysTime = new SYSTEMTIME();
            SystemTimeToTzSpecificLocalTime(ref tzi, ref utcSysTime, out localSysTime);

            // Преобразование SYSTEMTIME в DateTime
            return new DateTime(localSysTime.wYear, localSysTime.wMonth, localSysTime.wDay,
                                localSysTime.wHour, localSysTime.wMinute,
                                localSysTime.wSecond, localSysTime.wMilliseconds);
        }
    }
}

Метод преобразует объект .NET DateTime в структуру Win32 SYSTEMTIME, получает значение dynamic_time_zone_information из закрытого словаря, после чего вызывает метод GetTimeZoneInformationForYear, который возвращает информацию в виде структуры TIME_ZONE_INFORMATION, передаваемой функции SystemTimeToTzSpecificLocalTime. Полученное значение SYSTEMTIME преобразуется обратно к типу .NET DateTime.

Не могу сказать, что я полностью доволен этим методом. Дело в том, что программа ClockRack отображает несколько часов и использует метод CompositionTarget.Rendering для получения обновленного значения DateTime.UtcNow, которое используется для всех часов. (Я решил, что такое решение будет эффективнее, чем вызывать из GetLocalTime функцию Win32 GetSystemTime с получением значения SYSTEMTIME для времени UTC по каждым часам.) Мне не нравится повторный вызов метода GetTimeZoneInformationForYear. В действительности его достаточно вызвать только один раз для каждого часового пояса, а в дальнейшем повторно использовать TIME_Z0NE_INFORMATION в последующих вызовах. Но если программа работает при переходе с 31 декабря к 1 января, потребуется повторный вызов. Я решил не загромождать класс подобной логикой.

Год, передаваемый GetTimeZoneInformationForYear, должен быть годом местного времени, а не годом UTC; это еще один момент, который обрабатывается не совсем корректно. Эти два года теоретически могут различаться только в 24-часовом периоде, включающем Новый год по UTC, и в такой программе это не должно играть роли, потому что переход между стандартным и летним временем происходит намного позднее.

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

Большая часть кода вывода часов (класс, производный от UserControl, с именем TimeZoneClock) была позаимствована в программе AnalogClock из статьи "Создание стрелочных часов в WinRT", но я переработал его для использования модели представления через привязки данных. Также все координаты и размеры были уменьшены в 10 раз. Из-за того, что в очень малом пространстве (например, в режиме Snap View) может отображаться большое количество часов, программа должна быть способна изменять размеры часов в широком спектре.

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

Каждый из двух элементов TextBlock имеет фиксированную высоту из настроек RowDefinition панели Grid, но каждый из них также находится в элементе Viewbox; слишком длинный текст автоматически сжимается по размерам. Каждые часы находятся в панели Grid из одной ячейки, свойству Background которой задается привязка данных. Панель занимает все доступное для нее место. В панели Grid находится элемент Viewbox, изменяющий размер своего содержимого - еще одной панели Grid с фиксированным размером 30 x 20, фактический размер которой определяется элементом Viewbox в зависимости от доступного места:

<UserControl ... Name="ctrl">

    <UserControl.Resources>
        <Style TargetType="Path">
            <Setter Property="StrokeLineJoin" Value="Round" />
            <Setter Property="StrokeDashCap" Value="Round" />
            <Setter Property="StrokeThickness" Value="0.2" />
            <Setter Property="StrokeStartLineCap" Value="Round" />
            <Setter Property="StrokeEndLineCap" Value="Round" />
            <Setter Property="Fill" Value="Gray" />
        </Style>

        <Style TargetType="TextBlock">
            <Setter Property="Margin" Value="12 0" />
            <Setter Property="TextAlignment" Value="Center" />
        </Style>
    </UserControl.Resources>

    <UserControl.Foreground>
        <SolidColorBrush Color="{Binding Foreground}" />
    </UserControl.Foreground>

    <Grid>
        <Grid.Background>
            <SolidColorBrush Color="{Binding Background}" />
        </Grid.Background>

        <Viewbox StretchDirection="Both">
            <Grid Height="30" Width="20"
                  HorizontalAlignment="Center"
                  VerticalAlignment="Center">

                <Grid.RowDefinitions>
                    <RowDefinition Height="5" />
                    <RowDefinition Height="20" />
                    <RowDefinition Height="5" />
                </Grid.RowDefinitions>

                <Viewbox Grid.Row="0">
                    <TextBlock Text="{Binding Location}" />
                </Viewbox>

                <Grid Grid.Row="1">

                    <!-- Преобразование для всего циферблата -->
                    <Grid.RenderTransform>
                        <TranslateTransform X="10" Y="10" />
                    </Grid.RenderTransform>

                    <!-- Малые деления -->
                    <Path Fill="{x:Null}"
                          Stroke="{Binding ElementName=ctrl, Path=Foreground}"
                          StrokeThickness="0.3"
                          StrokeDashArray="0 3.14159">
                        <Path.Data>
                            <EllipseGeometry RadiusX="9" RadiusY="9" />
                        </Path.Data>
                    </Path>

                    <!-- Большие деления -->
                    <Path Fill="{x:Null}"
                          StrokeThickness="0.6"
                          StrokeDashArray="0 7.854"
                          Stroke="{Binding ElementName=ctrl, Path=Foreground}">
                        <Path.Data>
                            <EllipseGeometry RadiusX="9" RadiusY="9" />
                        </Path.Data>
                    </Path>

                    <!-- Часовая стрелка, указывающая вверх -->
                    <Path Data="M 0 -6 C 0 -3, 2 -3, 0.5 -2 L 0.5 0
                                       C 0.5 0.75, -0.5 0.75, -0.5 0 L -0.5 -2
                                       C -2 -3, 0 -3, 0 -6"
                          Stroke="{Binding ElementName=ctrl, Path=Foreground}">
                        <Path.RenderTransform>
                            <RotateTransform Angle="{Binding HourAngle}" />
                        </Path.RenderTransform>
                    </Path>

                    <!-- Минутная стрелка, указывающая вверх -->
                    <Path Data="M 0 -8 C 0 -7.5, 0 -7, 0.25 -6 L 0.25 0
                                       C 0.25 0.5, -0.25 0.5, -0.25 0 L -0.255 -6
                                       C 0 -7, 0 -7.5, 0 -8"
                          Stroke="{Binding ElementName=ctrl, Path=Foreground}">
                        <Path.RenderTransform>
                            <RotateTransform Angle="{Binding MinuteAngle}" />
                        </Path.RenderTransform>
                    </Path>

                    <!-- Секундная стрелка, указывающая вверх -->
                    <Path Data="M 0 1 L 0 -8"
                          Stroke="{Binding ElementName=ctrl, Path=Foreground}">
                        <Path.RenderTransform>
                            <RotateTransform Angle="{Binding SecondAngle}" />
                        </Path.RenderTransform>
                    </Path>
                </Grid>

                <Viewbox Grid.Row="2">
                    <TextBlock Text="{Binding FormattedDateTime}" />
                </Viewbox>
            </Grid>
        </Viewbox>
    </Grid>
</UserControl>

Оба элемента TextBlock и все элементы RotateTransform имеют привязки к свойствам модели представления. В начале файла TimeZoneClock.xaml видно, что модель представления также включает свойства типа Color с именами Foreground и Background. Файл фонового кода не содержит ничего, кроме вызова InitializeComponent.

Чтобы программа была относительно простой, я решил ограничить фоновый и основной цвета каждых часов 140 цветами, которым присвоены имена и которые, следовательно, соответствуют членам статического класса Colors. Как и следовало ожидать, модель представления класса TimeZoneClock определяет свойства Foreground и Background типа Color, но также она определяет свойства ForegroundName и BackgroundName; при изменении одного из этих свойство другое также изменяется при помощи логики отражения:

using System;
using Windows.UI;
using System.ComponentModel;
using System.Reflection;
using System.Runtime.CompilerServices;

namespace WinRTTestApp
{
    public class TimeZoneClockViewModel : INotifyPropertyChanged
    {
        string location = "Нью-Йорк", timeZoneKey = "Eastern Standard Time";
        Color background = Colors.White, foreground = Colors.LimeGreen;
        string backgroundName = "White", foregroundName = "LimeGreen";
        DateTime dateTime;
        string formattedDateTime;
        double hourAngle, minuteAngle, secondAngle;
        TypeInfo colorsTypeInfo = typeof(Colors).GetTypeInfo();

        public event PropertyChangedEventHandler PropertyChanged;

        public string TimeZoneKey
        {
            set { SetProperty<string>(ref timeZoneKey, value); }
            get { return timeZoneKey; }
        }

        public string Location
        {
            set { SetProperty<string>(ref location, value); }
            get { return location; }
        }

        public string ForegroundName
        {
            set
            {
                if (SetProperty<string>(ref foregroundName, value))
                    this.Foreground = NameToColor(value);
            }
            get { return foregroundName; }
        }

        public Color Foreground
        {
            set
            {
                if (SetProperty<Color>(ref foreground, value))
                    this.ForegroundName = ColorToName(value);
            }
            get { return foreground; }
        }

        public string BackgroundName
        {
            set
            {
                if (SetProperty<string>(ref backgroundName, value))
                    this.Background = NameToColor(value);
            }
            get { return backgroundName; }
        }

        public Color Background
        {
            set
            {
                if (SetProperty<Color>(ref background, value))
                    this.BackgroundName = ColorToName(value);
            }
            get { return background; }
        }

        public DateTime DateTime
        {
            set
            {
                if (SetProperty<DateTime>(ref dateTime, value))
                {
                    this.FormattedDateTime = value.ToString("dddd, dd MMMM, yyyy HH:MM", 
                        new System.Globalization.CultureInfo("ru-RU"));
                    this.SecondAngle = 6 * (dateTime.Second + dateTime.Millisecond / 1000.0);
                    this.MinuteAngle = 6 * dateTime.Minute + this.SecondAngle / 60;
                    this.HourAngle = 30 * (dateTime.Hour % 12) + this.MinuteAngle / 12;
                }
            }
            get { return dateTime; }
        }

        public string FormattedDateTime
        {
            set { SetProperty<string>(ref formattedDateTime, value); }
            get { return formattedDateTime; }
        }

        public double HourAngle
        {
            set { SetProperty<double>(ref hourAngle, value); }
            get { return hourAngle; }
        }

        public double MinuteAngle
        {
            set { SetProperty<double>(ref minuteAngle, value); }
            get { return minuteAngle; }
        }

        public double SecondAngle
        {
            set { SetProperty<double>(ref secondAngle, value); }
            get { return secondAngle; }
        }

        private Color NameToColor(string name)
        {
            return (Color)colorsTypeInfo.GetDeclaredProperty(name).GetValue(null);
        }

        private string ColorToName(Color color)
        {
            foreach (PropertyInfo property in colorsTypeInfo.DeclaredProperties)
                if (color.Equals((Color)property.GetValue(null)))
                    return property.Name;

            return "";
        }

        protected void OnPropertyChanged(string propertyName)
        {
            if (PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }

        protected bool SetProperty<T>(ref T storage, T value,
                                      [CallerMemberName] string propertyName = null)
        {
            if (object.Equals(storage, value))
                return false;

            storage = value;
            OnPropertyChanged(propertyName);
            return true;
        }

    }
}

Эта модель представления также включает свойство DateTime, при каждом изменении которого также изменяются свойства HourAngle, MinuteAngle и SecondAngle, управляющие тремя объектами RotateTransform в TimeZoneClock.xaml.

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

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

using System;
using Windows.Foundation;
using Windows.UI.Xaml.Controls;

namespace WinRTTestApp
{
    public class DistributedUniformGrid : UniformGrid
    {
        protected override Size ArrangeOverride(Size finalSize)
        {
            double cellWidth = finalSize.Width / cols;
            double cellHeight = finalSize.Height / rows;
            int index = 0;
            int displayed = 0;

            if (this.Orientation == Orientation.Vertical)
            {
                for (int row = 0; row < rows; row++)
                {
                    double y = cellHeight * row;
                    int accumDisplay = (int)Math.Ceiling((row + 1.0) * this.Children.Count / rows);
                    int display = accumDisplay - displayed;
                    cellWidth = Math.Round(finalSize.Width / display);
                    double x = 0;

                    for (int col = 0; col < display; col++)
                    {
                        if (index < this.Children.Count)
                            this.Children[index].Arrange(new Rect(x, y, cellWidth, cellHeight));

                        x += cellWidth;
                        index++;
                    }
                    displayed += display;
                }
            }
            else
            {
                for (int col = 0; col < cols; col++)
                {
                    double x = cellWidth * col;
                    int accumDisplay = (int)Math.Ceiling((col + 1.0) * this.Children.Count / cols);
                    int display = accumDisplay - displayed;
                    cellHeight = Math.Round(finalSize.Height / display);
                    double y = 0;

                    for (int row = 0; row < display; row++)
                    {
                        if (index < this.Children.Count)
                            this.Children[index].Arrange(new Rect(x, y, cellWidth, cellHeight));

                        y += cellHeight;
                        index++;
                    }
                    displayed += display;
                }
            }

            return finalSize;
        }
    }
}

Файл MainPage.xaml не содержит практически ничего, кроме определения DistributedUniformGrid для хранения элементов управления с часами:

<Page ...>

    <Grid Background="#FF1D1D1D">
        <Grid Background="Transparent" Name="layoutGrid">

            <local:DistributedUniformGrid x:Name="uniformGrid"
                                          Orientation="Vertical" />
        </Grid>
    </Grid>
</Page>

Конструктор класса MainPage отвечает за заполнение панели DistributedUniformGrid по конфигурации приложения. Программа использует класс ApplicationData из пространства имен Windows.Storage для хранения четырех текстовых полей для каждых часов: название места (выбирается пользователем), ключ часового пояса, названия основного и фонового цветов. Для первых часов данные сохраняются с ключами «0Location», «0TimeZoneKey», «0Foreground» и «0Background»; у вторых часов ключи начинаются с цифры 1 и т.д. При выборке каждого набора параметров создаются и инициализируются объекты TimeZoneClock и TimeZoneClockViewModel:

using System;
using Windows.UI.Xaml.Controls;
using System.Runtime.InteropServices;
using Windows.UI.Popups;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Media;
using Windows.Foundation.Collections;
using Windows.Storage;
using Windows.ApplicationModel;
using Windows.UI.Xaml.Input;
using Windows.UI.Xaml.Controls.Primitives;
using Windows.Foundation;

namespace WinRTTestApp
{
    public sealed partial class MainPage : Page
    {
        TimeZoneManager timeZoneManager = new TimeZoneManager();
        IPropertySet appSettings = ApplicationData.Current.LocalSettings.Values;

        public MainPage()
        {
            this.InitializeComponent();

            // Загрузка конфигурации приложения
            int index = 0;

            while (appSettings.ContainsKey(index.ToString() + "Location"))
            {
                string preface = index.ToString();

                TimeZoneClock clock = new TimeZoneClock
                {
                    DataContext = new TimeZoneClockViewModel
                    {
                        Location = appSettings[preface + "Location"] as string,
                        TimeZoneKey = appSettings[preface + "TimeZoneKey"] as string,
                        ForegroundName = appSettings[preface + "Foreground"] as string,
                        BackgroundName = appSettings[preface + "Background"] as string 
                    },
                };
                uniformGrid.Children.Add(clock);
                index += 1;
            }

            // Если конфигурация отсутствует, создать часы по умолчанию
            if (uniformGrid.Children.Count == 0)
            {
                TimeZoneClock clock = new TimeZoneClock
                {
                    DataContext = new TimeZoneClockViewModel()
                };
                uniformGrid.Children.Add(clock);
            }

            // Назначить обработчик для события Suspending
            Application.Current.Suspending += OnApplicationSuspending;

            //Отслеживание события Rendering
            CompositionTarget.Rendering += OnCompositionTargetRendering;
        }

        private void OnApplicationSuspending(object sender, SuspendingEventArgs e)
        {
            appSettings.Clear();

            for (int index = 0; index < uniformGrid.Children.Count; index++)
            {
                TimeZoneClock timeZoneClock = uniformGrid.Children[index] as TimeZoneClock;
                TimeZoneClockViewModel viewModel =
                        timeZoneClock.DataContext as TimeZoneClockViewModel;
                string preface = index.ToString();

                appSettings[preface + "Foreground"] = viewModel.ForegroundName;
                appSettings[preface + "Background"] = viewModel.BackgroundName;
                appSettings[preface + "Location"] = viewModel.Location;
                appSettings[preface + "TimeZoneKey"] = viewModel.TimeZoneKey;
            }
        }

        // ...
    }
}

Как обычно, настройки сохраняются во время обработки события Suspending. Конструктор завершается запуском события CompositionTarget.Rendering. Оно отвечает за использование экземпляра TimeZoneManager для получения местного времени на основании текущего времени UTC с ключом часового пояса для каждых часов:

namespace WinRTTestApp
{
    public sealed partial class MainPage : Page
    {
        // ...

        private void OnCompositionTargetRendering(object sender, object e)
        {
            // Однократное получение времени
            DateTime utc = DateTime.UtcNow;

            foreach (UIElement child in uniformGrid.Children)
            {
                TimeZoneClockViewModel viewModel = 
                        (child as FrameworkElement).DataContext as TimeZoneClockViewModel;
                string timeZoneKey = viewModel.TimeZoneKey;

                // Получение местного времени по TimeZoneManager
                viewModel.DateTime = timeZoneManager.GetLocalTime(timeZoneKey, utc);
            }
        }

        // ...
    }
}

Правое касание открывает контекстное меню с тремя командами добавления (Add), изменения (Edit) и удаления (Delete) часов. Команды Edit и Delete относятся к конкретным часам, поэтому переопределение OnRightTapped начинается с определения часов, к которым прикоснулся пользователь. Этот объект передается обработчикам трех команд. Даже для команды Add эта информация необходима, потому что логика программы вставляет новые часы после выбранных:

namespace WinRTTestApp
{
    public sealed partial class MainPage : Page
    {
        // ...

        async protected override void OnRightTapped(RightTappedRoutedEventArgs e)
        {
            // Проверить, является ли родителем элемента, к которому
            // прикоснулся пользователь, объект TimeZoneClock
            FrameworkElement element = e.OriginalSource as FrameworkElement;

            while (element != null)
            {
                if (element is TimeZoneClock)
                    break;

                element = element.Parent as FrameworkElement;
            }

            if (element == null)
                return;

            // Создать объект PopupMenu 
            PopupMenu popupMenu = new PopupMenu();
            popupMenu.Commands.Add(new UICommand("Добавить", OnAddMenuItem, element));
            popupMenu.Commands.Add(new UICommand("Редактировать...", OnEditMenuItem, element));

            if (uniformGrid.Children.Count > 1)
                popupMenu.Commands.Add(new UICommand("Удалить", OnDeleteMenuItem, element));

            e.Handled = true;
            base.OnRightTapped(e);

            // Отобразить в меню
            await popupMenu.ShowAsync(e.GetPosition(this));
        }

        // ...
    }
}

Команда Delete присутствует в меню только в том случае, если в приложении открыто более одних часов:

namespace WinRTTestApp
{
    public sealed partial class MainPage : Page
    {
        // ...

        private async void OnDeleteMenuItem(IUICommand command)
        {
            TimeZoneClock timeZoneClock = command.Id as TimeZoneClock;
            TimeZoneClockViewModel viewModel = timeZoneClock.DataContext as TimeZoneClockViewModel;

            MessageDialog msgdlg = new MessageDialog("Удалить часы из списка?", 
                                                     viewModel.Location);
            msgdlg.Commands.Add(new UICommand("OK"));
            msgdlg.Commands.Add(new UICommand("Отмена"));
            msgdlg.DefaultCommandIndex = 0;
            msgdlg.CancelCommandIndex = 1;

            IUICommand msgDlgCommand = await msgdlg.ShowAsync();

            if (msgDlgCommand.Label == "OK")
                uniformGrid.Children.Remove(command.Id as TimeZoneClock);
        }
        
        // ...
    }
}

Для команды меню Add необходимо создать и вставить в коллекцию новый объект TimeZoneClock (с соответствующим объектом TimeZoneClockViewModel). Новые часы всегда вставляются после часов, к которым прикоснулся пользователь:

namespace WinRTTestApp
{
    public sealed partial class MainPage : Page
    {
        // ...

        private void OnAddMenuItem(IUICommand command)
        {
            // Создать объект TimeZoneClock
            TimeZoneClock timeZoneClock = new TimeZoneClock
            {
                DataContext = new TimeZoneClockViewModel()
            };

            // Новые часы вставляются после тех, к которым прикоснулся пользователь
            TimeZoneClock clickedClock = command.Id as TimeZoneClock;
            int index = uniformGrid.Children.IndexOf(clickedClock);
            uniformGrid.Children.Insert(index + 1, timeZoneClock);
        }
        
        // ...
    }
}

Конечно, команда Edit требует более сложной обработки. Скорее всего, вы вызовете ее сразу же после добавления новых часов (если только вас не устраивают часы с синим циферблатом на желтом фоне). Команда Edit создает экземпляр SettingsDialog (вскоре этот класс будет рассмотрен более подробно) как потомка объекта Popup. Так как SettingsDialog необходим доступ к экземпляру TimeZoneManager, объект TimeZoneManager передается в конструкторе SettingsDialog. Основная часть кода отвечает за позиционирование Popup, чтобы окно было визуально связано с выбранными часами и не выходило за край экрана:

namespace WinRTTestApp
{
    public sealed partial class MainPage : Page
    {
        // ...

        private void OnEditMenuItem(IUICommand command)
        {
            TimeZoneClock timeZoneClock = command.Id as TimeZoneClock;
            SettingsDialog settingsDialog = new SettingsDialog(timeZoneManager);
            settingsDialog.DataContext = timeZoneClock.DataContext;

            // Создание объекта Popup с потомком SettingsDialog
            Popup popup = new Popup
            {
                Child = settingsDialog,
                IsLightDismissEnabled = true
            };

            settingsDialog.SizeChanged += (sender, e) =>
                {
                    // Получение центра часов
                    Point position = new Point(timeZoneClock.ActualWidth / 2,
                                               timeZoneClock.ActualHeight / 2);

                    // Преобразование в координаты страницы
                    position = timeZoneClock.TransformToVisual(this).TransformPoint(position);

                    // Левый нижний или правый нижний угол совмещается с центром настраиваемых часов
                    if (position.X > this.ActualWidth / 2)
                        position.X -= settingsDialog.ActualWidth;

                    position.Y -= settingsDialog.ActualHeight;

                    // Поправка на размер страницы
                    if (position.X + settingsDialog.ActualWidth > this.ActualWidth)
                        position.X = this.ActualWidth - settingsDialog.ActualWidth;

                    if (position.X < 0)
                        position.X = 0;

                    if (position.Y < 0)
                        position.Y = 0;

                    // Задании позиции Popup
                    popup.HorizontalOffset = position.X;
                    popup.VerticalOffset = position.Y;
                };

            popup.IsOpen = true;
        }
        
        // ...
    }
}

На иллюстрации показано, как выглядит окно SettingsDialog. В первом поле EditBox просто вводится текстовое описание; три других поля используют поле со списком ComboBox для отображения текущего выделенного элемента. При получении фокуса ввода поле ComboBox открывает список вариантов:

Добавление меню для смены часовых поясов

Свойству DataContext объекта SettingsDialog задается значение свойства DataContext изменяемого объекта TimeZoneClock. Оно представляет собой объект TimeZoneClockViewModel, а в файле XAML содержатся привязки к свойствам Location.TimeZoneKey, ForegroundName и BackgroundName этого класса. Обратите внимание: у поля TextBox, привязанного к свойству Location, назначен обработчик события TextChanged; это позволяет файлу фонового кода «вручную» обновлять свойство Location объекта TimeZoneClockViewModel, что приводит к обновлению содержимого верхней части Popup.

<UserControl
    x:Class="WinRTTestApp.SettingsDialog"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:WinRTTestApp.Extensions"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d"
    d:DesignHeight="300"
    d:DesignWidth="400">

    <UserControl.Resources>
        <Style x:Key="DialogCaptionTextStyle"
               TargetType="TextBlock">
            <Setter Property="FontSize" Value="15" />
            <Setter Property="FontWeight" Value="Bold" />
            <Setter Property="Margin" Value="0 16 0 8" />
        </Style>

        <DataTemplate x:Key="colorItemTemplate">
            <!-- Варианты - SettingsDialog.ColorItem -->
            <StackPanel Orientation="Horizontal">
                <Rectangle Width="96" Height="24" Margin="12 6">
                    <Rectangle.Fill>
                        <SolidColorBrush Color="{Binding Color}" />
                    </Rectangle.Fill>
                </Rectangle>

                <TextBlock Text="{Binding Name}"
                           VerticalAlignment="Center" />
            </StackPanel>
        </DataTemplate>
    </UserControl.Resources>

    <!-- DataContext содержит TimeZoneClockViewModel -->
    <Border Background="{StaticResource ApplicationPageBackgroundThemeBrush}"
            BorderBrush="{StaticResource ApplicationForegroundThemeBrush}"
            Padding="7 0 0 0"
            BorderThickness="1"
            Width="384">
        <StackPanel Margin="24">
            <TextBlock Text="Настройки ClockRack"
                       FontSize="24"
                       TextAlignment="Center" />

            <TextBlock Text="{Binding Location}"
                       FontSize="24"
                       TextAlignment="Center"
                       Margin="0 0 0 12" />

            <!-- Географическое место -->
            <TextBlock Text="Location"
                       Style="{StaticResource DialogCaptionTextStyle}" />

            <TextBox Name="locationTextBox"
                     Text="{Binding Location}"
                     TextChanged="OnLocationTextBoxTextChanged" />

            <!-- Часовой пояс -->
            <TextBlock Text="Time Zone"
                       Style="{StaticResource DialogCaptionTextStyle}" />

            <ComboBox Name="timeZoneComboBox"
                      SelectedValuePath="TimeZoneKey"
                      SelectedValue="{Binding TimeZoneKey, Mode=TwoWay}">
                <ComboBox.ItemTemplate>
                    <!-- Данные - TimeZoneDisplayInfo -->
                    <DataTemplate>
                        <TextBlock Text="{Binding Display}" />
                    </DataTemplate>
                </ComboBox.ItemTemplate>
            </ComboBox>

            <!-- Цвета текста и фона -->
            <TextBlock Text="Цвет текста"
                       Style="{StaticResource DialogCaptionTextStyle}" />

            <ComboBox Name="foregroundComboBox"
                      ItemTemplate="{StaticResource colorItemTemplate}"
                      SelectedValuePath="Name"
                      SelectedValue="{Binding ForegroundName, Mode=TwoWay}" />

            <TextBlock Text="Цвет фона"
                       Style="{StaticResource DialogCaptionTextStyle}" />

            <ComboBox Name="backgroundComboBox"
                      ItemTemplate="{StaticResource colorItemTemplate}"
                      SelectedValuePath="Name"
                      SelectedValue="{Binding BackgroundName, Mode=TwoWay}" />
        </StackPanel>
    </Border>
</UserControl>

Файл фонового кода (приведенный далее) поставляет коллекции для трех элементов управления ComboBox. Список часовых поясов заполняется из свойства DisplayInformation объекта TimeZoneManager, а соответствующая разметка первого поля ComboBox ссылается на свойства TimeZoneKey и Display.

using Windows.UI.Xaml.Controls;

namespace WinRTTestApp
{
    public sealed partial class SettingsDialog : UserControl
    {
        public SettingsDialog(TimeZoneManager timeZoneManager)
        {
            this.InitializeComponent();

            // Назначение источника данных для поля ComboBox часового пояса
            timeZoneComboBox.ItemsSource = timeZoneManager.DisplayInformation;

            // Назначение источников данных для полей ComboBox
            foregroundComboBox.ItemsSource = NamedColor.All;
            backgroundComboBox.ItemsSource = NamedColor.All;
        }

        private void OnLocationTextBoxTextChanged(object sender, TextChangedEventArgs e)
        {
            (this.DataContext as TimeZoneClockViewModel).Location = (sender as TextBox).Text;
        }
    }
}

Ч решил использовать класс NamedColor, который мы создали в статье "Шаблон данных DataTemplate в WinRT" для получения коллекции объектов NamedColor. Как видно из файла XAML, шаблон ItemTemplate, используемый для этих двух элементов управления, ссылается на свойства Color и Name класса NamedColor, а в каждом элементе ComboBox свойству SelectedValuePath присваивается свойство Name.

Пройди тесты