Canvas Render - How to?

I have been reading documentation, sample scripts and plugins. but I am only more confused honestly.

My objective is to render something on canvas and for different use cases:

  1. Display something on canvas and this does not block any interactions and is untouchable to the mouse and brush strokes go through as it is just visual information until it is closed.
  2. Place a widget on canvas that can be interacted with and once you leave it closes. This would be like making a HUD.

in both cases I need to know where is the mouse at on the canvas. I done it before but I think I really did it wrong as it would break very easily or krita itself.

I think most of my confusion steams from the fact that there is a bunch of unknown widgets I never heard of before and their hierarchy to one another. And what kind of method do you use to render in each case. is it OpenGl or Paint Events? or is it something else? and if I want to render how should I do it?

2 Likes
  1. If it is just a HUD, you can make it transparent for both mouse and keyboard events.
selectorWidget.setWindowFlags(Qt.WindowTransparentForInput)
selectorWidget.setAttribute( Qt.WA_TransparentForMouseEvents )

(This is how I do the developer tools selector so that you can continue clicking despite the overlay)

  1. As for how to make it interacted with, you just overlay a widget. If you want something more fancy, then a QGraphicsScene/View. (Like LazyTextTool). And as for closing it, the easiest way is an event filter on the mdi.

From ShapesAndLayers (show eraser)

self.mdiFilter = self.mdiFilterClass(self)
self.mdi.viewport().installEventFilter(self.mdiFilter)

    class mdiFilterClass(QWidget):
        def __init__(self, caller, parent=None):
            super().__init__()
            self.caller = caller
            
        def eventFilter(self, obj, event):
            if event.type() == 10:
                #print ("Event", event.type(), obj)
                self.caller.setEraserCursor(1)
            elif event.type() == 11:
                #print ("Event", event.type(), obj)
                self.caller.setEraserCursor(0)
                
            return False
1 Like

Honestly I don’t even know what a QGraphicsScene or View is yet I need to read about it.

So I should place all my things on the QMdiArea then?

I was doing some odd tests on the scripter, it is not something like this is it? or do I have to add a child and move it around to render things on it?

I was thinking maybe render in the QMdiArea here?

from krita import *
from PyQt5 import *

class mdiFilterClass(QWidget):
    def __init__(self, parent=None):
        super().__init__()

    def eventFilter(self, obj, event):
        if event.type() == 10:
            QtCore.qDebug("10")
        elif event.type() == 11:
            QtCore.qDebug("11")

        return False

    def paintEvent(self, event):
        painter = QPainter(self)
        painter.setRenderHint(QtGui.QPainter.Antialiasing, True)

        painter.setPen(QtCore.Qt.NoPen)
        painter.setBrush(QBrush(QColor(150,0,0)))
        painter.drawRect(event.x(),event.y(),200,200)

qwin = Krita.instance().activeWindow().qwindow()
mdi = qwin.centralWidget().findChild(QtWidgets.QMdiArea)
mdiFilter = mdiFilterClass(mdi)
mdi.viewport().installEventFilter(mdiFilter)

Krita doesn’t use QGraphicsScene at all, so no need to worry about that.

(QGraphicsScene and QGraphicsView apparently were originally developed to make it possible to create maps for applications for the oil industry. Back when we ported KOffice to Qt4, we were told we probably should implement the document classes for the various apps as a QGraphicsScene. In the early Nokia days, it was repurposed to implement first a completely new type of widget system next to qwidget, and then as the basis for qml – before qml was ported to a scenegraph.)

3 Likes

Another tricky question is how do I render something that rotates with a canvas?

It is mostly a way to do all kinds of items and transformations like rotating, resizing and it handles a lot of the item management, like moving stuff across the scene. But if you need just a HUD, a simple QWidget would work fine.

That depends on what you want to do specifically. QMdiArea is fine in many cases for an overlay, but if you plan to implement scrolling and the like, it would have to be in the scrollarea of the QMdiArea subwindow. But you would have to regenerate everything with each new subwindow. And take into account new window mode and tab mode.

I gave you an example of that before right? It creates a QGraphicsView/QGraphicsScene as a child of the subwindow scrollarea. But you can use a regular QWidget if you don’t plan to do anything fancy.

As for the filter on QMdiArea, it tells you when they entered or left. 10 is QEvent.Enter and 11 is QEvent.Leave so you can create entry and leave events.

