0

So I currently have a list of video clips being displayed in a QListView and have created a custom Delegate to paint preview thumbnails for them using data from a QStandardItemModel.

Ultimately I want to be able to animate the thumbnails as you mouse over them so they play a preview of the clip (by showing only a couple frames). I want two versions of this. One that just plays, and another that shows frames based upon your mouse position (further towards the left is the beginning of the clip, and as you move towards the right, it scrubs through).

Right now I am trying to figure out how to implement the animation piece. Should I be using the Delegate to draw frames and be updating a custom frame data on my model that the Delegate will use to know what frames to draw with a reimplemented paint function (already have the framework of a paint function there)? Or will this be too resource intensive? And then what is the best way to do that? I looked into editor widgets, but those seem to be not seem to edit model data/update delegates in realtime and instead only upon finishing editing. Also I would like this to initialize on mouseover and that doesn't seem to be an option in the built-in edit triggers.

class AnimatedThumbDelegate(QItemDelegate):

def __init__(self,parent=None):
    super().__init__(parent)
    self.height=200
    self.width=self.height*1.77
    self.frames=10

def paint(self,painter,option,index):
    painter.save()

    image=index.data(FootageItem.FILMSTRIP)
    if not image.isNull():
        painter.drawPixmap(QRect(option.rect),image,QRect(image.width()/self.frames*index.data(FootageItem.FRAME),0,image.width()/self.frames,image.height()))

    painter.restore()

def sizeHint(self,option,index):
    return QSize(self.width,self.height)

This delegate paints a portion of a strip image that I am using for preview frames and does so by referencing framedata. FootageItem is just a QStandardItem class that helps construct the data I want to store for these clips and I am just using indices from it here. It fetches a QPixmap from my model. The filmstrip image I am using looks like this:FilmstripExample

Can I use an editor widget to update values and force a repaint on the delegate object based upon mouseMoveEvents? And then can I make editors appear on mouseovers? Or should I look at reimplementing QListView to update the delegate with mouse events? Or is there another way I haven't discovered?

I made a widget that behaves roughly how I would want the updating frames portion of this to work, but was hoping to port it over to a delegate class instead of a QWidget so I could have it display data from a table and utilize the Model/View programming that QT has to offer.

class ScrollingThumbnail(QWidget):

def __init__(self,parent,image,rect):
    super().__init__(parent)
    self.image=image
    self.paused=False
    self.frame=5
    self.frames=10
    self.bar=False
    self.vis=True
    self.thumbRect=rect
    self.setMouseTracking(True)

def leaveEvent(self):
    self.stop()

def mouseMoveEvent(self,e):
    if(e.pos().y()>self.height*0.6):
        self.bar=True
        self.pause()
        self.frame=int(e.pos().x()/(self.width/self.frames))
        self.repaint()
    elif self.paused:
        self.paused=False
        self.bar=False
        self.play()

def paintEvent(self,e):
    qP=QPainter()
    qP.begin(self)
    if self.vis:
        self.drawWidget(qP)
    qP.end()

def drawWidget(self,qP):
    if not self.image.isNull():
        qP.drawPixmap(self.thumbRect,self.image,QRect(self.image.width()/self.frames*self.frame,0,self.image.width()/self.frames,self.image.height()))
        if self.bar:
            pen=QPen(QColor(255,255,255,50),self.height/60,cap=Qt.RoundCap)
            qP.setPen(pen)
            off=self.height/20
            inc=((self.width-(off*2))/(self.frames-1))
            qP.drawLine(off,self.height-off-20,off+(inc)*(self.frame),self.height-off-20)

def play(self):
    if not self.paused:
        self.frame=(self.frame+1)%self.frames
        self.repaint()
        self.timer=threading.Timer(0.1,self.play)
        self.timer.start()

def pause(self):
    try:
        self.timer.cancel()
    except:
        pass
    self.paused=True

def stop(self):
    try:
        self.timer.cancel()
    except:
        pass
    self.frame=5
    self.repaint()

It uses a threading timer as a cheap hacked way of playing frames in a loop. Probably will look more into better ways to achieve this (possibly using QThread?) Also a gif of it in action as far as desired behavior: i.imgur.com/aKoKs3m.gifv

