Cursor personalizado en plugin QGIS

Mar. 16, 2014

El pasado enero desarrollé un plugin QGIS llamado Transectizer. Afortunadamente fue catalogado y enviado a la caja (o quién sabe, al ataúd) de los plugins experimentales.

Custom cursor showing bearing

Un cursor parlanchín personalizado en QGIS, mostrando el rumbo del transecto que se está editando

Sí, digo afortunadamente porque esto me urgió a cambiar algunas de las características del plugin. Una de ellas es la capacidad del plugin de proporcionar cierto feedback  al usuario sobre una línea dibujada en el mapa, para hacer posible seleccionar interactivamente cierto parámetro que se necesita para que el plugin funcione.

En la primera versión del plugin QGIS (la segunda versión está próxima, ver más abajo), simplemente se actualiza el texto de un widget lineEdit dentro del diálogo del plugin QGIS. Hacer esto es una tarea relativamente simple, si ya has buceado entre la algo dispersa documentación disponible sobre desarrollo de plugins QGIS. EL primer lugar para empezar es, desde luego, el  repositorio de plugins QGIS donde existe una creciente documentación sobre programación de plugins en QGIS. Pero también existen sitios, como el blog de Anita Graser. donde se puede encontrar documentacion muy útil y, lo que es mejor, ejemplos. Y puedes escrutar el código fuente de otros plugins para ver cómo otros han resuelto el mismo o parecidos problemas a los que te enfrentas.

Pero en la siguiente versión, que será lanzada en un par de semanas si  Mr. Pareto me lo permite, deseo tener esa información en el cursor del ratón, de manera que el plugin sea más agradable y fácil de usar. Y para conseguirlo, debes ser capaz de cambiar el puntero del ratón.

Primer paso. Cambiar la forma del puntero del ratón de tu plugin QGIS

Atención: Este artículo ha sido escrito por un program (ni siquiera llego a programador) amateur y autodidacta. Por favor, no seas demasiado duro conmigo, sobre todo si la terminología relacionada con la programación no es la apropiada. Gracias.

Para empezar, todo esto tiene sentido sólo si tu plugin QGIS necesita interactuar con el mapa. En este caso, tu plugin debe tener activa una QgsMapTool para realizar interacciones con el mapa. Esto está  muy bien explicado en  el pyQGIS cookbook, y, para simplificar, lo que has de hacer es crear una subclase a partir de QgsMapTool o una de sus clases derivadas, dependiendo de la utilidad que quieras darle( una herramienta zoom, una para identificar o para emitir puntos, entre otras) e implementar el código para el cambio de cursor dentro de la nueva clase. Normalmente, querrás cambiar el cursor en respuesta a una interacción con el mapa, de manera que la manera más natural de lograrlo será reimplementando los métódos de clase relacionados con los eventos de ratón (canvasMoveEvent, canvasClickEvent, etc.):