I am pretty sure I saw a QGraphicsView for the live brush preview ( liveBrushPreviewView / KisPresetLivePreviewView )… but yes, I might be doing things the old way and probably should catch up… just there was a good decade of documentation for it.

1 Like

Not perfect, but can give some Ideas.

"""
tested in Krita 4.4.8 Win 10
Notes:
    create new document before running.
    coordinates are little bit wrong. (integer vs float pixels...)
"""

from PyQt5.QtCore import (
        Qt,
        QEvent,
        QPointF,
        QRect)

from PyQt5.QtGui import (
        QTransform,
        QPainter,
        QBrush,
        QColor,
        QPolygonF,
        QInputEvent)

from PyQt5.QtWidgets import (
        QWidget,
        QMdiArea,
        QAbstractScrollArea)


def get_q_view(view):
    window = view.window()
    q_window = window.qwindow()
    q_stacked_widget = q_window.centralWidget()
    q_mdi_area = q_stacked_widget.findChild(QMdiArea)
    for v, q_mdi_view in zip(window.views(), q_mdi_area.subWindowList()):
        if v == view:
            return q_mdi_view.widget()


def get_transform(view):
    def _offset(scroller):
        mid = (scroller.minimum() + scroller.maximum()) / 2.0
        return -(scroller.value() - mid)
    canvas = view.canvas()
    document = view.document()
    q_view = get_q_view(view)
    area = q_view.findChild(QAbstractScrollArea)
    zoom = (canvas.zoomLevel() * 72.0) / document.resolution()
    transform = QTransform()
    transform.translate(
            _offset(area.horizontalScrollBar()),
            _offset(area.verticalScrollBar()))
    transform.rotate(canvas.rotation())
    transform.scale(zoom, zoom)
    return transform


class MyOverlay(QWidget):
    _degree = 6
    _colors = [
            QBrush(QColor(0, 0, 255)),
            QBrush(QColor(0, 255, 0)),
            QBrush(QColor(255, 0, 0)),
            QBrush(QColor(255, 0, 255)),
            QBrush(QColor(0, 255, 255)),
            QBrush(QColor(255, 255, 0))]

    def __init__(self, view):
        parent = get_q_view(view)
        super().__init__(parent)
        # ToDo: is there better way to ignore events?
        self.setAttribute(Qt.WA_TransparentForMouseEvents)
        self.setFocusPolicy(Qt.NoFocus)
        self._view = view
        q_canvas = parent.findChild(QAbstractScrollArea).viewport()
        size = q_canvas.size()
        self.setGeometry(QRect(0, 0, size.width(), size.height()))
        parent.installEventFilter(self)

    def paintEvent(self, e):
        document = self._view.document()
        width = float(document.width())
        height = float(document.height())
        half_width = 0.5 * width
        half_height = 0.5 * height
        triangle = (
                QPointF(-half_width, half_height),
                QPointF(0.0, -half_height),
                QPointF(half_width, half_height))
        # Turtles all the way down
        painter = QPainter(self)
        try:
            painter.setRenderHint(QPainter.Antialiasing, True)
            painter.translate(self.rect().center())
            painter.setTransform(get_transform(self._view), combine=True)
            painter.setPen(Qt.NoPen)
            self.draw_sierpinski(painter, triangle, self._degree)
        finally:
            painter.end()

    def draw_sierpinski(self, painter, triangle, degree):
        painter.setBrush(self._colors[degree % len(self._colors)])
        polygon = QPolygonF()
        polygon.append(triangle[0])
        polygon.append(triangle[1])
        polygon.append(triangle[2])
        painter.drawConvexPolygon(polygon)
        if degree > 0:
            next_degree = degree - 1
            mid_1 = 0.5 * (triangle[0] + triangle[1])
            mid_2 = 0.5 * (triangle[0] + triangle[2])
            mid_3 = 0.5 * (triangle[1] + triangle[2])
            self.draw_sierpinski(painter, (triangle[0], mid_1, mid_2), next_degree)
            self.draw_sierpinski(painter, (triangle[1], mid_1, mid_3), next_degree)
            self.draw_sierpinski(painter, (triangle[2], mid_3, mid_2), next_degree)

    def eventFilter(self, obj, e):
        if e.type() == QEvent.Resize:
            q_canvas = self.parent().findChild(QAbstractScrollArea).viewport()
            size = q_canvas.size()
            self.setGeometry(QRect(0, 0, size.width(), size.height()))
        return super().eventFilter(obj, e)


