Смена часовых поясов

Coding4Fun

Опубликовано 2 июня 2010 г.

Вы много путешествуете? Смена часового пояса может оказаться болезненной, и иногда проще оставить его прежним, если поездки непродолжительны. Пусть Windows 7 сама обновляет необходимые параметры, используя свою новую функцию распознавания местонахождения (location awareness feature).

Автор: Ариан Т. Кулп (Arian T. Kulp)
Исходный код: загрузить
Сложность: средняя
Необходимое время: 3 часа
Затраты: бесплатно!
ПО: Visual Basic или Visual C# Express (или выше), Windows API Code Pack, Sensors and Location API, MEF Utility Runner
Необязательное дополнение: GeoSense for Windows (или другой провайдер определения местонахождения)

Введение

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

Этот код требует выполнения в Windows 7 из-за использования функции определения местонахождения. Так как в большинстве систем нет модулей GPS (во всяком случае, пока!), вам также понадобится программное или аппаратное обеспечение для получения текущего местонахождения. Вы можете задействовать великолепный (и бесплатный) GeoSense for Windows для определения местонахождения по принципу наиболее вероятного предположения (best-guess location determination) на основе вашего текущего сетевого адреса или купить лэптоп под управлением Windows 7 и установленным модулем GPS или чипом сотовой связи.

clip_image002

Код этого проекта доступен в вариантах для Visual Basic и Visual C# и выполняется в Visual Studio 2010 редакций Express. Если таких редакций у вас нет, их можно загрузить отсюда.

Что такое часовые пояса

Часовые пояса — штука интересная. Их смысл в выравнивании времени по всему миру так, чтобы в любой точке восход и закат солнца происходил примерно в один и тот же момент суток. Я всегда считал, что часовые пояса — это просто смещения на один час при движении вдоль широты. Именно так дело обстоит в США, но, как я теперь узнал, это вовсе не так в остальной части мира. Оказывается, не везде осуществляется переход на «летнее время» и вдобавок там, где он происходит, величина изменения может варьироваться. Наконец, я выяснил, что определить текущий часовой пояс на основе информации о местонахождении не так-то просто!

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

clip_image004

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

Не столь тривиальная задача

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

Покопавшись в Интернете, я нашел веб-сервис, который делает как раз то, чего я хотел от Windows, — возвращает часовой пояс при передаче ему набора координат. Я было обрадовался, что вот оно, решение. Какая глупость с моей стороны!

Оказалось, что названия часовых поясов, возвращаемых этим веб-сервисом, не совпадают с таковыми в Windows. Веб-сервис использует Unix-формат Olson, применяемый утилитой TZ. Ранее я полагал, что названия часовых поясов стандартизованы, так как привык ассоциировать часовой пояс в Америке с таким названием, как Pacific Standard Time или Eastern Daylight Time. Так вот, правильное название — это ни Pacific Standard Time, ни Pacific Daylight Time. И даже не Pacific Time. Согласно Windows, это Pacific Time (US & Canada). А название в формате Olson — America/Los_Angeles. Но, к счастью, это сопоставление типа «один к одному» (one-to-one mapping).

Поиск часового пояса

Прежде чем дело дойдет до проблемы названий, вам нужно подписаться на системное событие изменения местонахождения. С этой целью можно использовать либо LatLongLocationProvider, либо CivicAddressLocationProviderв пространстве имен Windows7. Location(из Windows API Code Pack) в зависимости от того, что именно вам требуется — координаты широты/долготы или городской адрес (civic address). Создайте провайдер и подпишитесь на событие LocationChanged. Как и в случае других типов устройств Windows 7, для работы с этим событием необходимо разрешение от пользователя. Проверку разрешения выполняет метод RequestPermissions, и, если его нет, автоматически выводит запрос пользователю на это разрешение.

Visual C#

 gps = new LatLongLocationProvider(30000);

gps.LocationChanged += new LocationChangedEventHandler(gps_LocationChanged);

LocationProvider.RequestPermissions(IntPtr.Zero, true, gps);

Visual Basic

 

 gps = New LatLongLocationProvider(30000)

LocationProvider.RequestPermissions(IntPtr.Zero, True, gps)

