Adding Signals/Slots (Qobject) to Qgraphicsitem: Performance Hit

How can I add signal to QGraphicsEllipseItem?

In general, PyQt doesn't allow multiple inheritance of Qt classes, so there are only two alternatives.

QObject "proxy" as instance attribute

Create a subclass of QObject that acts as a signal "proxy", then add an instance of the subclass as an instance attribute of the graphics item:

class KnobSignalProxy(QObject): changePos = pyqtSignal(QPointF)

class Knob(QGraphicsEllipseItem):
def __init__(self, x, y, d, maxRadius):
super(QGraphicsEllipseItem, self).__init__(0, 0, d, d)
self._proxy = KnobSignalProxy()
self.changePos = self._proxy.changePos
# ...

This is the most basic solution, it has the benefit of allowing direct access to all the methods of the graphics items, most importantly those of the QGraphicsEllipseItem (setRect, setPen, etc.).

The drawback is that the signal is not actually emitted by the graphics item, so in case you need to use self.sender() (which should be avoided anyway, if possible), you cannot directly know the item that sent the signal.

A possible workaround would be to create a property or attribute on the KnobSignalProxy instance as a reference to the graphics item:

        self._proxy.item = self

# ...

def someFunction(self, pos):
item = self.sender().item

Note that, in order to receive mouse move events, you must implement mousePressEvent():

The mouse press event decides which item should become the mouse grabber (see QGraphicsScene::mouseGrabberItem()). If you do not reimplement this function, the press event will propagate to any topmost item beneath this item, and no other mouse events will be delivered to this item.

QGraphicsObject with a child item

In this case the graphics item added is a QGraphicsObject (which is a QGraphicsItem that also inherits from QObject), and the ellipse item is actually its child. Since QGraphicsObject is a base class as QGraphicsItem, it requires that at least boundingRect and paint are implemented.

Obviously you're not interested in the painting (the QGraphicsEllipseItem will do it on its own), but the returned bounding rect has to use that of the child.

This approach has the benefit of using a more correct pattern from the OOP perspective, but doesn't directly expose the child item's method. In any case, you can just create a reference to those functions.

class Knob(QGraphicsObject):
changePos = pyqtSignal(QPointF)

def __init__(self, x, y, d, maxRadius):
super().__init__()
self.ellipseItem = QGraphicsEllipseItem(0, 0, d, d, self)
self.outPos = QPointF()
self.rect = self.ellipseItem.rect
self.setRect = self.ellipseItem.setRect
self.setPen = self.ellipseItem.setPen
# ...

def boundingRect(self):
return self.childrenBoundingRect()

def mousePressEvent(self, event):
pass

def mouseMoveEvent(self, event):
self.changePos.emit(self.outPos)

def paint(self, qp, opt, widget=None):
pass

Does large use of signals and slots affect application performance?

First of all, you should probably not put any slots in QThreads. QThreads aren't really meant to be derived from other than by reimplementing the run method and private methods (not signals!).

A QThread is conceptually a thread controller, not a thread itself. In most cases you should deal with QObjects. Start a thread, then move the object instance to that thread. That's the only way you'll get slots working correctly in the thread. Moving the thread instance (it is QObject-derived!) to the thread is a hack and bad style. Don't do that in spite of uninformed forum posts telling otherwise.

As to the rest of your question: a signal-slot call does not have to locate anything nor validate much. The "location" and "validation" is done when the connection is established. The main steps done at the time of the call are:

  1. Locking a signal-slot mutex from a pool.

  2. Iterating through the connection list.

  3. Performing the calls using either direct or queued calls.

Common Cost

Any signal-slot call always starts as a direct call in the signal's implementation generated by moc. An array of pointers-to-arguments of the signal is constructed on the stack. The arguments are not copied.

The signal then calls QMetaObject::activate, where the connection list mutex is acquired, and the list of connected slots is iterated, placing the call for each slot.

Direct Connections

Not much is done there, the slot is called by either directly calling QObject::qt_static_metacall obtained at the time the connection was established, or QObject::qt_metacall if the QMetaObject::connect was used to setup the connection. The latter allows dynamic creation of signals and slots.

Queued Connections

The arguments have to marshalled and copied, since the call has to be stored in an event queue and the signal must return. This is done by allocating an array of pointers to copies, and copy-consting each argument on the heap. The code to do that is really no-frills plain old C.

The queuing of the call is done within queued_activate. This is where the copy-construction is done.

The overhead of a queued call is always at least one heap allocation of QMetaCallEvent. If the call has any arguments, then a pointers-to-arguments array is allocated, and an extra allocation is done for each argument. For a call with n arguments, the cost given as a C expression is (n ? 2+n : 1) allocations. A return value for blocking calls is counter as an argument. Arguably, this aspect of Qt could be optimized down to one allocation for everything, but in real life it'd only matter if you're calling trivial methods.