active_window = Application.activeWindow()
active_view = active_window.activeView()
if active_view.document() is None:
    raise RuntimeError('Document of active view is None!')
my_overlay = MyOverlay(active_view)
my_overlay.show()

/AkiR

3 Likes

Just to point out a few quick things, for the mdiarea, it is better to cache it rather than calling it every time, also, it should be noted that if someone has subwindows or detached canvas, that won’t work as mdiarea will not be part of the centralwidget (yes I keep forgetting that too until people point out the plugin breaks :frowning: ). Also, is there really a point of doing findChild when you have direct access? A for loop would be faster.

And yes positions are off especially when zooming.

But that should be a good start for EyeOdin to go by.

1 Like

I think this is the type of behavior I was looking for for case one. I need to study this and experiment.
To do a clickable widget is to just eliminate Qt.WA_TransparentForMouseEvents and add the events?

Was trying to see if I could make this work with triggering something like the eraser or some other command in real time and not get crossed with swapping documents. Found an odd bug doing this with file name versioning and Python reading it.

Tela on the right is showing what is being hidden by the sampled code.

eraser_canvas_rander

Different documents in windows mode swapping the events. Was not able to find improper behavior yet.

eraser_canvas_windows

TransparentForMouseEvents really blocks all inputs. with a installed Mouse event it should be clickable.

I will need to study this with time to absorb but it works and read more not to mention do experimentation.
I will have to see how to cache this better as referred above and all that but at least it is not a complete mystery now. thank you so much.

Hi all, I’m playing with AkiR’s code now, which is very helpful.
The code works well using Scripter, but when I make it into an extension, it fails to work. It’s a straightforward combination of AkiR’s code and the extension example given in Krita’s Manual.

When I load the plugin and click the menu tools->My Plugin, I see that eventFilter function is executed only once and draw_sierpinski is not called at all. So what’s wrong??

from krita import Krita, Extension
from PyQt5.QtCore import (
        Qt,
        QEvent,
        QPointF,
        QRect)

from PyQt5.QtGui import (
        QTransform,
        QPainter,
        QBrush,
        QColor,
        QPolygonF,
        QInputEvent)

from PyQt5.QtWidgets import (
        QMenu,
        QAction,
        QWidget,
        QMdiArea,
        QAbstractScrollArea)


def get_q_view(view):
    window = view.window()
    q_window = window.qwindow()
    q_stacked_widget = q_window.centralWidget()
    q_mdi_area = q_stacked_widget.findChild(QMdiArea)
    for v, q_mdi_view in zip(window.views(), q_mdi_area.subWindowList()):
        if v == view:
            return q_mdi_view.widget()


def get_transform(view):
    def _offset(scroller):
        mid = (scroller.minimum() + scroller.maximum()) / 2.0
        return -(scroller.value() - mid)
    canvas = view.canvas()
    document = view.document()
    q_view = get_q_view(view)
    area = q_view.findChild(QAbstractScrollArea)
    zoom = (canvas.zoomLevel() * 72.0) / document.resolution()
    transform = QTransform()
    transform.translate(
            _offset(area.horizontalScrollBar()),
            _offset(area.verticalScrollBar()))
    transform.rotate(canvas.rotation())
    transform.scale(zoom, zoom)
    return transform


