Здравствуйте! Данная статья является продолжением цикла статей, посвященных разработке приложений для мобильной платформы 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.
Автор: Иван Щитов
Поделиться с друзьями