Вступление


Недавно мы открылись миру (совершили coming out, так сказать) и опубликовали статью про наш скромный фреймворк (исходники на GitHub). После общения с заинтересовавшимися участниками (большое им спасибо!) мы пришли к выводу, что для раскрытия темы необходимо написать подобие туториала на каком-нибудь реальном примере. На сайте проекта есть раздел с уроками, но эти уроки скорее описывают специфические ситуации, нежели картину в целом. Вот почему мы решили написать небольшой гайд. Для реалистичности, по шагам опишем создание простого, но реального, проекта, который хорошо показывает портируемость решений из веба в SmartTV. И да, результат этого гайда уже доступен в LG Smart World для телевизоров на базе WebOS (вы можете найти это приложение по названию «Earth Online»). В этой статье мы описываем создание ровно такого же приложения для десктопных и мобильных браузеров.

Идея


Идея довольно проста: показывать виды Земли с борта МКС (за эту возможность надо благодарить NASA, а именно, проект High Definition Earth-Viewing System). Помимо видео, приложение будет показывать текущее положение МКС над Землей. Для этих целей мы воспользуемся сервисом Where the ISS at?. Приступим!

Начало


Создаем папку для нашего проекта:

$ mkdir earth-online
$ cd earth-online

Чтобы создать новый проект, нужно скопировать qmlcore в папку с проектом. Можно просто скачать и скопировать содержимое или склонировать с помощью GIT:

$ git clone git@github.com:pureqml/qmlcore.git

Чтобы поменьше писать руками, сгенерируем каркас приложения:

$ qmlcore/build --boilerplate

После выполнения команды в папке проекта появится два новых файла:

  • src/app.qml — PureQML код приложения
  • .manifest — минимальный манифест, описывающий приложение

Папка src используется для хранения qml исходников проекта.

В app.qml, оставим только такой код:

Item {
	anchors.fill: context; // растягиваем Item по размерам всего окна
}

Чтобы собрать приложение, нужно выполнить команду:

$ qmlcore/build

и если все прошло успешно, то в папке проекта появится новая папка build.web с результирующими файлами.

$ ls build.web/
flashlsChromeless.swf  index.html  modernizr-custom.js  qml.app.js

На секунду отвлечемся и посмотрим, что это за файлы:

  • index.html — полученная html-страница
  • qml.app.js — полученный js файл, который загружается в index.html
  • modernizr-custom.js — используемый кастомный modernizr файл
  • flashlsChromeless.swf — файл, который используется для проигрывания видео с помощью флеша (буэ), в нашем случае он не нужен, но он автоматически добавляется в проекты web платформы.

Controls


Помимо qmlcore, необходимой и наиболее стабильной части проекта, есть еще отдельный модуль controls, в котором найдется много полезных компонент уже готовых к использованию. Для подключения этого модуля его также достаточно скопировать в src папки проекта или склонировать:

$ cd src
$ git clone git@github.com:pureqml/controls.git

Для нашего приложения как раз понадобится один компонент из controls, который позволит играть live stream видео с youtube, он так и называется — YouTube. Добавим его в app.qml:

Item {
	anchors.fill: context; // растягиваем Item по размерам всего окна

	YouTube {
		anchors.fill: parent;
		source: "https://www.youtube.com/embed/ddFvjfvPnqk?autoplay=1&controls=0&showinfo=0";
	}
}

В результате, если открыть Index.html в браузере, то можно увидеть видео с МКС:



Карта


С видео разобрались. Теперь перейдем к местоположению станции. Так как для задачи достаточно отобразить позицию станции над Землей, то можно использовать просто картинку с развернутой поверхностью планеты и двигать точку по ней согласно текущим координатам (хотя в controls есть компонента YandexMap для работы с картами).



Мы подготовили картинку с картой Земли. Чтобы наша картинка, или любые другие ресурсы, попали в сборку, нужно создать специальную папку в корне проекта — dist:

$ mkdir dist

Все содержимое этой папки будет копироваться после сборки в build.web. Добавим в dist картинку с картой:

$ mkdir dist/res
$ cp <path-to-map-image> dist/res