class myMapTool((QgsMapToolEmitPoint):

def \_\_init\_\_(self, canvas):
     QgsMapToolEmitPoint.\_\_init\_\_(self, self.canvas)

def canvasPressEvent(self, event):
    #Aquí hago algo

def canvasReleaseEvent(self,event):
    #Y aquí hago otra cosa

Ahora tienes que efectuar el cambio de cursor. Para hacerlo, primero echa un vistazo a la documentación de PyQt4 acerca de QCursor. Ahora, has de tener en cuenta que el cursor, en sí mismo, pertenece al objeto mapCanvas que le habrás pasado a tu clase myMapTool al crearlo. De manera que simplemente tienes que obtener una referencia al mismo y cambiar el cursor a través del método setCursor de la clase qgsMapCanvas, reemplazando así el cursor por el que tú quieras. En el siguiente ejemplo, vamos a establecer un mapa de bits personalizado para el cursor cuando el usuario haga click sobre el mapa, y volveremos al cursor anterior cuando libere el botón:

# Estableciendo el cursor en un plugin QGIS. Puedes crear el pixmap

y el QCursor en el método __init__ de la mapTool,

pero si quieres cambiarlo dinámicamente, creo que es mejor crearlo

dentro de los métodos de control de eventos

Implementación del método canvasPressEvente que

cambiará la forma del cursor por una personalizada

def canvasPressEvent(self, event):

Primero cargamos el cursor

self.cursorPixmap = QPixmap('/mypixmap.png')

Ahora creamos el nuevo cursor

self.cursor = QCursor(self.cursorPix)

Puede ser de utilidad almacenar el cursor actual, antes

de cambiarlo

self.stdCursor = self.parent().cursor()

Y finalmente establecemos nuestro recién creado cursor

como cursor del ancestro de nuestra mapTool

self.parent().setCursor(self.cursor)

Ahora viene la implementación del método canavsReleaseEvent

que restaura el cursor anterior

def canvasReleaseEvent(self, event):

Simplemente establecemos el cursor del ancestro

de nuestra mapTool al cursor anterior

self.parent().setCursor(self.stdCursor)

 Y ahora… cursores parlanchines en un plugin QGIS

Bien, cambiar el cursor es bastante fácil y también lo es hacerlo dicharachero o, mejor dicho, informativo. En el caso del plugin Transectizer, lo que quiero es mostrar el rumbo de la línea que se está editando en el cursor, o a lo mejor lo que quieres es mostrar una distancia, las coordenadas del cursor o lo que sea pero, en cualquier caso, en tiempo real.

De manera que ¿qué tenemos que hacer? Bien, es bastante simple. Tenemos que implementar el método canvasMoveEvent en nuestra mapTool, haciendo que el cursor cambie cada vez que el método es llamado. Y lo que hacemos es pintar sobre el pixmap del cursor la información que queremos mostrar.

Para lograrlo, tenemos que crear un QPainter en nuestro pixmap y proceder de la misma manera que en el método canvasPressEvent: primero creamos el QCursor y luego asignamos el cursor a nuestro mapCanvas:

# Actualización en tiempo real del cursor de una QGIS mapTool

haciendo uso de su método canvasMoveEvent

def canvasMoveEvent(self, event): # Por supuesto, necesitamos alguna información que mostrar. # Aquí el parámetro event pasado al llamar al método # nos puede proporcionar las coordenadas del cursor del ratón # y pueden ser usadas para realizar cálculos # Por ejemplo, si tuviéramos una función calculateBearing # en nuestro código que nos devolviera el rumbo desde el # punto (0,0) hasta nuestra posición actual, podríamos hacer: x = event.pos().x() y = event.pos().x() bearing = calculateBearing(x,y)

# Para crear the pixmap para el cursor, hay dos posibilidades:
# Cargar un pixmap prediseñado:
self.cursorPix = QPixmap('/myCursorPixmap.png')

# O podemos instanciar un QPixmap desde cero. En este caso,
# no te olvides de que su tamaño ha de ser compatible con
# los tamaños permitidos por el sistema. Un tamaño de 32x32
# pixels es el más compatible.
self.cursorPix = QPixmap(32,32)

# En este último caso, la documentación especifica que es preciso
# rellenar el pixmap con un color antes de proceder a pintar
# sobre él. En nuestro caso, parece conveniente que sea
# transparente.
self.cursorPix.fill(Qt.transparent)

# Para pintar la información sobre el pixmap, 
# necesitamos crear un QPainter de esta manera:
cursorPainter=QPainter(self.cursorPix)

# Ahora dibujamos los textos sobre el pixmap. 
# Debes tener cuidado con el tamaño de los textos.
# Consulta la documentación de PyQt4 sobre el particular
font = QFont()
font.setPixelSize(8)
cursorPainter.setFont(font)
cursorPainter.drawText(0,0,32,32,Qt.AlignHCenter,str('BRG'))

# Ahora pintamos el rumbo calculado al principio:

cursorPainter.drawText(0,24,32,32,Qt.AlignHCenter,str('%.1f' %bearing))

# Y finalmente, asignamos nuestro nuevo pixmap al cursor
# del mapCanvas subyacente
self.cursor = QCursor(self.cursorPix)
self.parent().setCursor(self.cursor)

Bueno… eso es todo. Puedes usarlo para mejorar tu plugin en buena medida mediante la interacción con el mapa, dando simple pero atractivo feedback a sus usuarios.

Tengo pensado comprobar si cursores de grandes dimensiones pueden ser usados en QGIS, para mostrar el juego completo de coordenadas de un punto, por ejemplo. O, si quieres una idea rápida, para mejorar el plugin ValueTool de manera que muestre el valor de capa en el propio cursor y no en un widget.

¡¡Espero que te sea de utilidad!!Last january I developed QGIS plugin called Transectizer. Fortunately it was catalogued and sent to the box (or, who knows if a coffin) of the expermiental plugins.

[caption id=“attachment_667” align=“alignleft” width=“300”]Custom cursor showing bearing A custom, chatty QGIS plugin cursor showing the bearing of the rubberband being drawn.[/caption]

Yes, I said fortunately, because that prompted me towards changing some features of the plugin. One of these features is the ability of the plugin of give some feedback to the user about the bearing of a line drawn in the canvas, to make possible to interactively choose a certain parameter that is needed for the plugin to work.

In the first version of the plugin (the second is upcoming, see below), it just updates the text of a LineEdit widget inside the QGIS plugin dialog. This is a relatively simple task, if you have already dived into the somehow spread information available on the internet about QGIS plugin development. The first place to start is, of course the QGIS plugin repository where there is a growing documentation about plugin programming in QGIS, but Anita Graser’s Blog, for instance, are places where you can find very useful documentation and, ever better, examples. Also you can dig in other plugins to see how others have solved the same ,or similar, problems that you are facing.

But in the next version, which will be released in two weeks or less if Mr. Pareto lets me do it, I want to get that information in the mouse cursor, so the plugin use is more comfortable and easy to use. And to get that kind of behavior, you must change your mouse’s cursor shape.

First step: change the shape of your QGIS plugin mouse cursor

WARNING: This article has been written by an amateur, self-taught  programme (not even a complete programmer, you know)… please don’t blame on me too much if the programming-related terminology is not accurate. Thanks.

First of all, it is useful only if your QGIS plugin needs to interact with the map canvas. In that case, your plugin must have a QgsMapTool map tool enabled to perform canvas interactions. This is very well explained in the pyQGIS cookbook, and, to simplify, what you have to do is to subclass QgsMapTool or one of its derived classes, depending on what you want to achieve (a zoom tool, an identifier tool or a point emmiting tool, among others) and implement the cursor changing code inside the new class. Usually you’ll want to do change the cursor in response to a canvas interaction, so the natural way of achieving this is to override the mouse-event related methods of the these classes (canvasMoveEvent, canvasClickEvent, etc.):

class myMapTool((QgsMapToolEmitPoint):

def \_\_init\_\_(self, canvas):
     QgsMapToolEmitPoint.\_\_init\_\_(self, self.canvas)

def canvasPressEvent(self, event):
    #Do something

def canvasReleaseEvent(self,event):
    #Do something again

Now you have to change the cursor. To make this first, take a look of the Pyqt4 documentation about QCursor. Now, bear in mind that the cursor itself belongs, say so, to the mapCanvas which you passed to your mapTool in its creation. So you have just to get a reference to it and change the cursor through the mapCanvas method setCursor, replacing the cursor for the one you want. In the following example, we are going to set a custom pixmap for the cursor when the user clicks in the canvas and set it back to a standard cursor when the button is released.

# Setting the cursor in QGIS plugin. You can create the

Qpixmap and QCursor in the __init__ mehtod of the plugin,

but if you want to change it dinamically I think is better

in the event driving methods.

Implementing the canvasPressEvent which

changes the cursor shape for a custom one

def canvasPressEvent(self, event):

First we have to load the pixmap for the cursor

self.cursorPixmap = QPixmap('/mypixmap.png')

#Now we create a QCursor with the pixmap self.cursor = QCursor(self.cursorPix)

#Maybe it is useful to store the previous cursor self.stdCursor = self.parent().cursor()

And finally we set the mapTool’s parent cursor

self.parent().setCursor(self.cursor)

Now comes the implementation of the mouse release event

which restores the previous cursor

def canvasReleaseEvent(self, event):

Just set the mapTool’s parent cursor to

the previous cursor

self.parent().setCursor(self.stdCursor)

 And now… chatty cursors in QGIS plugin

Well, changing the cursor is pretty easy and so is to make our cursor talkative or, better informative. In the case of the Transectizer QGIS plugin, what I wanted was to show the bearing of the line currently being edited in the cursor or maybe you want to show a distance, the cursor coordinates or whatever but, in any case, in real time.

So… what have we to do? Well, that’s pretty simple. We have to implement the method canvasMoveEvent in our mapTool making it change the cursor every time it is called. And what we do is to paint over our cursor’s pixmap the information we want to show.

To perform this, we have to create a QPainter on our pixmap and then act as we did in the canvasPressEvent: first, create the QCursor and then assigning our cursor to our canvas:

# Real-time update of QGIS mapTool cursor using

its canvasMoveEvent method

def canvasMoveEvent(self, event): # We need some information to show, of course. # Here, the event parameter got with the event # can provide us with the current coordinates of the mouse, and they can be used to perform useful calculations # For instance, if we had a calculateBearing function implemented # in our code which returs the bearing from (0,0) to our current # position, we could do: x = event.pos().x() y = event.pos().x() bearing = calculateBearing(x,y)

# To create the pixmap for the cursor, we have
# two alternatives: Load a pre-designed pixmap:
self.cursorPix = QPixmap('/myCursorPixmap.png')

# Or we can instantiate a QPixmap from scracth, In this case,
# don't forget to make it in a size compatible with the
# cursor size (32x32 pixels is the most compatible one)
self.cursorPix = QPixmap(32,32)

# In this last case, documentation says that you must fill the
# pixmap prior to paint over it. In our case, it looks convenient
# to fill it with transparent color
self.cursorpix.fill(Qt.transparent)

# To paint the information we want to show over the pixmap.
# We need to create a QPainter to do this:
cursorPainter=QPainter(self.cursorPix)
# Now we draw the texts over the pixmap. 
# You must take care about the size of the text!
# Please refer to PyQt4 documentation for information about this
font = QFont()
font.setPixelSize(8)
cursorPainter.setFont(font)
cursorPainter.drawText(0,0,32,32,Qt.AlignHCenter,str('BRG'))

# And now we draw the bearing calculated at the begginning:

cursorPainter.drawText(0,24,32,32,Qt.AlignHCenter,str('%.1f' %bearing))

# And finally, we assign our pixmap to the tool's parent
# (the underlying canvas) cursor
self.cursor = QCursor(self.cursorPix)
self.parent().setCursor(self.cursor)

Well, and that’s all… you could use it to improve a lot your plugins  with canvas interaction, giving simple but eye-candy feedback to your users.

I have to check if big cursors can be used in QGIS, for instance to show the whole set of coordinates of a point. Or, to make it inmediate, to improve the ValueTool plugin to show the value in the cursor itself, rather than in a widget.

I hope that it would be useful to you in your plugins!!!