To set up feature selection from the map canvas, we need to register a QField pointHandler, similar to a QgsMapTool in pyQGIS. This is done in the main code file demo2_selection.qml.
In doing so, we will also learn:
Retrieve and store a reference to the QField interface's pointHandler as a class property.
Item{
id: plugin
property var pointHandler: iface.findItemByObjectName("pointHandler")
}
The onCompleted function of the root item is triggered when the plugin loads.
Item{
id: plugin
property var pointHandler: iface.findItemByObjectName("pointHandler")
Component.onCompleted{
pointHandler.registerHandler("demo2_selection", my_callback);
}
}
If you don't do this, your point handler will contaminate your other projects.
Use Component.onDestruction to define behavior on closing.
Item{
id: plugin
<...>
Component.onDestruction{
pointHandler.deregisterHandler("demo2_selection");
}
}
Our callback function is written in JavaScript. Instead of using a function reference like below, as a Python programmer would do:
Item{
id: plugin
<...>
Component.onCompleted{
pointHandler.registerHandler("demo2_selection", my_callback);
}
}
it is more common to see JavaScript arrow function syntax, as we will see in demo2_selection
Item{
Component.onCompleted{
pointHandler.registerHandler("demo2_selection", (point, type, interactionType)=>{
iface.logMessage("Interaction Type: " + interactionType)
return true
});
}
}
The boolean return value from the pointHandler callback tells QField whether your handler has consumed the event or not:
return true - event consumed
Your handler has processed the click
return false or no return - event not consumed
"clicked": Triggering on single click can cause conflicts, as the feature drawer opens on a single click.
"doubleClicked": Triggering on double-click prevents conflicts. We can let the feature drawer open on a single click and enter the plugin with a double-click.
"pressAndHold": This would theoretically also be a nice mode for opening our plugin, but in practice it's a poor choice
Does this mean the boolean return value doesn't work quite as advertised? Perhaps. There is also a priority set on pointHandler callbacks that can affect the processing of the boolean return value from the callback.
To avoid conflicts with QField and ensure our plugin works in both environments, use clicked when we're on Windows or Ubuntu, and doubleClicked when we're on iOS. I did not test this on Android.
Item{
Component.onCompleted{
pointHandler.registerHandler("demo2_selection", (point, type, interactionType) => {
var shouldHandle = (Qt.platform.os === "windows" && interactionType === "clicked") ||
(Qt.platform.os !== "windows" && interactionType === "doubleClicked")
if (shouldHandle) {
iface.logMessage("Platform " + Qt.platform.os)
iface.logMessage("Interaction " + interactionType)
return true // block further processing of the click
}
return false // let QField pick up the interaction
});
}
}
pointHandler.registerHandler("demo2_selection", (point, type, interactionType) => {
var shouldHandle = ...
if (shouldHandle){
iface.logMessage(point.x)
iface.logMessage(point.y)
}
});
pointHandler.registerHandler("demo2_selection", (point, type, interactionType) => {
var shouldHandle = ...
if (shouldHandle){
let coords = iface.mapCanvas().mapSettings.screenToCoordinate(Qt.point(point.x, point.y))
iface.logMessage(coords.x)
iface.logMessage(coords.y)
}
});
We will perform a spatial query on the plots layer with a bounding box of 20m around our clicked coordinates. If we find a feature in this box, we will activate the plugin and pass the feature's plot ID to our plugin component.
You can call a subset of the QGIS API functions from QField and JavaScript. You can find out which functions can be called from QField by going to the QGIS C++ Class Reference for the class you're interested in. Functions you can call are marked with Q_INVOKABLE.
We get the map layer from the QgsProject instance, which is available in QField as qgisProject, imported with org.qgis.
pointHandler.registerHandler("demo2_selection", (point, type, interactionType) => {
var shouldHandle = ...
if (shouldHandle){
let layer = qgisProject.mapLayersByName("plots")[0]
iface.logMessage("Got plots layer")
}
});
getFeatures is not yet a callable QGIS function. Instead, we can pass an expression to get a feature iterator from QField's LayerUtils class, which is imported from org.qfield.
We will query our plots layer for the feature with plot_id = 'b.1'.
We will retrieve this feature and output its plot ID. (Not using our coordinates yet.)
pointHandler.registerHandler("demo2_selection", (point, type, interactionType) => {
var shouldHandle = ...
if (shouldHandle){
var layer = qgisProject.mapLayersByName("plots")[0]
var expression = "plot_id = 'b.1'";
let it = LayerUtils.createFeatureIteratorFromExpression(layer, expression)
if (it.hasNext()){
feature = it.next()
plot_id = feature.attribute("plot_id")
iface.logMessage("found the feature of the plots layer: " + plot_id)
}
it.close(); // NEVER forget to close your iterator
return true
}
return false
});
We will create a bounding box of 20m around our interaction coordinates. We will use the bounding box coordinates to pass an intersection query to LayerUtils, instead of our simple query.
if (shouldHandle) {
// Create a pair of points representing a buffer area in which to search for features.
let tl = mapCanvas.mapSettings.screenToCoordinate(Qt.point(point.x - 20, point.y - 20))
let br = mapCanvas.mapSettings.screenToCoordinate(Qt.point(point.x + 20, point.y + 20))
let expression = "intersects(geom_from_wkt('POLYGON(("+tl.x+" "+tl.y+", "+br.x+" "+tl.y+", "+br.x+" "+br.y+", "+tl.x+" "+br.y+", "+tl.x+" "+tl.y+"))'), $geometry)"
let it = LayerUtils.createFeatureIteratorFromExpression(qgisProject.mapLayersByName("plots")[0], expression)
if (it.hasNext()) {
const feature = it.next()
console.log(feature.id + " " + feature.attribute("plot_id"))
}
it.close();
}
return false
The Loader class has the "item" property that contains the item loaded from its source component.
In our plugin component, we have defined a plotId property. Setting it directly on pluginLoader.item triggers a QML property binding in the plugin component, which automatically updates the display.
Remember to close the iterator, regardless of whether the feature is found or not!
Remember to return the boolean value for the pointHandler.
pointHandler.registerHandler("demo2_selection", (point, type, interactionType) => {
// ...
if (shouldHandle) {
// ...
if (it.hasNext()) {
// Get your feature
const feature = it.next()
// Close your iterator
it.close()
// Turn on the plugin, just like the camera button
pluginLoader.active = true
// Pass the plot ID to the plugin component
pluginLoader.item.plotId = feature.attribute("plot_id")
// Block the interaction signal
return true
}
// Close the iterator if you don't find a feature
it.close();
}
// Pass through the interaction
return false
});
parent changed from Demo 1In Demo 1 the root item used parent: iface.mapCanvas(). Here it uses parent: iface.mainWindow().contentItem instead. This is required because the plugin dialog needs to overlay the entire QField window, not just the map area inside it. mapCanvas() clips child items to the map rectangle, which would hide parts of the plugin UI. mainWindow().contentItem is the full-screen surface that all QField UI elements share.
// imports
Item {
id: plugin
parent: iface.mainWindow().contentItem
anchors.fill: parent
// Map Selection: 1. Hold a reference to the map canvas
property var mapCanvas: iface.mapCanvas()
// Map Selection: 2. Add the pointHandler to the plugin
property var pointHandler: iface.findItemByObjectName("pointHandler")
Loader {
id: pluginLoader
// ...
}
Component.onCompleted: {
// Map Selection: 3. Register the point handler and define its callback
pointHandler.registerHandler("demo2_selection", (point, type, interactionType) => {
// Decide on the interaction
var shouldHandle = (Qt.platform.os === "windows" && interactionType === "clicked") ||
(Qt.platform.os !== "windows" && interactionType === "doubleClicked")
if (shouldHandle) {
// Create a pair of points representing a 20m buffer area in which to search for features.
let tl = mapCanvas.mapSettings.screenToCoordinate(Qt.point(point.x - 20, point.y - 20))
let br = mapCanvas.mapSettings.screenToCoordinate(Qt.point(point.x + 20, point.y + 20))
// Perform a spatial query
let expression = "intersects(geom_from_wkt('POLYGON(("+tl.x+" "+tl.y+", "+br.x+" "+tl.y+", "+br.x+" "+br.y+", "+tl.x+" "+br.y+", "+tl.x+" "+tl.y+"))'), $geometry)"
let it = LayerUtils.createFeatureIteratorFromExpression(qgisProject.mapLayersByName("plots")[0], expression)
if (it.hasNext()) {
// You have a feature, play with it! :)
const feature = it.next()
console.log(feature.id)
it.close()
pluginLoader.active = true
// Pass the plot ID to the plugin component
pluginLoader.item.plotId = feature.attribute("plot_id")
return true
}
it.close();
}
return false
});
}
// Map Selection: 4. Deregister the point handler on destruction (should be on project close)
Component.onDestruction: {
pointHandler.deregisterHandler("demo2_selection");
}
}
That was a lot. Let's look at how this plot ID gets to the MessageBox. This part is pretty simple.