Если попробуете собрать проект сейчас, то увидите как в build.web появилась папка res с добавленной картинкой:

$ ls build.web/res/
map.png

Логика работы с карта у нас будет в отдельной компоненте, создадим новый файл для нее IssMap.qml в src и напишем следующий код:

Item {
	anchors.fill: context;	// компонента растягивается на весь экран

	// картинка с картой
	Image {
		anchors.fill: parent;	// растягиваем по размерам родителя
		source: "res/map.png";	// путь к картинке
		fillMode: Image.Stretch;	// тип заливки, Stretch растягивает по размерам Image

		// точка для обозначения позиции станции
		Rectangle {
			id: station;		// id, по которому можно обращаться к компоненте
			width: 30;
			height: width;		// высота равна ширине
			radius: width / 2;	// радиус, чтобы скруглить прямоугольник до круга
			visible: false;		// по умолчанию точку не видно
			color: "red";
		}
	}

	// метод для установки точки соответственно долготе long и широте lat
	setPos(long, lat): {
		// делаем точку видимой
		station.visible = true

		// все константы используются для преобразования из координат
		// широты, долготы в координаты карты
		station.x = (long + 180) * this.width / 360 - (this.width / 28.4)
		station.y = (90 - lat) * this.height / 180 + (this.height / 19.45)
	}
}

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

Для выполнения HTTP запросов в controls есть специальный объект Request, сделаем на его базе контрол для асинхронного запроса координат в файле IssRequest.qml в папке src:

Request {
	// метод запроса координат
	// callback - функция, которая будет выполнена в случае успешного запроса
	call(callback): {
		this.ajax({
			url: "https://api.wheretheiss.at/v1/satellites/25544",
			done: callback,
			withCredentials: true
		})
	}
}

Теперь нужно сложить все в одно место и добавить еще текст с координатам станции. Назовем этот контрол OSD и создадим для него файл Osd.qml с кодом:

// WebItem - это обычный Item
// но с возможностью отслеживать hover и click события у мыши
WebItem {
	property bool active: false;	// объявим bool свойство - флаг отображения OSD
	anchors.fill: parent;
	opacity: active ? 1.0 : 0.0;	// прозрачность в зависимости от флага active

	// инстанциируем протокол
	IssRequest { id: request; }

	// инстанцируем нашу карту из IssMap.qml
	IssMap { id: map; }

	// текст о видимости Земли (на темной стороне или освещенной)
	// прижимаем к правому нижнему краю
	Text {
		id: visibilityText;
		anchors.right: parent.right;
		anchors.bottom: parent.bottom;
		anchors.margins: 10;
		font.pixelSize: 24;
		color: "#fff";
		text: "Earth visibility: -";
	}

	// текст с координатами: долготой и широтой
	// текст прижимаем к левому нижнему краю
	Text {
		id: positionText;
		anchors.left: parent.left;
		anchors.bottom: parent.bottom;
		anchors.margins: 10;
		font.pixelSize: 24;
		color: "#fff";
		text: "Lon: -<br>Lat: -";	// в текст можно вставлять html теги
	}

	// таймер для повторения запросов
	Timer {
		running: parent.active;	// таймер работает пока активен OSD
		triggeredOnStart: true;	// таймер срабатывает при старте
		interval: 10000;	// интервал повторения 10 секунд
		repeat: true;		// таймер повторяется

		// обработчик срабатывания, зовем метод updatePositionRequest
		onTriggered: { this.parent.updatePositionRequest() }
	}

	// делаем запрос координат, в случае успеха парсим результат
	// передаем его в doUpdatePosition
	updatePositionRequest: {
		var self = this
		request.call(function(res) {
			var data = JSON.parse(res.target.response)
			if (data)
				self.doUpdatePosition(data)
			else
				log("Request error")
		})
	}

	// заполняем полученные данные
	doUpdatePosition(data): {
		var long = parseFloat(data.longitude) // долгота
		var lat = parseFloat(data.latitude) // широта
		// заполняем текст с координатами станции
		positionText.text = "Lon: " + Number((long).toFixed(1)) + "<br>Lat: " + Number((lat).toFixed(1))
		// заполняем текст видимости data.visibility возвращает строку 'eclipsed' или 'daylight'
		visibilityText.text = "Earth visibility: " + data.visibility

		map.setPos(long, lat) // передаем координаты станции карте
	}

	// объявляем анимацию на изменение прозрачности
	// время анимации 300 мс
	Behavior on opacity { Animation { duration: 300; } }
}