class MyOverlay(QWidget):
    _degree = 6
    _colors = [
            QBrush(QColor(0, 0, 255)),
            QBrush(QColor(0, 255, 0)),
            QBrush(QColor(255, 0, 0)),
            QBrush(QColor(255, 0, 255)),
            QBrush(QColor(0, 255, 255)),
            QBrush(QColor(255, 255, 0))]

    def __init__(self, view):
        print("init")
        parent = get_q_view(view)
        super().__init__(parent)
        # ToDo: is there better way to ignore events?
        self.setAttribute(Qt.WA_TransparentForMouseEvents)
        self.setFocusPolicy(Qt.NoFocus)
        self._view = view
        q_canvas = parent.findChild(QAbstractScrollArea).viewport()
        size = q_canvas.size()
        self.setGeometry(QRect(0, 0, size.width(), size.height()))
        parent.installEventFilter(self)

    def paintEvent(self, e):
        print("paintEvent")
        document = self._view.document()
        width = float(document.width())
        height = float(document.height())
        half_width = 0.5 * width
        half_height = 0.5 * height
        triangle = (
                QPointF(-half_width, half_height),
                QPointF(0.0, -half_height),
                QPointF(half_width, half_height))
        # Turtles all the way down
        painter = QPainter(self)
        try:
            painter.setRenderHint(QPainter.Antialiasing, True)
            painter.translate(self.rect().center())
            painter.setTransform(get_transform(self._view), combine=True)
            painter.setPen(Qt.NoPen)
            self.draw_sierpinski(painter, triangle, self._degree)
        finally:
            painter.end()

    def draw_sierpinski(self, painter, triangle, degree):
        print("Sierpinski")
        painter.setBrush(self._colors[degree % len(self._colors)])
        polygon = QPolygonF()
        polygon.append(triangle[0])
        polygon.append(triangle[1])
        polygon.append(triangle[2])
        painter.drawConvexPolygon(polygon)
        if degree > 0:
            next_degree = degree - 1
            mid_1 = 0.5 * (triangle[0] + triangle[1])
            mid_2 = 0.5 * (triangle[0] + triangle[2])
            mid_3 = 0.5 * (triangle[1] + triangle[2])
            self.draw_sierpinski(painter, (triangle[0], mid_1, mid_2), next_degree)
            self.draw_sierpinski(painter, (triangle[1], mid_1, mid_3), next_degree)
            self.draw_sierpinski(painter, (triangle[2], mid_3, mid_2), next_degree)

    def eventFilter(self, obj, e):
        print("eventFilter")
        if e.type() == QEvent.Resize:
            q_canvas = self.parent().findChild(QAbstractScrollArea).viewport()
            size = q_canvas.size()
            self.setGeometry(QRect(0, 0, size.width(), size.height()))
        return super().eventFilter(obj, e)


def drawAction():
    print("drawAction")
    active_window = Application.activeWindow()
    active_view = active_window.activeView()
    if active_view.document() is None:
        raise RuntimeError('Document of active view is None!')
    my_overlay = MyOverlay(active_view)
    my_overlay.show()


class MyExtension(Extension):

    def __init__(self, parent):
        super().__init__(parent)

    def setup(self):
        pass

    def createActions(self, window):
        action = window.createAction("myplugin", i18n("My Plugin"), "tools")
        action.triggered.connect(drawAction)


Krita.instance().addExtension(MyExtension(Krita.instance()))

Oh, the problem can be more specific. In Scripter, if you put the following part

    active_window = Application.activeWindow()
    active_view = active_window.activeView()
    if active_view.document() is None:
        raise RuntimeError('Document of active view is None!')
    my_overlay = MyOverlay(active_view)
    my_overlay.show()

in a function, and then call that function, it will not work as expected. I don’t know why.

def draw():
    active_window = Application.activeWindow()
    active_view = active_window.activeView()
    if active_view.document() is None:
        raise RuntimeError('Document of active view is None!')
    my_overlay = MyOverlay(active_view)
    my_overlay.show()

draw()

It looks like that python instance gets deleted at end of function.

def draw():
    active_window = Application.activeWindow()
    active_view = active_window.activeView()
    if active_view.document() is None:
        raise RuntimeError('Document of active view is None!')
    my_overlay = MyOverlay(active_view)
    my_overlay.show()
    # instance my_overlay gets destroyed at
    # end of draw() function, and Qt automatically
    # destroys it also at C side.
    #
    # One way to keep instance alive is to attach it to something
    # that is more persistent, like class definition.
    MyOverlay._still_alive_instance = my_overlay
    
draw()

one good place to keep instances alive is the Extension instance.
self._my_overlay is instance attribute of Extension, so it kept alive by Krita.

class MyExtension(Extension):

    def __init__(self, parent):
        super().__init__(parent)

    def setup(self):
        pass

    def createActions(self, window):
        action = window.createAction("myplugin", i18n("My Plugin"), "tools")
        action.triggered.connect(self.drawAction)
    
    def drawAction(self):
        active_window = Application.activeWindow()
        active_view = active_window.activeView()
        if active_view.document() is None:
            raise RuntimeError('Document of active view is None!')
        self._my_overlay = MyOverlay(active_view)
        self._my_overlay.show()

/AkiR

1 Like

thanks, Now I understand my mistake here.