Skip to content

Instantly share code, notes, and snippets.

@mhugo
Created July 22, 2016 09:45
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mhugo/f809970c952f318cb2abc57af1a2446d to your computer and use it in GitHub Desktop.
Save mhugo/f809970c952f318cb2abc57af1a2446d to your computer and use it in GitHub Desktop.
#!/usr/bin/env python
# This set of classes demonstrate how to implement a defered renderer with QT
#
# The idea is to be source compatible with existing QPainter operations (drawXXX, setBrush, etc.)
# and add extra commands that allow to "insert back" operations in the pipeline before they are actually rendered.
#
# This is a possible component for the "symbol clipping" feature of QGIS
#
# The rendering is split into "rendering layers" that correspond to "symbol layers" of QGIS.
# Each rendering layer is made of a QPicture allowing to serialize painting commands and a clipping path defined as a QPainterPath
#
# The augmented MyPainter class has two new methods:
# * setRenderingLayer(name): switch to another rendering layer with a given name
# * addToClickMask(path, [names]): add to the clipping mask of each layer given in parameter
# * paintOn(device): unstack every painting commands and render them on the given device (replaces end())
import sys
from PyQt4.QtCore import *
from PyQt4.QtGui import *
class RenderingLayer:
def __init__(self, name):
self._picture = QPicture()
self._name = name
self._clip_mask = QPainterPath()
def picture(self):
return self._picture
def name(self):
return self._name
def mask(self):
return self._clip_mask
def add_to_mask(self, path):
self._clip_mask = self._clip_mask.united(path)
class MyPainter(QPainter):
# QPainter augmented with two methods
def __init__(self):
# rendering layers
self.layers = [RenderingLayer("main")]
self.current_layer = self.layers[0]
self.layer = {"main":self.current_layer}
QPainter.__init__(self, self.current_layer.picture())
def setRenderingLayer(self, name):
"Sets the current layer where every following painting commands will be part of"
if self.current_layer.name() == name:
return
# switch to another layer
self.current_layer = RenderingLayer(name)
self.layer[name] = self.current_layer
self.layers.append(self.current_layer)
self.end()
self.begin(self.current_layer.picture())
def addToClipMask(self, path, layers):
"""Sets the clip mask for a set of layers
:param path: a QPainterPath
:param layers: a list of layers defined by setRenderingLayer
"""
for layer in layers:
self.layer[layer].add_to_mask(path)
# to be called instead of end()
def paintOn(self, device):
print "paintOn"
self.end()
painter = QPainter(device)
for layer in self.layers:
print "rendering layer", layer.name()
# "invert" the mask
path = QPainterPath()
path.addRect(0, 0, device.width(), device.height())
path = path.subtracted(layer.mask())
painter.setClipPath(path)
# replay painting commands
layer.picture().play(painter)
class Window(QWidget):
def __init__(self):
QWidget.__init__(self)
def paintEvent(self, event):
painter = MyPainter()
painter.setRenderHint(QPainter.Antialiasing)
painter.fillRect(event.rect(), QBrush(QColor("white")))
painter.fillRect(QRect(0, 0, 100, 100), QBrush(QColor("green")))
# tag any following commands as part of the "L1" layer (simulate symbol layer rendering)
painter.setRenderingLayer("L1")
painter.setPen(QPen(QBrush(QColor("black")), 6))
painter.drawLine(0, 0, 20, 20)
painter.drawLine(20, 20, 50, 20)
painter.drawLine(50, 20, 70, 70)
painter.setPen(QPen(QBrush(QColor("yellow")), 3))
painter.drawLine(0, 0, 20, 20)
painter.drawLine(20, 20, 50, 20)
painter.drawLine(50, 20, 70, 70)
# tag any following commands as part of the "L2" layer (~ label "layer")
painter.setRenderingLayer("L2")
mask = QPainterPath()
mask.addRect(40, 25, 40, 20)
painter.drawText(40, 40, "Label")
# Set the clipping mask for the current layer
painter.addToClipMask(mask, ["L1"])
mask = QPainterPath()
mask.addRect(0, 5, 50, 20)
painter.drawText(0, 20, "Label2")
# Set the clipping mask for the current layer
painter.addToClipMask(mask, ["L1"])
# Actually render
painter.paintOn(self)
def sizeHint(self):
return QSize(500, 500)
if __name__ == "__main__":
app = QApplication(sys.argv)
window = Window()
window.show()
sys.exit(app.exec_())
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment