Julkaistu 15.10.2024

QField on nyt entistä monipuolisempi – näin tehdään QField-lisäosa

Kesäkuussa 2024 julkaistiin QFieldin versio 3.3.0 – Darién, jossa yhtenä uutena merkittävänä ominaisuutena saatiin tuki lisäosien rakentamiselle. Jatkossa siis kuka tahansa voi luoda QFieldille omia lisäosia yleiseen, omaan tai organisaation käyttöön, aivan kuten QGISissä. QGIS-lisäosista poiketen QFieldissä ohjelmointikielenä toimii Pythonin sijaan Qt-ympäristöön kuuluva QML (Qt Modeling Language), jota käytetään lisäosan käyttöliittymän luomiseen ja joka sisältää myös tuen JavaScript-kielelle, jolla toteutetaan lisäosan varsinaiset toiminnallisuudet.

QField-lisäosa

Lisäosia voi QFieldissä käyttää kahdella tapaa, projektikohtaisena tai koko sovelluksen laajuisena. Projektikohtaisen lisäosan lähdekoodi tulee sisällyttää samaan kansioon QField-projektin kanssa ja antaa lisäosan .qml-tiedostolle sama nimi kuin projektitiedostolle. Sovelluslisäosan voi asentaa käyttöliittymän kautta syöttämällä linkin, josta lisäosa on ladattavissa .zip-tiedostona.

qfield-lisäosa
QFieldin asetuksista löytyy lisäosille uusi valikko, jossa niitä voi hallinnoida.

Lisäosan kehittämisessä on kätevää käyttää projektikohtaista lisäosaa, koska sen voi päivittää ja asentaa testikäyttöön yksinkertaisesti kopioimalla lähdekoodin projektitiedoston kanssa samaan sijaintiin ja avaamalla projektitiedoston. Vaikka QField on tarkoitettu ensisijaisesti puhelin- ja tablettikäyttöön, siitä löytyy myös työpöytäversio, mikä helpottaa lisäosien kehittämistä ja testaamista. Täältä voit ladata viimeisimmän QField-julkaisun työpöytäkäyttöön.

Näin teet QField-lisäosan

Käydään läpi yksinkertaisen lisäosan kehittäminen vaiheittain. Voit kokeilla pluginia tallentamalla samaan kansioon QGIS-projektin ja QML-tiedoston, esimerkiksi nimillä test_plugin.qgz ja test_plugin.qml. Kun avaat QGIS-projektin QFieldillä, sen pitäisi kysyä otetaanko lisäosa käyttöön.

test_plugin.qml:

import QtQuick // tuodaan QML-standardikirjasto

import org.qfield

Item { // osa QtQuickia
  id: examplePlugin // määritellään tunniste lisäosalle

  // samoin kuin QGIS-lisäosissa käytössä on "iface"-muuttuja,
  // joka antaa pääsyn QFieldin eri komponentteihin
  // tallennetaan muuttujaan viittaus sovelluksen ikkunaan
  property var mainWindow: iface.mainWindow()

  // kun lisäosa on luotu se lähettää "completed()"
  // signaalin. Tässä määritellään ns. signal handler
  // johon kirjoitetaan JavaScriptillä mitä
  // halutaan tapahtuvan kun lisäosa on ladattu
  Component.onCompleted: {
	// tässä tapauksessa mainWindow-muuttujaa
	// hyödyntäen lähetetään viesti, joka
	// näkyy käyttöliittymässä hetken ajan
	mainWindow.displayToast('Hello world!');
  }
}

Esimerkin pitäisi näyttää tältä, kun QField avataan lisäosan kanssa:

qfield-lisäosa

Tähän hyvin yksinkertaiseen lisäosaan koodattu toiminto (”Hello world!”) tapahtuu, kun QField avataan. Tässä yksinkertaisessa lisäosassa toimintoa ei voi toistaa mitenkään (paitsi sulkemalla ja käynnistämällä uudestaan). Koska nappulan painaminen on kivaa ja tärkeää, lisätään lisäosaan seuraavaksi painike:

import QtQuick

import org.qfield
import Theme // tuodaan QFieldin teemakirjasto