Теперь нужно разместить OSD в нашем приложении поверх видео и отображать его, скажем, по клику мыши на экране и также (по клику) закрывать. Теперь app.qml примет следующий вид:

Item {
	anchors.fill: context;

	Youtube {
		anchors.fill: parent;
		source: "https://www.youtube.com/embed/ddFvjfvPnqk?autoplay=1&controls=0&showinfo=0";
	}

	Osd {
		// обработчик клика мыши
		// инвертируем флаг отображения OSD по клику
		onClicked: { this.active = !this.active }
	}
}

Собираем проект, открываем в браузере build.web/index.html, кликаем по экрану и OSD появляется с анимацией по прозрачности!



Последние штрихи


Попробуем внести еще небольшое улучшение: было бы здорово, если бы текст с координатами был выровнен:


для решения проблемы можно использовать какой-нибудь monospace шрифт.

Чтобы добавить новый шрифт, нужно подредактировать выходной index.html файл. Помните, как мы использовали папку dist, чтобы добавлять файлы в build.web после сборки? Важным моментом является именно тот факт, что копирование происходит после сборки, и таким образом, мы можем переопределить index.html. За основу возьмем текущий index.html:

$ cp build.web/index.html dist

и добавим линк на новый шрифт. После небольших правок dist/index.html будет выглядеть вот так:

<!DOCTYPE html>
<html>
<head>
	<title></title>
	<meta charset="utf-8">
	<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
	<link href="https://fonts.googleapis.com/css?family=Space+Mono" rel="stylesheet" type='text/css'>
	<script src="modernizr-custom.js"></script>
</head>
<body style="font-family: ’Space Mono’">
	<script src="qml.app.js"></script>
</body>
</html>

Текст с координатами примет вот такой вид:


Заключение


Конечно же, это приложение не раскрывает всех возможностей фреймворка, но дает наглядное представление о том, как быстро и непринужденно написать single page приложение на QML, код которого можно портировать на SmartTV платформы.

Ссылки



Поделиться с друзьями
-->

