Здравствуйте! Данная статья является продолжением цикла статей, посвященных разработке приложений для мобильной плат��ормы Sailfish OS. На этот раз речь пойдет об особенностях работы с датами и временными зонами в QML. Начнем статью с описания самой проблемы, а потом перейдем способам её решения.

Описание проблемы


При разработке 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.

Автор: Иван Щитов