Benchmark Results

Even a direct (non-queued) signal-slot call has a measurable overhead, but you have to choose your battles. Ease of architecting the code vs. performance. You do measure performance of your final application and identify bottlenecks, do you? If you do, you're likely to see that in real-life applications, signal-slot overheads play no role.

The only time signal-slot mechanism has significant overhead is if you're calling trivial functions. Say, if you'd call the trivial slot in the code below. It's a complete, stand-alone benchmark, so feel free to run it and see for yourself. The results on my machine were:

Warming up the caches...
trivial direct call took 3ms
nonTrivial direct call took 376ms
trivial direct signal-slot call took 158ms, 5166% longer than direct call.
nonTrivial direct signal-slot call took 548ms, 45% longer than direct call.
trivial queued signal-slot call took 2474ms, 1465% longer than direct signal-slot and 82366% longer than direct call.
nonTrivial queued signal-slot call took 2474ms, 416% longer than direct signal-slot and 653% longer than direct call.

What should be noted, perhaps, is that concatenating strings is quite fast :)

Note that I'm doing the calls via a function pointer, this is to prevent the compiler from optimizing out the direct calls to the addition function.

//main.cpp
#include <cstdio>
#include <QCoreApplication>
#include <QObject>
#include <QTimer>
#include <QElapsedTimer>
#include <QTextStream>

static const int n = 1000000;

class Test : public QObject
{
Q_OBJECT
public slots:
void trivial(int*, int, int);
void nonTrivial(QString*, const QString&, const QString&);
signals:
void trivialSignalD(int*, int, int);
void nonTrivialSignalD(QString*, const QString&, const QString &);
void trivialSignalQ(int*, int, int);
void nonTrivialSignalQ(QString*, const QString&, const QString &);
private slots:
void run();
private:
void benchmark(bool timed);
void testTrivial(void (Test::*)(int*,int,int));
void testNonTrivial(void (Test::*)(QString*,const QString&, const QString&));
public:
Test();
};

Test::Test()
{
connect(this, SIGNAL(trivialSignalD(int*,int,int)),
SLOT(trivial(int*,int,int)), Qt::DirectConnection);
connect(this, SIGNAL(nonTrivialSignalD(QString*,QString,QString)),
SLOT(nonTrivial(QString*,QString,QString)), Qt::DirectConnection);
connect(this, SIGNAL(trivialSignalQ(int*,int,int)),
SLOT(trivial(int*,int,int)), Qt::QueuedConnection);
connect(this, SIGNAL(nonTrivialSignalQ(QString*,QString,QString)),
SLOT(nonTrivial(QString*,QString,QString)), Qt::QueuedConnection);
QTimer::singleShot(100, this, SLOT(run()));
}

void Test::run()
{
// warm up the caches
benchmark(false);
// do the benchmark
benchmark(true);
}

void Test::trivial(int * c, int a, int b)
{
*c = a + b;
}

void Test::nonTrivial(QString * c, const QString & a, const QString & b)
{
*c = a + b;
}

void Test::testTrivial(void (Test::* method)(int*,int,int))
{
static int c;
int a = 1, b = 2;
for (int i = 0; i < n; ++i) {
(this->*method)(&c, a, b);
}
}

void Test::testNonTrivial(void (Test::* method)(QString*, const QString&, const QString&))
{
static QString c;
QString a(500, 'a');
QString b(500, 'b');
for (int i = 0; i < n; ++i) {
(this->*method)(&c, a, b);
}
}

static int pct(int a, int b)
{
return (100.0*a/b) - 100.0;
}