Item {
  id: examplePlugin

  property var mainWindow: iface.mainWindow()

  Component.onCompleted: {
	// lisätään alla määriteltävä painike
	// lisäosien työkalupalkkiin
	iface.addItemToPluginsToolbar(pluginButton)
  }

  // QField sisältää valmiita käyttöliittymäelementtejä,
  // joita voi hyödyntää. Tässä esimerkkinä QfToolButton
  QfToolButton {
	id: pluginButton
	// iconSource: '' // halutessaan painikkeelle voi määrittää ikonin.
	// Tällöin ikonitiedosto pitää sisällyttää osaksi lisäosaa

	// Lisätään teksti painikkeelle:
	Text {
  	text: "Toast"
  	anchors.centerIn: parent

  	font: Theme.defaultFont
  	color: Theme.light
	}

	bgcolor: Theme.darkGraySemiOpaque // määritellään painikkeen väri. QFieldin teemakirjasto sisältää valmiita värejä
	round: true

	// määritellään JavaScriptillä mitä tapahtuu, kun
	// painiketta painetaan
	onClicked: {
  	mainWindow.displayToast('Hello world!');
	}
  }
}

Nyt lisäosassamme on painike, jota painamalla Hello world! -tekstin saa ruudulle näkyviin.

Lisäosaan halutaan todennäköisesti enemmän toimintoja ja paineltavia nappuloita. Nämä sijoitetaan erikseen avautuvaan dialogi-ikkuna tai käyttöliittymään, jotta niiden käyttäminen olisi kätevää. Tehdään siis seuraavaksi yksinkertainen käyttöliittymä, joka avautuu, kun painiketta painetaan:

import QtQuick
// Uusi import-komento Dialogia varten
import QtQuick.Controls

import org.qfield
import Theme

Item {
  id: examplePlugin

  property var mainWindow: iface.mainWindow()

  // Määritellään dialogikomponentti
  Dialog {
	id: dialog
	parent: mainWindow.contentItem

	title: "Example plugin"

	// Ei näytetä dialogia heti
	visible: false

	// Modaalisuus viittaa tässä siihen
	// että dialogi täytyy sulkea ennen
	// kuin voi vuorovaikuttaa muiden
	// käyttöliittymäelementtien kanssa
	modal: true
	focus: true

	font: Theme.defaultFont

	// Keskitetään dialogi
	x: (parent.width - width) / 2
	y: (parent.height - height) / 2

	width: 300
	height: 400

	standardButtons: Dialog.Close

	// Lisätään teksielementti
	Text {
  	text: "This is a dialog!"
  	font: Theme.defaultFont
  	horizontalAlignment: Text.AlignLeft
	}
  }


  QfToolButton {
	id: pluginButton

	bgcolor: Theme.darkGraySemiOpaque
	round: true

	Text {
  	anchors.centerIn: parent
  	text: "Dialog"
  	color: Theme.light
	}

	onClicked: {
  	// Näytetään yllä määritelty dialogi
  	dialog.open();
	}
  }

  Component.onCompleted: {
	iface.addItemToPluginsToolbar(pluginButton)
  }
}

Nyt meillä on dialogi, muttei sisältöä. Lisätään dialogiin muutama painike, jotka demonstroivat joitakin ohjelmointirajapinnan ominaisuuksista:

import QtQuick
import QtQuick.Controls
// uusi import-komento
import QtQuick.Layouts

import org.qfield
import Theme

