Здравствуйте! Данная статья является продолжением цикла статей, посвященных разработке приложений для мобильной плат��ормы Sailfish OS. На этот раз речь пойдет об особенностях работы с датами и временными зонами в QML. Начнем статью с описания самой проблемы, а потом перейдем способам её решения.
При разработке Sailfish OS приложений довольно часто в том или ином виде придется работать с датами и временем (как, впрочем, и при разработке под любую другую платформу). Для указания даты и времени в приложениях Sailfish OS используются такие компоненты как DatePickerDialog и TimePickerDialog. Внутри для управления датой и временем они используют QML-объект Date, унаследованный от стандартного JavaScript объекта Date, который не поддерживает возможности создавать дату и время с тайм-зоной отличной от UTC или локальной. Объект Date просто не имеет конструктора и методов для этого.
Казалось бы, здесь должен помочь третий конструктор из списка, если передать ему строку с датой, временем и смещением относительно UTC, но нет. Временная зона объекта все равно будет локальной, а не той, что указана в смещении.
Вы можете спросить: «А зачем вообще использовать временные зоны? Почему нельзя обойтись временем в UTC?» И я Вам отвечу: да, иногда временные зоны не имеют смысл. Достаточно ��спользовать только дату и время. Например, если Ваш рабочий день начинается в 9:00, то вряд ли вы ожидаете что Ваш коллега с Камчатки начнет работать в 18:00. Однако, в случае с регулярными событиями, происходящими в один момент времени в разных часовых поясах, временная зона все таки нужна. К примеру, ежедневное обсуждение текущей работы над проектом начинается в 10:00 для Вас и в 19:00 для Ваших коллег на Камчатке.
Одним из вариантов решения проблемы по созданию даты и времени с установкой временной зоны было использование одной сторонних библиотек: timezone-js и moment.js. Но они оказались неподходящими, потому что DatePickerDialog и TimePickerDialog ничего не знают про данные библиотеки, а внутри активно используют стандартный Date, несовместимый с объектами, создаваемыми с помощью timezone-js и moment.js. В итоге были разработаны два других решения.
Первым решением, что пришло нам в голову, является создание собственного JavaScript объекта для управления датой и временем. Такой объект должен позволять хранить дату, время и информацию о временной зоне, а главное — изменять дату и время с помощью Sailfish OS компонентов DatePickerDialog и TimePickerDialog, не затрагивая при этом тайм-зоны.
Чтобы создать собственный JavaScript объект, необходимо в отдельном JavaScript файле определить функцию-конструктор.
Функция-конструктор принимает строку вида «yyyy-MM-ddTHH:mm:ssZ», где Z – смещение относительно UTC вида "[+-]HH:mm", стандарта ISO 8601. Из части строки создается объект Date и присваивается свойству dateTime. Это свойство будет содержать информацию о дате и времени без учета временной зоны. Оставшаяся часть строки, содержащая смещение относительно UTC, сохраняется в отдельное свойство utcOffset. Теперь мы можем создать объект, который будет содержать информацию о дате, времени и временной зоне.
Добавим к нашему объекту метод, возвращающий дату и время в том же формате «yyyy-MM-ddTHH:mm:ssZ».
Зачастую в приложениях, работающих с датой и временем, требуется отображать соответствующие значения. Мы, как разработчики, должны гарантировать, что у всех пользователей дата и время будут отображаться корректно в соотвествии с текущей локалью. Для этого добавим к нашему JavaScript объекту методы, возвращающие строки с языко-зависимым представлением даты и времени.
Так��м образом мы имеем объект, который хранит и позволяет редактировать информацию о дате, времени и временной зоне, создается с помощью строки в определенном формате, может возвращать строку в том же формате, а также форматированные строки в текущей локали. Такой объект легко позволит нам оперировать датой и временем в необходимой временной зоне.
Рассмотрим пример использования объекта CustomDateTime.
Пример содержит компоненты ValueButton для редактирования даты и времени. По клику на один компонент открывается DatePickerDialog, по клику на второй — TimePickerDialog. Подробнее описан компонент ValueButton для редактирования времени. Объект CustomDateTime создается как свойство компонента Page и используется для отображения даты и времени в ValueButton с помощью свойства value, а также для передачи значений в DatePickerDialog и TimePickerDialog, как описано в обработчике события onClicked. Там же описано получение данных из DatePickerDialog и TimePickerDialog и обновление свойства dateTime объекта CustomDateTime.
Итак, был создан JavaScript объект CustomDateTime, позволяющий хранить информацию о дате, времени и временной зоне, а также позволяющий редактировать дату и время с помощью DatePickerDialog и TimePickerDialog.
Минусом такого решения является то, что JavaScript объект не поддерживает связывания свойств. В примере после изменения даты или времени (изменения свойства dateTime объекта CustomDateTime) не обновится свойство value объекта ValueButton, т.е. в��зуально на экране не произойдет никаких изменений, несмотря на то, что фактически объект CustomDateTime изменился. Это связано с тем, что свойство dateTime объекта CustomDateTime не может быть связано со свойством value объекта ValueButton.
В тех случаях, когда связывание свойств не имеет значения, можно использовать описанное выше решение, но в других случаях необходимо обратиться к решению №2.
Вторым решением является создание собственного QML-компонента, в частности компонента типа QtObject. QtObject является самым «легковесным» стандартным QML-типом, не имеет визуальной составляющей и может быть полезен при создании объекта-модели. А главное — QML-компоненты поддерживают связывание свойств. Перепишем JavaScript объект, определенный выше, на QML-компонент.
Код стал лаконичнее, функция-конструктор и методы JavaScript объекта заменились на свойства внутри QtObject. Теперь, чтобы создать новый объект нам необходимо воспользоваться стандартным синтаксисом QML и определить лишь одно свойство dateTimeStringToSet, все остальные свойства будут посчитаны автоматически, т.к. сработает связывание свойств.
Перепишем пример, что был выше, с применением QML-объекта CustomDateTime.
Несложно заметить, что изменений совсем не много. Объявление свойства заменилось объявлением QML-компонента CustomDateTime, а также вместо функций toLocaleDateString() и toLocaleTimeString() используются свойства localeDateString и localeTimeString. Во всем остальном код абсолютно не изменился, но теперь работает связывание свойств. Изменение свойства dateTime объекта CustomDateTime приведет к обновлению всех свойств объекта и свойства localeTimeString в частности, что обновит внешний вид объекта ValueButton.
В результате было разработано решение по управлению датой, временем и информацией о временной зоне, поддерживаемое компонентами для редактирования даты и времени в Sailfish OS. Решением является создание собственного QML-компонента и использование его в качестве модели. Такой объект позволяет хранить дату, время и временную зону, а также поддерживает механизм связывания свойств и может использоваться внутри Sailfish OS компонентов DatePickerDialog и TimePickerDailog для редактирования. Исходный код описанного примера доступен на GitHub.
Автор: Иван Щитов
Описание проблемы
При разработке Sailfish OS приложений довольно часто в том или ином виде придется работать с датами и временем (как, впрочем, и при разработке под любую другую платформу). Для указания даты и времени в приложениях Sailfish OS используются такие компоненты как DatePickerDialog и TimePickerDialog. Внутри для управления датой и временем они используют QML-объект Date, унаследованный от стандартного JavaScript объекта Date, который не поддерживает возможности создавать дату и время с тайм-зоной отличной от UTC или локальной. Объект Date просто не имеет конструктора и методов для этого.
new Date();
new Date(value);
new Date(dateString);
new Date(year, month[, day[, hour[, minute[, second[, millisecond]]]]]);
Казалось бы, здесь должен помочь третий конструктор из списка, если передать ему строку с датой, временем и смещением относительно UTC, но нет. Временная зона объекта все равно будет локальной, а не той, что указана в смещении.
new Date('Jan 30 2017 10:00:00 GMT+0700') // Jan 30 2017 06:00:00 GMT+0300
Вы можете спросить: «А зачем вообще использовать временные зоны? Почему нельзя обойтись временем в UTC?» И я Вам отвечу: да, иногда временные зоны не имеют смысл. Достаточно ��спользовать только дату и время. Например, если Ваш рабочий день начинается в 9:00, то вряд ли вы ожидаете что Ваш коллега с Камчатки начнет работать в 18:00. Однако, в случае с регулярными событиями, происходящими в один момент времени в разных часовых поясах, временная зона все таки нужна. К примеру, ежедневное обсуждение текущей работы над проектом начинается в 10:00 для Вас и в 19:00 для Ваших коллег на Камчатке.
Одним из вариантов решения проблемы по созданию даты и времени с установкой временной зоны было использование одной сторонних библиотек: timezone-js и moment.js. Но они оказались неподходящими, потому что DatePickerDialog и TimePickerDialog ничего не знают про данные библиотеки, а внутри активно используют стандартный Date, несовместимый с объектами, создаваемыми с помощью timezone-js и moment.js. В итоге были разработаны два других решения.
Решение №1
Первым решением, что пришло нам в голову, является создание собственного JavaScript объекта для управления датой и временем. Такой объект должен позволять хранить дату, время и информацию о временной зоне, а главное — изменять дату и время с помощью Sailfish OS компонентов DatePickerDialog и TimePickerDialog, не затрагивая при этом тайм-зоны.
Чтобы создать собственный JavaScript объект, необходимо в отдельном JavaScript файле определить функцию-конструктор.
// CustomDateTime.js
function CustomDateTime(dateTimeString) {
this.dateTime = Date.fromLocaleString(Qt.locale(),
dateTimeString.substring(0, dateTimeString.length - 6),
"yyyy-MM-ddTHH:mm:ss");
this.utcOffset = dateTimeString.substring(dateTimeString.length - 6);
}
Функция-конструктор принимает строку вида «yyyy-MM-ddTHH:mm:ssZ», где Z – смещение относительно UTC вида "[+-]HH:mm", стандарта ISO 8601. Из части строки создается объект Date и присваивается свойству dateTime. Это свойство будет содержать информацию о дате и времени без учета временной зоны. Оставшаяся часть строки, содержащая смещение относительно UTC, сохраняется в отдельное свойство utcOffset. Теперь мы можем создать объект, который будет содержать информацию о дате, времени и временной зоне.
var myDateTime = new CustomDateTime("2016-12-22T13:40:00+05:00");
print(myDateTime.dateTime); // Dec 22 2016 13:40:00 GMT+03:00
print(myDateTime.utcOffset); // "+05:00"
myDateTime.dateTime = new Date(2016, 11, 23, 13, 00, 00);
print(myDateTime.dateTime); // Dec 23 2016 13:00:00 GMT+03:00
print(myDateTime.utcOffset); // "+05:00"
Добавим к нашему объекту метод, возвращающий дату и время в том же формате «yyyy-MM-ddTHH:mm:ssZ».
// CustomDateTime.js
CustomDateTime.prototype.toISO8601String = function() {
return this.dateTime.toLocaleString(Qt.locale(), "yyyy-MM-ddTHH:mm:ss").concat(this.utcOffset);
}
Зачастую в приложениях, работающих с датой и временем, требуется отображать соответствующие значения. Мы, как разработчики, должны гарантировать, что у всех пользователей дата и время будут отображаться корректно в соотвествии с текущей локалью. Для этого добавим к нашему JavaScript объекту методы, возвращающие строки с языко-зависимым представлением даты и времени.
// CustomDateTime.js
CustomDateTime.prototype.toLocaleDateString = function() {
return Qt.formatDate(this.dateTime, Qt.SystemLocaleShortDate);
}
CustomDateTime.prototype.toLocaleTimeString = function() {
return Qt.formatTime(this.dateTime, "HH:mm");
}
CustomDateTime.prototype.toLocaleDateTimeString = function() {
return this.toLocaleDateString() + " " + this.toLocaleTimeString();
}
Так��м образом мы имеем объект, который хранит и позволяет редактировать информацию о дате, времени и временной зоне, создается с помощью строки в определенном формате, может возвращать строку в том же формате, а также форматированные строки в текущей локали. Такой объект легко позволит нам оперировать датой и временем в необходимой временной зоне.
Рассмотрим пример использования объекта CustomDateTime.
//...
import "../model/CustomDateTime.js" as CustomDateTime
Page {
property var сustomDateTime: new CustomDateTime.CustomDateTime("2017-01-15T13:45:00+05:00")
SilicaFlickable {
anchors.fill: parent
contentHeight: column.height
Column {
id: column
//...
ValueButton {
label: qsTr("Date").concat(":")
value: сustomDateTime.toLocaleDateString()
//...
}
ValueButton {
width: parent.width
label: qsTr("Time").concat(":")
value: сustomDateTime.toLocaleTimeString()
onClicked: {
var dialog = pageStack.push("Sailfish.Silica.TimePickerDialog",
{ hour: сustomDateTime.dateTime.getHours(),
minute: сustomDateTime.dateTime.getMinutes()});
dialog.accepted.connect(function() {
сustomDateTime.dateTime = new Date(сustomDateTime.dateTime.getFullYear(),
сustomDateTime.dateTime.getMonth(),
сustomDateTime t.dateTime.getDate(),
dialog.hour, dialog.minute);
});
}
}
}
}
}
Пример содержит компоненты ValueButton для редактирования даты и времени. По клику на один компонент открывается DatePickerDialog, по клику на второй — TimePickerDialog. Подробнее описан компонент ValueButton для редактирования времени. Объект CustomDateTime создается как свойство компонента Page и используется для отображения даты и времени в ValueButton с помощью свойства value, а также для передачи значений в DatePickerDialog и TimePickerDialog, как описано в обработчике события onClicked. Там же описано получение данных из DatePickerDialog и TimePickerDialog и обновление свойства dateTime объекта CustomDateTime.
Итак, был создан JavaScript объект CustomDateTime, позволяющий хранить информацию о дате, времени и временной зоне, а также позволяющий редактировать дату и время с помощью DatePickerDialog и TimePickerDialog.
Минусом такого решения является то, что JavaScript объект не поддерживает связывания свойств. В примере после изменения даты или времени (изменения свойства dateTime объекта CustomDateTime) не обновится свойство value объекта ValueButton, т.е. в��зуально на экране не произойдет никаких изменений, несмотря на то, что фактически объект CustomDateTime изменился. Это связано с тем, что свойство dateTime объекта CustomDateTime не может быть связано со свойством value объекта ValueButton.
В тех случаях, когда связывание свойств не имеет значения, можно использовать описанное выше решение, но в других случаях необходимо обратиться к решению №2.
Решение №2
Вторым решением является создание собственного QML-компонента, в частности компонента типа QtObject. QtObject является самым «легковесным» стандартным QML-типом, не имеет визуальной составляющей и может быть полезен при создании объекта-модели. А главное — QML-компоненты поддерживают связывание свойств. Перепишем JavaScript объект, определенный выше, на QML-компонент.
// CustomDateTime.qml
import QtQuick 2.0
QtObject {
property string dateTimeStringToSet
property date dateTime: Date.fromLocaleString(Qt.locale(),
dateTimeStringToSet.substring(0, dateTimeStringToSet.length - 6),
"yyyy-MM-ddTHH:mm:ss")
property string utcOffset: dateTimeStringToSet.substring(dateTimeStringToSet.length - 6)
property string localeDateString: Qt.formatDate(dateTime, Qt.SystemLocaleShortDate)
property string localeTimeString: Qt.formatTime(dateTime, "HH:mm")
property string localeDateTimeString: localeDateString.concat(" ").concat(localeTimeString)
property string iso8601String: dateTime.toLocaleString(Qt.locale(), "yyyy-MM-ddTHH:mm:ss")
.concat(utcOffset)
}
Код стал лаконичнее, функция-конструктор и методы JavaScript объекта заменились на свойства внутри QtObject. Теперь, чтобы создать новый объект нам необходимо воспользоваться стандартным синтаксисом QML и определить лишь одно свойство dateTimeStringToSet, все остальные свойства будут посчитаны автоматически, т.к. сработает связывание свойств.
CustomDateTime {
dateTimeStringToSet: "2017-01-15T13:45:00+05:00"
}
Перепишем пример, что был выше, с применением QML-объекта CustomDateTime.
//...
Page {
CustomDateTime {
id: customDateTime
dateTimeStringToSet: "2017-01-15T13:45:00+05:00"
}
SilicaFlickable {
anchors.fill: parent
contentHeight: column.height
Column {
id: column
//...
ValueButton {
label: qsTr("Date").concat(":")
value: customDateTime.localeDateString
//...
}
ValueButton {
width: parent.width
label: qsTr("Time").concat(":")
value: customDateTime.localeTimeString
onClicked: {
var dialog = pageStack.push("Sailfish.Silica.TimePickerDialog",
{ hour: customDateTime.dateTime.getHours(),
minute: customDateTime.dateTime.getMinutes()});
dialog.accepted.connect(function() {
customDateTime.dateTime = new Date(customDateTime.dateTime.getFullYear(),
customDateTime.dateTime.getMonth(),
customDateTime.dateTime.getDate(),
dialog.hour, dialog.minute);
});
}
}
}
}
}
Несложно заметить, что изменений совсем не много. Объявление свойства заменилось объявлением QML-компонента CustomDateTime, а также вместо функций toLocaleDateString() и toLocaleTimeString() используются свойства localeDateString и localeTimeString. Во всем остальном код абсолютно не изменился, но теперь работает связывание свойств. Изменение свойства dateTime объекта CustomDateTime приведет к обновлению всех свойств объекта и свойства localeTimeString в частности, что обновит внешний вид объекта ValueButton.
Заключение
В результате было разработано решение по управлению датой, временем и информацией о временной зоне, поддерживаемое компонентами для редактирования даты и времени в Sailfish OS. Решением является создание собственного QML-компонента и использование его в качестве модели. Такой объект позволяет хранить дату, время и временную зону, а также поддерживает механизм связывания свойств и может использоваться внутри Sailfish OS компонентов DatePickerDialog и TimePickerDailog для редактирования. Исходный код описанного примера доступен на GitHub.
Автор: Иван Щитов
