Why you should use Qt/QML for your next cross-platform application — part 3— TV—Forecast app

part 1 — Desktop
part 2 — Mobile
part 3 — TV

So in part 1 we did a general introduction while in part 2 we wrote a small Todo app to get a better look of what Qt/QML development would look like.

In this part we are going to do something that is more interesting, at least for me — app for the living room running on Android TV.

UniqCast IPTV cross-platform solution is made by using Qt/QML

Designing for a big screen

If you are reading this then in your life you probably had opportunity to create interfaces for different form factors. Hopefully you even tackled all of them and worked on responsive design.

TV interfaces, as all of them, need special care. Designing something that is primarily used at a few meters distance by a remote (or a gamepad), is fairly different to having an interface that is going to be navigated with your finger while holding your device up close.

There are guidelines for existing platforms that one can read on that topic. One of them is coming from Google and their Android TV, while other is from Microsoft for TV and Xbox. Depending on the platform you are targeting and your branding that you want to design into your app, you will want to make investigation before hand to make sure it is nicely integrated and usage is intuitive for the users.

UWP automatic scaling

You should start reading about Qt and how it handles scaling of your apps through multiple platforms.
You will not want to miss docs on High DPI Displays and specifically Qt::AA_EnableHighDpiScaling and Qt::AA_UseHighDpiPixmaps.

Things that you need to know is that Qt/QML tries to get you covered, however, this will need a separate article to get into all the details.

In general, when you design for TVs, you probably want 2 things:

  1. one element should be the same size on TVs who are the same size — independent of the resolution and this is hopefully handled by your framework
  2. bigger TV should have a bigger element

The second point is maybe something you don’t expect. This is also dependent on your design and how you split your layouts.
You still need to set preferred, minimum and maximum sizes on elements since TVs could also range from small to big which can be used at the same viewing distance, and this is especially important for text, as you need to make sure it’s always readable.
However, you don’t want the same element size on a small TV at your grandma’s and the one that covers whole wall in your room, as that would mean that your grandma has one big button taking half of the screen, or you need to get blindingly close to your huge TV to see it.

One approach which you will see used in QML is to have elements and fonts being dependent on some other element size or each other.
As an example, if you need to design a TV card interface, and you need to make sure you have a nice fit of x number of cards from one edge to the other, you will put a width of your cards to be window.width / x (window being id your root QML Window).
This way you can be sure that you will always get what you asked for (even if you hit a bug or two where automatic scaling wouldn’t work) even on popular Android Boxes (Android boxes to be used for TV but not running Android TV but regular Android version) who present themselves as tablets with weird configuration.

This is shown in the article below with Button “height” and Forecast card “width”

As a difference, on mobile devices, in general, you expect to have a Button which will have same “physical” size on all devices— regardless of bigger or smaller screen, higher or lower resolution — since your viewing distance or finger doesn’t change.
Extra space then would be used to present additional elements and information.

Qt based Felgo framework shows how to make device independent design

QML setup

As mentioned in previous articles all you need to do is follow official docs.
Once you have Qt setup, you will want to take a look at the android platform configuration.

There is also a fresh blog post from the Qt on how to make your app compliant with upcoming requirements in Google Play which will also guide you step by step.

App Mockup

Again, we will start with a mockup. This time we are going to create an Forecast app.

Now first thing we realize when developing for a big screen is that there is a quite a lot of space compared to smaller devices. However, already moving from phones to tablets to PC we have similar experience. The major difference we have to think about this time is handling navigation.

App design and user experience is a topic for itself so here we are going to pretend this is already done and just focus on the solution.

Weather app mockup

Of course, this needs a bit of spec for behavior, and hopefully you create or work with better mockups and design.

So, on the left we have a list of cities, it could be endless as far as our solution is concerned.

On the right we have a lot of info but splitting it bit by bit we see a section at the top, middle and bottom.

First section is simple, text information on the top left for date and city in a column.

Middle section is a row with two columns of information with an image for weather as well.

Last section is a list with information displayed in a column.

Finally, there will be ability to navigate on the left list, to select a city, and then switch to the bottom to select weather day.

To render the real weather, we need to fetch this data from somewhere. In this case we are going to use https://openweathermap.org forecast daily API.
For that you need a free API key so if you want to have this app working you can get one easily.

To make actual request we are going to use JavaScript and XMLHttpRequest. There are some differences to the one you might be using in the browser so make sure to check the docs. Otherwise, usage is as straightforward as it can get:

function getJSON(uri){
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.onreadystatechange = () => {
if (xhr.readyState === XMLHttpRequest.DONE) {
const { status, responseText } = xhr
if(status === 200) {
resolve(JSON.parse(responseText))
}
else {
reject({ code: status, msg: responseText })}};
}
xhr.open("GET", uri)
xhr.setRequestHeader('Accept', 'application/json');
xhr.send()
})
}

Now the actual call for the forecast will be as simple as:

function getForecast(city){
const uri = `${baseUrl}/data/2.5/forecast/daily?q=${city}&units=metric&appid=${appid}`;
return SDK.getJSON(uri)
}

Which in turn gives us (among other things) a list of weather information by day.

"list": [
{
"dt": 1566208800,
"sunrise": 1566187231,
"sunset": 1566237567,
"temp": {
"day": 22.2,
"min": 22.2,
"max": 22.2,
"night": 22.2,
"eve": 22.2,
"morn": 22.2
},
"pressure": 1017.54,
"humidity": 81,
"weather": [
{
"id": 801,
"main": "Clouds",
"description": "few clouds",
"icon": "02n"
}
],
"speed": 0.84,
"deg": 359,
"clouds": 13
},
...
]

Data models

In the part 2 we have used a ListModel to show benefits of instant rendering when data has been updated.
In this example, we are going to show how we can reuse JSON data we got from the API as models.

Models and Views in Qt Quick as described in official docs

So first we are going to create our model as an empty array:

property var weatherModel: []

And then update it whenever a city is changed:

function updateCity(city) {
Service.getForecast(city)
.then(data => weatherModel = data.list)
.catch(error => console.error(error.code, error.msg))
}

This way, whenever our data is changed, our elements using this data model will also update.

For city list we are just going to use an string array with the names as a model. This we can then conveniently provide to the forecast API.

readonly property var citiesModel: ["Amsterdam", "Venice", "Prague", "Lisbon","London", "Paris", "Berlin", "Zagreb", "Vienna"]

Since we don’t plan to modify the properties we also mark them as read-only so if somebody tries to do that, an error would be triggered.

Theme and styling

There is many nice things about using Qt Quick Controls 2. One of them is ability to use one of the predefined styles.

In this case we are going to use Material dark with a specific accent. This is as simple as setting properties on main ApplicationWindow:

Material.theme: Material.Dark
Material.accent: Material.BlueGrey

City list

We are again primarily going to use the Qt Layouts and you should not forget to visit official docs on the topic — Important Concepts In Qt Quick — Positioning .

To render our list we are going to use a ListView:

ListView {
id: cityList
focus: true
Layout.preferredWidth: 400
Layout.fillHeight: true
highlightFollowsCurrentItem: true
highlightMoveDuration: 250
// delay city data update
onCurrentIndexChanged: updateModelTimer.restart()
model: citiesModel keyNavigationWraps: true KeyNavigation.right: weatherList delegate: Button {
width: cityList.width
height: cityList.height / 7
font.pixelSize: 32
highlighted: activeFocus && ListView.isCurrentItem
text: modelData
}
Timer {
id: updateModelTimer
interval: 300
onTriggered: updateCity(currentCity)
}
}

We set focus: true so this will be element to start our key handling logic. Make sure to read more about it below.

Sizing options are self explanatory and highlight options are pretty straightforward and they are used to set our selection indicator properties which affects navigation speed even if we are not using the highlight property of the ListView.

The interesting thing is:

onCurrentIndexChanged: updateModelTimer.restart()

Here we are going to use the power of signal event systems of Qt/QML and the fact that a QML property triggers a signal when it changes.

Whenever a selection of the city in the list has changed, we want to update our weather model. However, we don’t actually want to do this instantly as that triggers a new request on each move, and the response, due to the IO nature of network requests, will not be instant.

So to prevent making needless requests to the API while navigating up/down with a remote, we are going to do them only after we stayed on our selection for a period of time.

Here we are going to use the signal from the list that current selection has changed and then (re)start a QML Timer. When that timer is finally triggered, it will in turn trigger a request and a model update.

Yes, implementing a proper HTTP caching (QNetworkRequest::CacheLoadControlAttribute) and aborting a pending request when a new one is made is left as an exercise for the reader

The model for the list is the city list array. The array length will determine how many delegates to create. In this case that is how many Buttons will we instantiate.

So here it gets interesting, how do we know which button is a current one?

ListView.isCurrentItem

The ListView provides and attached property on each delegate which tells us exactly that information.

And we immediately apply this information to the highlighted property of the button as this will apply different styling. We also use the activeFocus property as that will only do the highlighting if our ListView has an active focus which is exactly what we want. If focus moves to the other list, then we don’t want to keep the highlight.

Even if there was no attached property to tell us which element is current we can easily get the same with:
index === cityList.currentIndex
as only one element that has the index same a list current index would evaluate to
true

The button height we also make dependent on the window height so we get buttons which always fit nicely in the screen.

After we apply all of this we get:

Initial app working (PC)

Forecast

On the right things start very simple. We use a Column around Date and the city name which is a Label.

Column {
width: parent.width
spacing: 30
Label {
id: date
font.pixelSize: 40
Timer {
running: Qt.application.state === Qt.ApplicationActive
interval: 5000
repeat: true
triggeredOnStart: true
onTriggered: date.text = Qt.formatDate(new Date(), Qt.SystemLocaleLongDate)
}
}
Label {
id: city
font.pixelSize: 30
text: currentCity
}
}