void Test::benchmark(bool timed)
{
const QEventLoop::ProcessEventsFlags evFlags =
QEventLoop::ExcludeUserInputEvents | QEventLoop::ExcludeSocketNotifiers;
QTextStream out(stdout);
QElapsedTimer timer;
quint64 t, nt, td, ntd, ts, nts;

if (!timed) out << "Warming up the caches..." << endl;

timer.start();
testTrivial(&Test::trivial);
t = timer.elapsed();
if (timed) out << "trivial direct call took " << t << "ms" << endl;

timer.start();
testNonTrivial(&Test::nonTrivial);
nt = timer.elapsed();
if (timed) out << "nonTrivial direct call took " << nt << "ms" << endl;

QCoreApplication::processEvents(evFlags);

timer.start();
testTrivial(&Test::trivialSignalD);
QCoreApplication::processEvents(evFlags);
td = timer.elapsed();
if (timed) {
out << "trivial direct signal-slot call took " << td << "ms, "
<< pct(td, t) << "% longer than direct call." << endl;
}

timer.start();
testNonTrivial(&Test::nonTrivialSignalD);
QCoreApplication::processEvents(evFlags);
ntd = timer.elapsed();
if (timed) {
out << "nonTrivial direct signal-slot call took " << ntd << "ms, "
<< pct(ntd, nt) << "% longer than direct call." << endl;
}

timer.start();
testTrivial(&Test::trivialSignalQ);
QCoreApplication::processEvents(evFlags);
ts = timer.elapsed();
if (timed) {
out << "trivial queued signal-slot call took " << ts << "ms, "
<< pct(ts, td) << "% longer than direct signal-slot and "
<< pct(ts, t) << "% longer than direct call." << endl;
}

timer.start();
testNonTrivial(&Test::nonTrivialSignalQ);
QCoreApplication::processEvents(evFlags);
nts = timer.elapsed();
if (timed) {
out << "nonTrivial queued signal-slot call took " << nts << "ms, "
<< pct(nts, ntd) << "% longer than direct signal-slot and "
<< pct(nts, nt) << "% longer than direct call." << endl;
}
}

int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
Test t;
return a.exec();
}

#include "main.moc"

Emit signals from QgraphicsItem

If you want signal/slot support for graphics items, you could use QGraphicsObject instead of QGraphicsItem. This would allow you to emit customs signals, like this:

class Node(QtGui.QGraphicsObject):
customSignal = QtCore.pyqtSignal(str)
...

@QtCore.pyqtSlot()
def notifyaction(self):
message = 'action1'
self.customSignal.emit(message)

To receive these custom signals, just connect an appropriate handler:

 item.customSignal.connect(scene.handleCustomSignal)

However, it may be simpler to avoid signals altogether and just call the scene directly. Every graphics item can access the scene its been added to via its scene method. So you could just do something as simple as this:

    @QtCore.pyqtSlot()    
def notifyaction(self):
message = 'action1'
scene = self.scene()
if scene is not None:
scene.handleItemAction(self, message)

And doing things this way means you don't have to connect up signals for every graphics item that is created.

Events and signals in Qt's QGraphicsItem: How is this *supposed* to work?

Signaling is not part of QGraphicItem because they do not inherit from QObjects. This was a design decision for performance reasons, to allow very large and fast scenes. If you decide you really need special cases for signals, QGraphicsWidget was created to fill this gap. It does inherit from QObject and lets you have a mixture of QWidget and QGraphicsItem functionality. Though it is recommended that you avoid this if your scenes are even moderately sizable.

Another option which might be relevant to your situation is to make use of the sceneEventFilter method. You can set one item to receive events for another and decide if they should be propagated or not:
http://www.riverbankcomputing.co.uk/static/Docs/PyQt4/html/qgraphicsitem.html#sceneEventFilter

One item can be set as the filter for multiple objects. And it can identify each individual item and event to respond to.

Generally though you should make use of the scene for coordination across its objects. That is already the pattern being used for events (the scene coordinating delivery of all events to the items).

Also, it seems that your option A is not possible because QGraphicsItem does not even have an emit method. You would need to compose a QObject instance inside it as a member and use that to emit signals. Something along the lines of myItem.qobject.emit(). Otherwise, you would have to inherit your own completely custom one from QGraphicsObject

Update 1: Addressing your main comment update

Your specific situation is a rectangle with "hot corners". I would see this being a custom QGraphicsItem. You would probably subclass QGraphicsRectItem, and then compose the child hot corner items inside as children items (setParentItem()). Now your rectangle item knows about its children and can act on them directly. You could set the rectangle item to be the sceneEventFilter for the children and handle their events directly. No need to go back up to the scene. Let all this logic live in the class.

Update 2: Addressing your added question #3

Propagating communications up beyond the scene to QWidget's has a couple approaches I can think of:

  1. This is a situation where you can consider if you want to use a QGraphicsObject subclass as your root item, and then compose the rest of your objects as children (the rect, then the hot corners as children of the rect). This would allow the object to emit signals. For clarity they would probably still be connected to the scene, and then the higher order container of the scene would connect to the scene. You would have to choose this approach on a case by case, depending on the complexity of your scene and whether the QGraphicsObject has any performance impact on it. You should probably avoid this if you will have a great number of these instances.
  2. You could define a callback for your rect class, for which the scene can set. Either something like: graphicsRect.resizedCallback as an attribute, or a setter graphicsRect.setResizedCallback(cbk). In your rect class, you would just call that when appropriate. If the callback it set, it can be used to call something on your scene directly. The rect class still has no knowledge of that logic. It just calls a callback.

Those are just some suggestions. I'm sure there are other ways.



Related Topics



Leave a reply



Submit