Комментарии (22)


  1. napa3um
    09.04.2017 12:38

    Очень похоже по духу RAD на Polymer, только он использует HTML (Web Components) вместо JSON и CSS Flex вместо anchors.


  1. vitaly_KF
    09.04.2017 14:38
    +5

    Вижу *QML, ставлю "+" =))

    А если серьезно, ваш проект очень крут. Желаю удачи в развитии!

    Офтоп.

    Последнее время меня терзает вопрос, а почему собственно производители браузеров не хотят добавить нативную поддержку нового языка, похожего на QML, как альтернативу HTML в свои движки?

    Это же было бы так круто — заменить весь этот ужас HTML/CSS/etc на новый JSON-подобый и декларативный стандарт! Тем более что есть готовый пример в виде Qt/QML.

    Я понимаю, что сейчас активно пилится WebAssembly и когда он будет готов, то хоть весь Qt можно будет в браузере использовать, но все же это немного не то — не будет 99% веб-программистов/верстальщиков его юзать.


    1. ggrnd0
      09.04.2017 14:52
      +3

      Потому что MS встанет в позу, и скажет сами свой QML используйте, а у нас будет XAML+WPF…
      Ой, кажется мы уже к этому приплыли — webasm…

      Вообще web никогда нормально не развивался…


      1. S_A
        09.04.2017 15:40
        +3

        Изначально html предполагался как human-readable.
        Нынешние html'и даже браузеры не все понимают…


        1. worldmind
          10.04.2017 11:47
          +1

          Изгначально html был языком разметки гипертекста, а не языком разметки приложений или вёрстки


    1. Deosis
      10.04.2017 07:06

      Это же было бы так круто — заменить весь этот ужас HTML/CSS/etc на новый JSON-подобый и декларативный стандарт!
      Так не будут делать, потому что уже есть 14 стандартов


  1. GooRoo
    09.04.2017 16:54

    Я чего-то не знаю про QML или вы заменили QML-ный синтаксис объявления методов на TypeScript-овый?


    1. whoozle
      09.04.2017 19:53

      Разве? Вот ссылка на qml-ный синтаксис.

      Мы поддерживаем и простой вариант, типа

      method: { }
      , с одной лишь оговоркой, что
      function onFoo() { }
      это метод onFoo, а не обработчик сигнала.


      1. GooRoo
        09.04.2017 20:48

        Ну ок, не заменили, а добавили. Тем не менее синтаксис новый (и на мой взгляд добавляет путаницу), причём на сайте в разделе с различиями с оригинальным QML я этого не нашёл.


        1. whoozle
          09.04.2017 21:10

          Я видимо что-то не понимаю, в qml обработчики событий задаются так же как у нас. Просто мы синтаксически не разделяем эти две декларации, что позволяет задавать методы, если они не с onXyz начинаются. Визуально путаницы никакой нет.

          reset : { this.model.reset() }
          update: { this.model.update(this.range) }
          

          Я, честно признаюсь, не исследовал оригинальный qml, насколько он строг в этом смысле.


          1. GooRoo
            10.04.2017 01:25
            +2

            На самом деле я просто хотел убедиться, что семантика Qt/QML в вашем PureQML сохранена. Но потом увидел вот это:


            // ...
            setPos(long, lat): {
                // ...
            }
            // ...

            Для Qt/QML это в принципе невалидная запись, что меня и заинтересовало.


            На мой взгляд разница между одной записью и другой всё же есть. Если я правильно понял, то для вас эти две записи эквивалентны:


            function foo() { }

            и


            foo: { }

            С одной стороны вторая запись слишком похожа на binding какого-то выражения к свойству foo. Окей, допустим, вы не стали делать различия между свойствами и методами (мемберы и есть мемберы, почему бы и нет). Но в Qt/QML есть другая штука. Вот эти две записи с его точки зрения эквивалентны:


            propName: jsExpression

            и


            propName: { return jsExpression; }

            Таким образом, если мы хотим забиндить ("забайндить" не звучит, так что буду называть это забиндить) на свойство не результат выполнения блока кода, а сам этот блок кода, то в Qt/QML мы пишем как-то так:


            property var foo
            // ...
            foo: function() {
                // ...
            }

            или даже


            foo: {
                return function() { /* ... */ };
            }

            В общем, немного сложно понять, что происходит.


            С другой стороны вы, возможно, не стали делать различия между методами и обработчиками сигналов. Окей, тоже почему бы и нет. Фактически, единственное, что их у вас отличает, это наличие префикса on. На мой взгляд, это различие недостаточно явное. Например, предположим, у нас есть компонент Foo, описанный в Foo.qml следующим образом:


            Item {
                foo: { 
                    console.log("inner"); 
                }
            }

            Потом мы его используем как-то так:


            Item {
                Foo {
                    id: _foo
            
                    foo: {  // переопределение метода, объявленного внутри
                         console.log("outer");
                    }
                }
            
                onSomeEvent: {
                    _foo.foo() // выводит только "outer"
                }
            }

            В случае же с сигналами всё работает немного иначе. Допустим, Bar выглядит так:


            Item {
                signal bar()
                onBar: { 
                    console.log("inner"); 
                }
            }

            Потом мы его используем как-то так:


            Item {
                Bar {
                    id: _bar
            
                    onBar: {  // пишем свой обработчик сигнала
                         console.log("outer");
                    }
                }
            
                onSomeEvent: {
                    _bar.bar() // выводит сначала "inner", потом "outer"
                }
            }

            Другими словами, если бы вы позволяли объявить таким же образом метод, начинающийся с on, то была бы вообще полная неразбериха. А так только наполовину, на мой взгляд. Странно всё это :)


  1. justboris
    09.04.2017 18:29
    +1

    На мобильном телефоне все съехалось в кучу.
    https://habrastorage.org/files/f8e/876/e8c/f8e876e8ccb04defa676693dd3f77b55.png


    Можно как-нибудь подружить QML и media-queries?


    1. vkrv
      09.04.2017 20:59

      Можно подружить одним из двух способов:
      1. Добавив стили в html файл, т.е. как обычно;
      2. Добавить в рантайме через stylesheet._addRule().

      Но проблему разъезжания можно, и это наверное даже проще, решить декларативно через context.system.layoutType или context.width, что-то вроде:
      font.pixelSize: context.system.layoutType <= System.MobileL? 14: 18;


      1. justboris
        09.04.2017 21:11

        А как расположить visibility под широтой и долготой, если на одну строку не вмещается?


        Нельзя же уменьшать размер шрифта до бесконечности, нечитаемо будет.


        1. comrat
          09.04.2017 21:23

          При желании зацепиться анкерами можно:

          	Text {
          		id: visibilityText;
          		anchors.left: parent.left;
          		anchors.bottom: parent.bottom;
          		anchors.margins: 10;
          		font.pixelSize: 24;
          		color: "#fff";
          		text: "Earth visibility: -";
          	}
          
          	// текст с координатами: долготой и широтой
          	// текст прижимаем к левому нижнему краю
          	Text {
          		id: positionText;
          		anchors.left: parent.left;
          		anchors.bottom: visibilityText.top;
          		anchors.margins: 10;
          		font.pixelSize: 24;
          		color: "#fff";
          		text: "Lon: -<br>Lat: -";	// в текст можно вставлять html теги
          	}
          

          Теперь visibilityText прижат к левому нижнему краю, а positionText к левому краю но к верхей границе visibilityText


        1. vkrv
          09.04.2017 21:33
          +2

          Можно сложить все это добро в Grid и тогда оно само перенесется, если не влезет. Или же руками руками навертеть логики разной степени безумности, типа:

          anchors.bottomMargin: (visibilityText.width + latText.width) < context.width ? 20 : 0;
          

          Или завернуть во FlexBox и доверить браузеру. Или крутить display.
          Но грид, в данном случае, наиболее изящнен.


    1. comrat
      11.04.2017 00:22

      Сделал UI адаптируемым для мобильных телефонов, посмотреть можно тут
      Теперь, чтобы открыть/закрыть OSD нужно нажать кнопку в верхнем левом углу:


      Для десктопа логика осталась прежней


      1. justboris
        11.04.2017 00:26

        Спасибо, стало и красивее и понятно как это сделать!


  1. apro
    10.04.2017 18:08

    Обычно для тестирования GUI framework я пытаюсь реализовать
    следующую "простую" задачу:


    в прямоугольник слева, вверху, шириной 1/5 от ширины окна приложения,
    и высотой 1/5 от высоты окна приложения, вывести произвольное число от 0 до 999
    шрифтом по умолчанию. Размер шрифта должен подбираться автоматически,
    с помощью GUI framework, таким образом, чтобы максимально занять выбранный прямоугольник.


    Насколько сложно это будет реализовать с помощью PureQML?


    1. comrat
      10.04.2017 19:21
      +2

      Если я правильно понял условия то код нужен такой:

      Item {
      	anchors.fill: context;
      
      	Rectangle {
      		width: 20%;
      		height: 20%;
      		color: "red";
      
      		Text {
      			font.pixelSize: parent.height;
      			text: "42";
      		}
      	}
      }
      

      А результат будет такой:


      1. apro
        11.04.2017 10:57

        Если я правильно понял условия то код нужен такой:

        Да спасибо именно то, что я имел ввиду.


        А ваши инструменты. типа ./qmlcore/build --boilerplate
        проверяют валидность, что в данном контексте доступны данные свойства,
        callbacks и подобные вещи?


        1. comrat
          11.04.2017 21:39
          +1

          Команда

          ./qmlcore/build --boilerplate
          

          сгенерит простое Hello World приложение, чтобы собрать свой проект нужно просто позвать
          ./qmlcore/build
          

          Насчет свойств, декларативные property проверяются, а вот сигналы нет