The interesting thing is again the Timer, which we use to draw current local time for the user. This will also only be running when our application is active.

Note the nice thing about Date object being extended with additional Qt methods for locale and formatting which will make your life easier.

So how do we actually get that current city?

readonly property string currentCity: citiesModel[cityList.currentIndex]

This will always hold the currently selected city string name due to the power of QML property binding.

You could also use cityList.currentItem.text since currentItem in the list is a Button whose text we can access with a .text property, but approach like this should be discouraged outside of contained components as this makes one component dependent on the other while they are not related

For current weather things get very interesting. The reason for this is that we will also have a list below which will allow showing the forecast for the whole week.

We showed above that our model will be the array which holds weather for each day. This means weatherModel[0] is our current day, weatherModel[1] is next day and so on. Each day also has an attached timestamp so we can easily create the Date for it.

This obviously makes it easy for us to create a forecast for each day by just attaching that model.

ListView {
id: weatherList
Layout.fillWidth: true
Layout.preferredHeight: contentItem.childrenRect.height
orientation: Qt.Horizontal highlightFollowsCurrentItem: true
highlightMoveDuration: 250
highlight: Rectangle { visible: weatherList.activeFocus; color: Material.accent }
model: weatherModel delegate: ColumnLayout {
readonly property var weather: modelData.weather[0]
readonly property var temp: modelData.temp
readonly property int timestamp: modelData.dt
width: weatherList.width / 7
spacing: 80
Label {
font.pixelSize: 26
text: SDK.timpestampToDay(timestamp)
Layout.alignment: Qt.AlignHCenter
}
Image {
source: Service.getIconUrl(weather.icon)
Layout.alignment: Qt.AlignHCenter
}
Label {
font.pixelSize: 26
text: SDK.temperatureToString(temp.day)
Layout.alignment: Qt.AlignHCenter
}
}
}

So once we have that the current weather we can just bind by using the currently selected day from the list:

readonly property var currentWeather: weatherModel[weatherList.currentIndex]

Now onto the list itself.

Similar things as with a cityList we want to highlight currently selected item. In that example we used a Button and it’s highlight property.

However, that is not really needed. And one can have arguably nicer transition by using a highlight property of the ListView itself.

So this is exactly what we are doing here. We set our regular highlight options but this time we actually provide an element for rendering. In this case just a regular Rectangle with a main theme color. We also render this (visible property) only in case when weatherList has an activeFocus to give clear indication to the user on where he is in our interface when he will switch between the left and bottom list.

The delegate is straightforward. We only show how one can help themselves by saving properties from the model on the root element. The additional methods are mostly used to help format the data or get the weather icon from the OpenWeatherMap api.

function timpestampToDay(timestamp)
{
let d = new Date(timestamp * 1000)
return d.toLocaleDateString(Qt.locale(), "ddd")
}
Final app in action (PC)

Key handling

So the only leftover part is the key navigation. Make sure to read Qt docs on the topic as this really explains and covers all you might ever want to know.

So what do we need to do navigate on the list and between the lists?
Well, if you run the app you will immediately notice that key navigation already works on our city list and moving up/down our weather is already updating!

The reason for that is once we have set focus: true on the list it received the focus. And since ListView supports key navigation and has it enabled by default, it just works. Additional, we set keyNavigationWraps: true property so when coming to the end or beginning of the list, selection will wrap.

So the leftover thing is navigating between the list. This cannot get more nicer than what QML provides with it’s KeyNavigation attached property (note the difference between the property casing).
By simply setting KeyNavigation.right: weatherList we have solved navigation between both of the list by pressing Left/Right key buttons!

Was that much simpler than expected? Yes, I think it was.
But what if we want to have some manual key handling on top of all of this — that must be tricky right?
Luckily for us this is again very simple by using Keys attached property.

So let’s say we want to be able to move from the bottom list by pressing up/down keys.

Keys.onUpPressed: { cityList.decrementCurrentIndex(); cityList.focus = true }
Keys.onDownPressed: { cityList.incrementCurrentIndex(); cityList.focus = true }

Yes, it’s that simple as adding this to our weatherList.
Keep in mind that the only thing we need to do is set the focus on the element we want to move to, in this case cityList.
But we also wanted to show what more you can do, so before moving the navigation, we will also programmatically move one element up/down in the cityList by calling appropriate methods.

Wrapping it up

Running on Sony Bravia Android TV

Doesn’t look much different to our PC version, and there was zero code change, but here it is running on Android TV.

The are many improvements that cannot fit in one article without making it too big so people lose interest half way through — like adding BusyIndicator while data for the city is being loaded, and showing an error message if API is not available— possibly by using Loader .
You also need to cleanup those TypeError warnings which are left there to get you interested in exactly how QML model — view — delegate actually works :)

Finally, it would be really nice and easy to add click support as some TVs even have point & click remotes.

The final code you can find on github so take a look while you wait to tackle other interesting things in next articles!

Developer by day, architect at night — never satisfied

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store