Cheers, Alex

  • According to what I see, you have an image that, depending on the case, takes a fragment and must be shown in the delegate. What mouse event do you want to use: mousePressEvent, mouseMoveEvent, mouseReleaseEvent? And what should the behavior be, more details please? and if it is using the image it shows it would be great. – eyllanesc Feb 13 '18 at 04:31
  • Here is an example of how I want it to operate (screencap of a widget version I made before I went the delegate route). https://i.imgur.com/aKoKs3m.gifv This is all happening as you move your mouse over the image. Switches "modes" of playing based on if your cursor is over the top section or the bottom scrollbar-esque section. So Probably with mouseMoveEvent. – Alex Rideout Feb 13 '18 at 04:53
  • From what I see normally the sequence of images is shown, and with the mouse you can control the sequence, you could show that code. – eyllanesc Feb 13 '18 at 04:57
  • Updated the original post with that. It is code for a widget which was my original approach, though I am assuming using a delegate is the way to go? Especially in the case of having quite a few entries, and wanting to be able to filter/sort them later. – Alex Rideout Feb 13 '18 at 06:14
  • image is QPixmap or QImage? – eyllanesc Feb 13 '18 at 15:06
  • It's a QPixmap. – Alex Rideout Feb 13 '18 at 16:46
  • What model are you using ?, the answer depends on it. – eyllanesc Feb 13 '18 at 16:50
  • I am using a QStandardItemModel (most of the answers to your questions have been in the original post). Not necessarily specifically tied to this model if there is a better option. – Alex Rideout Feb 13 '18 at 18:48
  • I just realized, I got distracted in another part of your question. One recommendation, do not use threading or QThread, you should use QTimer. Finally, are you using PyQt5 or PyQt4? – eyllanesc Feb 13 '18 at 18:51
  • Thanks! Yeah I will look into QTimer. I am actually using Qt.py but primarily developing as if for PyQt5 (or Pyside2). – Alex Rideout Feb 13 '18 at 19:08

1 Answers1

0

Think I figured out a way to go about it using some signals and connecting some slots. Here are the proof of concept classes.

Created this editor class that will update the frame data via signal based upon the mouse position, and then will destroy the editor when your cursor leaves the widget/item. It doesn't really need a paintevent, but I just put one in there so I could see when editing was active.

class TestEditor(QWidget):

editingFinished = Signal()
updateFrame = Signal()

def __init__(self,parent):
    super().__init__(parent)
    self.setMouseTracking(True)
    self.frame=5

def mouseMoveEvent(self,e):
    currentFrame=int(e.pos().x()/(self.width()/10))
    if self.frame!=currentFrame:
        self.frame=currentFrame
        self.updateFrame.emit()

def leaveEvent(self,e):
    self.frame=5
    self.updateFrame.emit()
    self.editingFinished.emit()

def paintEvent(self,e):
    painter=QPainter()
    painter.begin(self)
    painter.setBrush(QColor(255,0,0,100))
    painter.drawRect(self.rect())
    painter.end()

My Delegate connects the editor signals to some basic functions. One will close the editor and the other will update the Frame Data on my model.

class TestDelegate(QItemDelegate):
def __init__(self,parent):
    super().__init__(parent)
    self.height=200
    self.width=300

def createEditor(self,parent,option,index):
    editor=TestEditor(parent)
    editor.editingFinished.connect(self.commitEditor)
    editor.updateFrame.connect(self.updateFrames)
    return editor

def setModelData(self, editor, model, index):
    model.setData(index,editor.frame,TestData.FRAME)

def paint(self,painter,option,index):
    painter.save()
    painter.setBrush(QColor(0,255,0))
    painter.drawRect(option.rect)
    painter.setPen(QColor(255,255,255))
    painter.setFont(QFont('Ariel',20,QFont.Bold))
    painter.drawText(option.rect,Qt.AlignCenter,str(index.data(TestData.FRAME)))
    painter.restore()

def sizeHint(self,option,index):
    return QSize(self.width,self.height)

def commitEditor(self):
    editor = self.sender()
    self.closeEditor.emit(editor)

def updateFrames(self):
    editor=self.sender()
    self.commitData.emit(editor)

Then all I had to do is enable mouse tracking and connect the "entered" signal to the "edit()" slot on my viewer

    dataView=QListView()
    dataView.setViewMode(1)
    dataView.setMovement(0)
    dataView.setMouseTracking(True)
    dataView.entered.connect(dataView.edit)

Also had a super simple class for constructing test data items. Basically just set an empty Role to 5 for frame data.

class TestData(QStandardItem):
FRAME=15
def __init__(self,data=None):
    super().__init__(data)
    self.setData(5,self.FRAME)

It currently isn't fully functional as far as including a "play" function that will scrub through frames automatically. But I think that should be easy enough to set up. Also need to figure out how to now handle selections because the editor becomes active when you move over an item effectively blocking it from being selected. Currently looking into maybe implementing a Mouse up event that will then update the selection model attached to my viewer.