[PyKDE] rubberbanding a QCanvas (demo code)

Andrew Dalke adalke at mindspring.com
Wed May 15 14:52:01 BST 2002


I'm experimenting with QCanvas/QCanvasView.  I wanted to support
rubberbanding of items on the canvas.  I also wanted the view to
scroll automatically if the mouse moves outside of the canvas,
so the selection can be done in one action.  (Eg, similar to what
the QTable does.)

Attached is code that does it.  I'm pretty sure it isn't the
proper way to do it -- I was experimenting with mixins as a style
for handling complex actions, but I don't think that will work.
Still, it does some interesting things and I haven't seen similar
example code elsewhere.  (Eg, I had to look at the source code for
QTable to figure out what it did.)

So, umm, how do real GUI programmers do rubberbanding?  And lassoing?
I suppose I should look at QIconView's code for the first...

And I guessed at a function for how much to move the mouse during
the autoscrolling.  Any pointers to a more standard algorithm which
provides the expected response?  Plus, lassoing calls for movement
of the canvas *before* the mouse gets to the edge, since otherwise
you don't know what you're lassoing.

                    Andrew
                    dalke at dalkescientific.com

-------------- next part --------------
import sys
from qt import *
from qtcanvas import *

# Show off a rubber band in a canvasview

# Mixin with a canvasview
class ScrollMixin:
    def __init__(self):
        self.__scrolltimer = QTimer()
        QObject.connect(self.__scrolltimer,
                        SIGNAL("timeout()"),
                        self.__doscroll)
        self.__count = 0
    def contentsMousePressEvent(self, event):
        self.__scrolltimer.start(100, 0)
        self.__count += 1

    def contentsMouseReleaseEvent(self, event):
        self.__count -= 1
        if self.__count == 0:
            self.__scrolltimer.stop()

    def __doscroll(self):
        pos = QCursor.pos()
        pos = self.mapFromGlobal(pos)
        if pos.x() < 0:
            x = self.contentsX() + (pos.x() / 500.) * self.width()
        elif pos.x() > self.width():
            x = self.contentsX() + self.width() + \
                ((pos.x() - self.width())/ 500.) * self.width()
        else:
            x = self.contentsX()
        if pos.y() < 0:
            y = self.contentsY() + (pos.y()/500.) * self.height()
        elif pos.y() > self.height():
            y = self.contentsY() + self.height() + \
                ((pos.y() - self.height())/500.) * self.height()
        else:
            y = self.contentsY()
        self.ensureVisible(x, y, 0, 0)
        

class RubberBandMixin:
    def __init__(self):
        self.__acquire = 0
        self.__startpos = self.__endpos = None
    def startRubberBand(self, pos):
        self.clearRubberBand()
        # Position is in widget space -- need it in canvas space
        self.__startpos = self.inverseWorldMatrix().map(pos.x(), pos.y())
        self.__endpos = self.__startpos
        self.__acquire = 1
        self.__rubberband_update()
        
    def contentsMouseMoveEvent(self, event):
        if not self.__acquire:
            return
            
        self.__rubberband_update()
        pos = event.pos()
        self.__endpos = self.inverseWorldMatrix().map(pos.x(), pos.y())
        self.__rubberband_update()
        
    def drawContents(self, painter, cx, cy, cw, ch):
        if self.__startpos is None:
            return
        # These are in canvas space, I want them in widget space
        m = self.worldMatrix()
        x1, y1 = m.map(*self.__startpos)
        x2, y2 = m.map(*self.__endpos)
        
        x = min(x1, x2)
        w = abs(x1-x2)+1
        y = min(y1,y2)
        h = abs(y1-y2)+1
        painter.setPen(QPen.DotLine)
        painter.drawRect(x, y, w, h)

        
    def endRubberBand(self, pos):
        self.__acquire = 0
        self.__endpos = self.inverseWorldMatrix().map(pos.x(), pos.y())

    def clearRubberBand(self):
        if self.__startpos is None:
            return
        assert not self.__acquire, "Cannot clear while rubberbanding"
        self.__rubberband_update()
        self.__startpos = self.__endpos = None

    def __rubberband_update(self):
        m = self.worldMatrix()
        x1, y1 = m.map(*self.__startpos)
        x2, y2 = m.map(*self.__endpos)
        x = min(x1, x2) - self.contentsX()
        w = abs(x1-x2)+1
        y = min(y1,y2) - self.contentsY()
        h = abs(y1-y2)+1
        d = 1
        dd = 2*d + 1
        self.viewport().update(x-d, y-d, dd, h+dd)
        self.viewport().update(x-d, y-d, w+dd, dd)
        self.viewport().update(x+w-d, y-d, d, h+dd)
        self.viewport().update(x-d, y+h-d, w+d, dd)

    def getRubberBand(self):
        if self.__startpos is None:
            return None
        m = self.worldMatrix()
        x1, y1 = m.map(*self.__startpos)
        x2, y2 = m.map(*self.__endpos)
        
        x = min(x1, x2)
        w = abs(x1-x2)+1
        y = min(y1,y2)
        h = abs(y1-y2)+1
        return QRect(x, y, w, h)

class MyCanvasView(QCanvasView, ScrollMixin, RubberBandMixin):
    def __init__(self, *args):
        QCanvasView.__init__(self, *args)
        ScrollMixin.__init__(self)
        RubberBandMixin.__init__(self)
        self._get_rubberband = 0

    def contentsMousePressEvent(self, event):
        if not self._get_rubberband:
            self._get_rubberband += 1
            self.startRubberBand(event.pos())
        ScrollMixin.contentsMousePressEvent(self, event)
        QCanvasView.contentsMousePressEvent(self, event)

    def contentsMouseReleaseEvent(self, event):
        if self._get_rubberband == 1:
            self._get_rubberband -= 1
            self.endRubberBand(event.pos())
            rect = self.getRubberBand()
            print "The rubberband encloses", rect.x(), rect.y(), \
                  rect.width(), rect.height()
            for obj in self.canvas().collisions(rect):
                print "  -->", obj.text()
        ScrollMixin.contentsMouseReleaseEvent(self, event)
        QCanvasView.contentsMousePressEvent(self, event)


    def drawContents(self, painter, *args):
        if len(args) == 0:
            return
        cx, cy, cw, ch = args
        QCanvasView.drawContents(self, painter, cx, cy, cw, ch)
        # Draw this second so it appears on top
        RubberBandMixin.drawContents(self, painter, cx, cy, cw, ch)

def main(argv):
    app = QApplication(argv)
    w = 500
    h = 300
    canvas = QCanvas(w, h)
    items = []
    view = MyCanvasView()
    for xname, x in (("Left", 30),
                     ("Center", 200),
                     ("Right", 400),
                     ):
        for yname, y in (("Upper", 20),
                         ("Center", 150),
                         ("Bottom", 250)):
            item = QCanvasText(yname+xname, canvas)
            item.setX(x)
            item.setY(y)
            items.append(item)
            item.show()
                     
    
    view.setCanvas(canvas)
    view.setGeometry(0, 0, (w * 2) / 3, (h * 2) / 3)
    view.show()
    app.setMainWidget(view)
    app.exec_loop()

if __name__ == "__main__":
    main(sys.argv)


More information about the PyQt mailing list