Item {
  id: examplePlugin

  property var mainWindow: iface.mainWindow()

  // tallennetaan muutama olio, joita
  // tarvitaan myöhemmin
  property var layerTree: iface.findItemByObjectName('dashBoard').layerTree
  property var mapSettings: iface.mapCanvas().mapSettings

  Dialog {
	id: dialog
	parent: mainWindow.contentItem

	title: "Example plugin"

	visible: false
	modal: true
	focus: true

	font: Theme.defaultFont

	x: (parent.width - width) / 2
	y: (parent.height - height) / 2

	width: 300
	height: 400

	standardButtons: Dialog.Close

	// käytetään Column- ja RowLayouteja
	// järjestelemään eri käyttöliittymän
	// komponentit
	ColumnLayout {
  	spacing: 10

  	RowLayout {
    	Layout.fillWidth: true

    	// lisätään toimintoa kuvaava
    	// tekstikenttä (Label)
    	Label {
      	text: "Text Input:"
      	font: Theme.defaultFont
      	color: Theme.mainTextColor

      	Layout.fillWidth: true
    	}

    	// lisätään teksikenttä, johon käyttäjä
    	// voi kirjoittaa tekstiä
    	QfTextField {
      	id: textField
      	text: "Hello"

      	Layout.fillWidth: true
      	Layout.preferredWidth: 100
      	Layout.preferredHeight: font.height + 20

      	horizontalAlignment: TextInput.AlignHCenter

      	font: Theme.defaultFont

      	enabled: true
      	visible: true
    	}

    	// lisätään painike
    	QfToolButton {
      	id: textFieldButton

      	bgcolor: Theme.darkGraySemiOpaque
      	round: true

      	Text {
        	anchors.centerIn: parent
        	text: "Toast"
        	color: Theme.light
      	}

      	onClicked: {
        	// näytetään tekstikentän teksti
        	mainWindow.displayToast(textField.text);
      	}
    	}
  	}

  	RowLayout {
    	Layout.fillWidth: true

    	Label {
      	text: "List layers"
      	font: Theme.defaultFont
      	color: Theme.mainTextColor

      	Layout.fillWidth: true
    	}

    	QfToolButton {
      	id: listLayersButton

      	bgcolor: Theme.darkGraySemiOpaque
      	round: true

      	Text {
        	anchors.centerIn: parent
        	text: "List"
        	color: Theme.light
      	}

      	onClicked: {
        	let list = "Layers:";

        	// iteroidaan layerTree-olion rivejä
        	for (let i = 0; i < layerTree.rowCount(); i++) {
          	let index = layerTree.index(i, 0);
          	// haetaan tason nimi
          	let name = layerTree.data(index, FlatLayerTreeModel.Name);
          	list = list.concat("\n", name);
        	}

        	mainWindow.displayToast(list);
      	}
    	}
  	}

  	Label {
    	text: "Jump To Coordinates (WGS 84)"
    	font: Theme.defaultFont
    	color: Theme.mainTextColor
  	}

  	RowLayout {
    	Layout.fillWidth: true

    	Label {
      	text: "X"
      	font: Theme.defaultFont
      	color: Theme.mainTextColor

      	Layout.fillWidth: true
    	}

    	QfTextField {
      	id: xField
      	text: "0"

      	Layout.fillWidth: true
      	Layout.preferredWidth: 30
      	Layout.preferredHeight: font.height + 20

      	horizontalAlignment: TextInput.AlignHCenter

      	font: Theme.defaultFont

      	enabled: true
      	visible: true

      	inputMethodHints: Qt.ImhFormattedNumbersOnly
    	}

    	Label {
      	text: "Y"
      	font: Theme.defaultFont
      	color: Theme.mainTextColor

      	Layout.fillWidth: true
    	}

    	QfTextField {
      	id: yField
      	text: "0"

      	Layout.fillWidth: true
      	Layout.preferredWidth: 30
      	Layout.preferredHeight: font.height + 20

      	horizontalAlignment: TextInput.AlignHCenter

      	font: Theme.defaultFont

      	enabled: true
      	visible: true

      	inputMethodHints: Qt.ImhFormattedNumbersOnly
    	}

    	QfToolButton {
      	id: addFeatureButton

      	bgcolor: Theme.darkGraySemiOpaque
      	round: true

      	Text {
        	anchors.centerIn: parent
        	text: "Jump"
        	color: Theme.light
      	}

      	onClicked: {
        	let x = Number(xField.text);
        	let y = Number(yField.text);

        	if (isNaN(x) || isNaN(y))
        	{
          	mainWindow.displayToast('Invalid input!', 'error');
          	return;
        	}
        	mainWindow.displayToast('Jumping to (x: ' + xField.text + ', y: ' + yField.text + ')');

        	// luodaan piste käyttäjän antamista koordinaateista
        	let point = GeometryUtils.point(x, y);

        	// muunnetaan piste projektin koordinaattijärjestelmään
        	let reprojected_point = GeometryUtils.reprojectPoint(point, CoordinateReferenceSystemUtils.wgs84Crs(), mapSettings.destinationCrs);

        	// siirretään karttanäkymän keskikohta projisoituun pisteeseen
        	mapSettings.setCenter(reprojected_point);
      	}
    	}
  	}
	}
  }


  QfToolButton {
	id: pluginButton

	bgcolor: Theme.darkGraySemiOpaque
	round: true

	Text {
  	anchors.centerIn: parent
  	text: "Dialog"
  	color: Theme.light
	}

	onClicked: {
  	dialog.open();
	}
  }

  Component.onCompleted: {
	iface.addItemToPluginsToolbar(pluginButton)
  }
}

Tältä lisäosa näyttää käytössä:

Nyt meillä on siis lisäosa, jonka painikkeen takaa aukeaa dialogi-ikkuna. Tässä valikossa meillä on kolme painiketta

  • Toast: julkaisee käyttäjän määrittelemän tekstin ruudulle
  • List: listaa projektin karttatasot
  • Jump: siirtää näkymän käyttäjän määrittämiin koordinaatteihin

Tarvitsetko lisää ominaisuuksia QFieldiisi? Ota yhteyttä ja me autamme.

Profiilikuva

Juho Ervasti

Juho Ervasti on luonnontieteen FM, jota kiinnostaa paikkatieto ja eri luonnonmaantieteelliset ilmiöt. Vapaa-aika menee liikuntaharrastusten kuten kiipeilyn ja lenkkeilyn parissa.