Когда местонахождение изменяется, срабатывает событие LocationChanged. Оно предоставляет ссылки на провайдер определения местонахождения и отчет о новом местонахождении. Объекты провайдера и отчета нужно привести к специфическому типу: LatLong или CivicAddress. Получив адрес, вы должны определить реальный часовой пояс. Найденный мной бесплатный и общедоступный источник, который выполняет такой поиск, был веб-сервис timezoneна сайте GeoNames.org. Этот сервис возвращает данные в формате XML, в том числе страну, название часового пояса и необработанные значения смещений времени:

XML

 <timezone>
    <countryCode>ES</countryCode>
    <countryName>Spain</countryName>
    <lat>39.5</lat>
    <lng>-5.97</lng>
    <timezoneId>Europe/Madrid</timezoneId>
    <dstOffset>2.0</dstOffset>
    <gmtOffset>1.0</gmtOffset>
    <rawOffset>1.0</rawOffset>
    <time>2009-11-02 01:31</time>
</timezone>

В этом примере часовой пояс называется «Europe/Madrid». Преобразование из формата Olson в Windows выполняется с помощью таблицы сопоставлений, которую я нашел в блоге Тима Дэвиса (Tim Davis). Эта таблица — отличный ресурс. В данном случае соответствующее название часового пояса в Windows — «Central European Standard Time». Так как .NET использует объект TimeZoneInfoдля представления часовых поясов, нам нужно найти правильный объект. Для этого вызывается метод FindSystemTimeZoneByIdкласса TimeZoneInfo. Получив правильный объект TimeZoneInfo(элемент в списке часовых поясов Windows), вы должны вывести запрос пользователю, а потом обновить соответствующие настройки системы.

Установка часового пояса

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

В Win32 API (в kernel32.dll) есть соответствующие функции Set /GetDynamicTimeZoneInformation . Однако в них нельзя использовать управляемые типы данных для даты и времени, поэтому я создал дополнительную логику для маршалинга данных между управляемым TimeZoneInfoи неуправляемым Win32-типом DYNAMIC_TIME_ZONE_INFORMATION.

Примечание: до появления Vista / Win7 Win32-функции и типы именовались без ключевого слова Dynamic . Это было связано с тем, что в тот период не было столь гибких схем «летнего времени». Так как это приложение требует выполнения в Windows 7 для поддержки определения местонахождения, я решил использовать более новые Win32-функции.

Visual C#

 [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
public struct DynamicTimeZoneInformation
{
    [MarshalAs(UnmanagedType.I4)]
    public int bias;
    [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)]
    public string standardName;
    public SystemTime standardDate;
    [MarshalAs(UnmanagedType.I4)]
    public int standardBias;
    [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)]
    public string daylightName;
    public SystemTime daylightDate;
    [MarshalAs(UnmanagedType.I4)]
    public int daylightBias;
    [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)]
    public string timeZoneKeyName;
    public bool dynamicDaylightTimeDisabled;
}
 Visual Basic
 <StructLayout(LayoutKind.Sequential, CharSet:=CharSet.Unicode)> _
Public Structure DynamicTimeZoneInformation
  <MarshalAs(UnmanagedType.I4)> _
   Public bias As Integer
  <MarshalAs(UnmanagedType.ByValTStr, SizeConst:=32)> _
   Public standardName As String
  Public standardDate As SystemTime
  <MarshalAs(UnmanagedType.I4)> _
    Public standardBias As Integer
  <MarshalAs(UnmanagedType.ByValTStr, SizeConst:=32)> _
     Public daylightName As String
  Public daylightDate As SystemTime
  <MarshalAs(UnmanagedType.I4)> _
   Public daylightBias As Integer
  <MarshalAs(UnmanagedType.ByValTStr, SizeConst:=128)> _
   Public timeZoneKeyName As String
  Public dynamicDaylightTimeDisabled As Boolean
End Structure

Создайте метод расширения класса TimeZoneInfo для преобразования в класс DynamicTimeZoneInformation и оберните Win32-функцию SetDynamicTimeZoneInformation . Используя перегруженную версию, вы можете задать системный часовой пояс либо с помощью класса TimeZoneInfo, либо напрямую через DynamicTimeZoneInformation.

Однако установить часовой пояс одним вызовом не выйдет. Пользователи получают право на смену часового пояса через привилегию SE_TIME_ZONE_NAME, но она не включается по умолчанию. Поэтому, чтобы ее включить, нужно выдать системный вызов AdjustTokenPrivileges, сменить часовой пояс, а затем вновь отключить эту привилегию. Смена часового поясаникак не проявляет себя, кроме как в новых процессах. Чтобы увидеть изменение, вам нужно послать системное уведомляющее сообщение. Для этого вызывается SendMessageTimeout с WM _ SettingChangeи параметром «intl». Столько всего для простой смены часового пояса!

Visual C#

 const int WM_SETTINGCHANGE = 0x1a;
const int HWND_BROADCAST = (-1);
const int SMTO_ABORTIFHUNG = 0x2;

[DllImport("user32", EntryPoint = "SendMessageTimeoutA", CharSet = CharSet.Ansi, SetLastError = true, ExactSpelling = true)]
private static extern int SendMessageTimeout(int hwnd, int msg, int wParam, string lParam, int fuFlags, int uTimeout, ref int lpdwResult);

public static int BroadcastSettingsChange()
{
    int rtnValue = 0;
    return SendMessageTimeout(HWND_BROADCAST, WM_SETTINGCHANGE, 0, "intl", SMTO_ABORTIFHUNG, 5000, ref rtnValue);
}

Visual Basic

 Const WM_SETTINGCHANGE As Integer = &H1A
Const HWND_BROADCAST As Integer = (-1)
Const SMTO_ABORTIFHUNG As Integer = &H2

<DllImport("user32", EntryPoint:="SendMessageTimeoutA", CharSet:=CharSet.Ansi, SetLastError:=True, ExactSpelling:=True)> _
Private Function SendMessageTimeout(ByVal hwnd As Integer, ByVal msg As Integer, ByVal wParam As Integer, _
   ByVal lParam As String, ByVal fuFlags As Integer, ByVal uTimeout As Integer, _
   ByRef lpdwResult As Integer) As Integer
End Function

Public Function BroadcastSettingsChange() As Integer
    Dim rtnValue As Integer = 0
    Return SendMessageTimeout(HWND_BROADCAST, WM_SETTINGCHANGE, 0, "intl", SMTO_ABORTIFHUNG, 5000, rtnValue)
End Function

Отладка и установка

Утилита Time Zone Changer создана как надстройка для моего проекта MEF Utility Runner. Вспомните, что этот проект был разработан для выполнения массы мелких утилит, чтобы избежать появления кучи значков в области уведомлений на панели задач (рядом с системными часами). Для создания подходящей структуры файлов в проект Visual Studio нужно добавить собственное событие, генерируемое после сборки (post-build event):

clip_image006

В полном виде команды в командной строке следует читать так:

mkdir "$(TargetDir)Addins\MefUtil-TZChanger.util"

copy /Y "$(TargetDir)$(TargetFileName)" "$(TargetDir)Addins\MefUtil-TZChanger.util"

copy /Y "$(TargetDir)*.dll" "$(TargetDir)Addins\MefUtil-TZChanger.util"

Чтобы разрешить запуск отладки нажатием F5, откройте вкладку Debug в свойствах проекта, в разделе Start Action выберите переключатель Startexternalprogramи в соседнем с ним поле укажите путь к исполняемому файлу HostedWpfApp . exe:

clip_image008

При запуске проекта вы увидите уведомление об успешном выполнении всех операций:

clip_image010

Для окончательного развертывания заархивируйте содержимое папки .util (только файлы в этой папке) в Zip-файл и смените его расширение с .zip на .util. Вот и все!

Следующие шаги

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

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

Заключение

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

Об авторе

Ариан Кулп (Arian Kulp) — разработчик ПО, живет в Западном Орегоне. Создает примеры, демо-ролики, лабораторные занятия и пишет статьи, выступает на различных мероприятиях, посвященных вопросам программирования, а также с удовольствием проводит свободное время со своей семьей.