Should I use Qt Signal or Event? - python

I'm using PySide2 and not clearly about Signal and Event
If we have two people are doing two View.
Person A is doing ListView
Person B is doing ParameterView
while ListItem be selected, update ParameterView
How should I Connect them? use Signal or Event?
Maybe I would have another View, it needs to be update also, while ListItem selectChanged
Signal
class ListView(QListView):
# do something
class ParameterView(QWidget):
def update(self):
# do something
list_view = ListView()
parameter_view = ParameterView()
list_view.selectChanged.connect(parameter_view.update)
Event
class ListView(QListView):
def selectChanged(self):
QApplication.sendEvent(self, SelectChangedEvent)
class SelectChangedEvent(QEvent):
# initialize ...
class ParameterView(QWidget):
def update(self):
# do something
def event(self, event):
if event.type() == SelectChangedEvent:
self.update()

Here you can read a comparison between signals and events and decide what is right for you
https://www.learnpyqt.com/courses/start/signals-slots-events/
I believe you better use single and slots to solve this problem but this is your choice
Hope this helped you decide, have a nice day.

Both options are valid since they use the same mechanism to transmit the information but the great difference is that if you want to send a QEvent then you must access the object in that space and instead with the Qt signals you should not know the object in that space but only in the connection.
My recommendation is that signals should be used to decouple classes. I recommend you read:
https://doc.qt.io/qt-5/signalsandslots.html
Qt events and signal/slots

Related

PyQt live editing/drawing of Delegate

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:
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
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.

PyQt - How can I re-initialise the whole code without restarting it?

I wrote a python GUI program consisting of two separate files; One is for logic code and the other for GUI using PyQt4. The behaviour of some objects (buttons, text fields ...) changes throughout the code and I need to reset everything to its original status by clicking on a QAction class menu item. How can I do that?
EDIT: the function that supposed to reset the GUI to the original status:
def newSession(self):
self.ui.setupUi(self)
self.filename = ""
self.paramsSplitted = []
self.timestep = None
self.index = None
self.selectedParam = None
self.selectedMethod = None
--Snip--
What you could do:
Define a ResetHandler(QtCore.QObject) object with a reset_everything signal
During startup create an instance and set it on the globally available QApplication like qapplication.reset_handler = ResetHandler()
Every UI element that needs to update itself defines a on_reset_everything_triggered() slot. (Optional: You could also just use update for example).
When You create UI elements that are supposed to update, connect them to the globally available reset_everything signal from the handler on the QApplication.
Connect your QAction.triggered with the ResetHandler.reset_everything signal.
Now every time you press the QAction the reset_everything signal is invoked, and all UI elements that you connected will update themself.
Like you requested in your comment here is a schematic way of utilizing a function to connect all signals and the method setupUi.
class MainWindow(QtGui.QMainWindow) :
def __init__(self) :
QtGui.QMainWindow.__init__(self)
self.ui.setupUi(self)
# Some code
self.connectAllSignals()
def connectAllSignals(self) :
self.someWidget.clicked.connect(self.someFunction)
self.someAction.triggered.connect(self.otherFunction)
# All the other signals
def disconnectAllSignals(self) :
try :
self.someWidget.clicked.disconnect()
self.someAction.triggered.disconnect()
# All the other signals
except :
print("Something went wrong. Check your code.")
pass
def newSession(self) :
self.ui.setupUi(self)
self.disconnectAllSignals()
self.connectAllSignals()
# Do whatever it takes
By this you ensure you have only the initial settings for your signals and all dynamically added connections are broken. In the method disconnectAllSignals be sure all widgets exist and all signals have at least one connection by the time you call it. If you have new widgets invoked dynamically you should consider deleting them in th method newSession after calling connectAllSignals.

How to get access to handler id from Gtk.Builder.connect_signals

I have a Glade file with some buttons and I use Gtk.Builder.connect_signals() to connect methods (on_button_toggled) with the corresponding signals (toggled).
(It is acutally quickly which does that for me, but I can see and change that code, so that is only a detail).
What I want to do now, is stop a signal from being processed, e.g. though a call to object.handler_block(handler_id) or object.disconnect(handler_id). So my question is: how can I get the handler_ids for connections created via Gtk.Builder.connect_signals()?
Normally you would get the handler_id from a call to one of:
handler_id = object.connect(name, cb, cb_args)
handler_id = object.connect_after(name, cb, cb_args)
handler_id = object.connect_object(name, cb, slot_object, cb_args)
handler_id = object.connect_object_after(name, cb, slot_object, cb_args)
but the Gtk.Builder version does not return the ids.
Sadly, I don't believe that there's any way to get at the signal handler IDs that were connected by Gtk.Builder. If you really want the handlers, you have to manually connect your signals, storing any handler_ids you care about.
An alternative approach is to decide that you don't actually need the handlers themselves, but can block/unblock/etc. based on the connected callable, using GObject.handler_block_by_func and similar.
The final option is to try to actually find the handler after the fact, using as many details as you can. In C, use g_signal_handler_find; this isn't bound for pygtk2, but presumably will work using pygobject3. The downside here is that there's no guarantee that you'll find what you actually connected.
This is probably too late for you but I have a solution to get the name of the widget you clicked that is set up with...
self.builder.connect_signals(self)
From this source, the author uses gtk.Buildable.get_name() to get the name of the widget/object. In my scenario, depending on what button was clicked a different action occurred. I wanted to attach the same event handler to all of the buttons of a certain type, because they largely do the same thing. Here's the code.
import gtk
class GUI:
def __init__(self):
self.builder = gtk.Builder()
self.builder.add_from_file('myGUI.glade')
self.builder.connect_signals(self)
self.window = self.builder.get_object("window1")
self.window.show()
def on_button_click(self, object, data=None):
print(gtk.Buildable.get_name(object))
Pretty cool, eh? I think so, at least.
With PyGObject, the best way is the signal_handler_find approach I think. Use this function, which uses two functions from GObject
def get_handler_id(obj, signal_name):
signal_id, detail = GObject.signal_parse_name(signal_name, obj, True)
return GObject.signal_handler_find(obj, GObject.SignalMatchType.ID, signal_id, detail, None, None, None)
Usage example:
get_handler_id(button, "clicked")
will get you the handler_id for a handler connected to the clicked signal of button. This can then be used for handler_block.

Tooltips or status bar not working in a PyQt app from Maya

I've got a PyQt4 QDialog that I'm launching from python in Autodesk Maya. I want to have a status bar in the window or, if need be, tooltips. Maya doesn't seem to approve of either. I've implemented it using the method described here:
http://www.qtcentre.org/threads/10593-QDialog-StatusBar
If I launch my app standalone, both work correctly. Running from Maya, though, the status updates get sent to the general Maya status bar (which is not very obvious if you're in a different window), and Maya seems to steal the events completely from me: if I monitor the events that my event() method is getting, it never gets a QEvent.StatusTip event. I've tried swapping my QDialog for a QMainWindow, but it doesn't seem to change anything.
Any suggestions for avenues to look down to get this working?
At the moment, I'm working around this in a horrible way: subclassing each of widgets I want to use, and adding a signal to send to the parent, self.setMouseTracking(True), and a mouseMoveEvent(self, e) that sends the signal to the parent. Then at the top of the tree I set the status bar. It's the sort of nasty code that makes me feel dirty, subclassing all the widget types, but it does seem to be working.
Any better suggestions VERY gratefully received!
I need to tackle this as well, so your post was quite helpful.
When I have encountered event issues like this before, I solved it by using installEventFilter on all widgets (the same filter), rather than subclassing. Then you can receive and accept the events to block them from Maya (or let them through, e.g. space bar for marking menus over your gui, etc)
Here is what I use to let Maya have the spacebar (marking menus), ctrl+A (attribute editor toggle) and ctrl+Z (undo). This would be added to your event filter:
if event.type() == QEvent.KeyPress:
key = event.key()
mod = event.modifiers()
if ((ctrla and key == Qt.Key_A and mod == Qt.ControlModifier) or # CTRL+A
(ctrlz and key == Qt.Key_Z and mod == Qt.ControlModifier) or # CTRL+Z
(space and key == Qt.Key_Space)): # Space Bar
event.ignore()
return True
return False
You would just need to do the opposite and use event.accept() and return False
For QWidgets, colts answer here is pretty good.
This is how I got it working for QActions:
class ActionFn(object):
def __init__(self, action):
self.action = action
def __call__(self):
self.action.parent()._displayStatusTip(self.action.statusTip())
Then after the action is created:
newAction._statusFn = _StatusTipActionFn(newAction)
newAction.hovered.connect(newAction._statusFn)
I hope this helps.

Switching scenes with pyglet

Can anybody recommend how to switch between scenes in pyglet.
I.e.
menu > game
game > menu
menu > help
ect
The only way that i can think to do it off the top of my head is by using different windows, which i am quite sure would be the complete wrong way to do it. Or by overloading all of the window's event functions.
Sorry if i haven't made myself clear but any help would be appreciated
The cocos2d.org framework is built on pyglet and includes scene management.
Here is a rough schematic of a class structure that might work for you:
class Game(object):
"This class contains the Scene which is the current scene the user is look ing at."
def __init__(self):
self.current_level = 0
self.current_screen = MainMenu(self)
def load(self):
"Load progress from disk"
pass
def save(self):
"Save progress to disk"
pass
def clearCurrentScreen(self):
self.current_screen.clear()
self.window.remove_handlers()
def startCurrentScreen(self):
self.window.set_handler("on_key_press", self.current_screen.on_key_press)
# etc
self.current_screen.start()
def gotoNextLevel(self):
"called from within LevelPlayer when the player beats the level"
self.clearCurrentScreen()
self.current_level += 1
self.current_screen = LevelPlayer(self, game, self.current_level)
self.startCurrentScreen()
def startPlaying(self):
"called by the main menu when the user selects an option"
self.clearCurrentScreen()
self.current_screen = LevelPlayer(self, game, self.current_level)
self.startCurrentScreen()
def execute(self):
self.window = pyglet.window.Window()
self.startCurrentScene()
pyglet.app.run()
class Screen(object):
def __init__(self):
pass
def start():
pass
def clear():
"delete all graphical objects on screen, batches, groups, etc. Clear all state in pyglet."
pass
def on_key_press(self, key, etc):
pass
def on_draw(self):
pass
# etc
class LevelPlayer(Screen):
"This class contains all your game logic. This is the class that enables the user to play through a level."
def __init__(self, game, level_to_play):
pass
# be sure to implement methods from Screen
class MainMenu(Screen):
"This class presents the title screen and options for new game or continue."
def __init__(self, game):
self.game = game
def handleNewGame(self):
self.game.startPlaying()
def handleContinue(self):
self.game.load()
self.game.startPlaying()
# be sure to implement methods from Screen
game = Game()
game.execute()
So you have a Game class who owns the window and that manages which Screen is displayed to the user. Here I use "Screen" to mean what the user is interacting with, for example a MainMenu, or a LevelPlayer. The key here is the clear() method of Screen, which you should implement to delete all the sprites, media, groups, and batches that you were displaying. You also have to remove the window handlers on clear and set them on start.
You can see this solution in action here: https://github.com/superjoe30/lemming/tree/master/lemming
I'm not very experienced, but for what it's worth, the method I use is as follows. It does not recognise explicit 'states' or 'scenes' as such, but rather it relies on the adding (and removal) of discrete items into my game world. Each such item may have its own key handlers, and knows how and when to create other such items.
A specific example:
GameItem is a subclass for all items that can be put into the world. It is a simple collection of attributes, with no behaviour. It is subclassed by items in the game world like Bush, Player, etc, and also by HUD items like ScoreDisplay and MainMenu.
World is just a collection of GameItems.
My 'update' function iterates through all items in the world, calling their update method if they have one.
My 'draw' function similarly iterates through all items in the world, drawing each of them. (many of them might be drawn en-masse by simply calling something like pyglet's Batch.draw)
Right at application start-up, one of the first items I add to the world is a MainMenu object. It has an on_key_down method.
World.add looks at the item being added. If an item has an on_key_down method, then it adds this handler to the pyglet key handlers stack. (Similarly, it undoes this in World.remove) (Actually, on reflection, I guess world.add raises an event when it adds an item. If the application's keyboard handler module is interested, then on recieving one of these events it then does the adding of the item's key hander, but this is kinda incidental to the question)
The MainMenu.on_key_handler method looks at the pressed key, and if it's the key to start the game, then it makes a series of calls:
# Do everything needed to start the game
# For dramatic pacing, any of these might be scheduled to be
# called in a second or so, using pyglet.clock.schedule_once
world.add(player)
camera.to_follow(player)
world.add(scoredisplay)
# Finally, remove the main menu from the world
# This will stop showing it on screen
# and it will remove its keyboard event handler
world.remove_item(self)
This is just a simple example, but hopefully you can see how, instead of starting the game, the mainmenu could instead display secondary menus by adding them to the world, or any such thing.
Once in the game, you can change 'scenes' by simply calling 'world.remove' on all items that you no longer want visible, and calling 'world.add' on all the items in the new scene.
An example of a game that uses this technique is previous pyweek entry SinisterDucks:
http://code.google.com/p/brokenspell/
(although, to be fair, it does not feature distinct 'scenes' during gameplay. It merely usesthis technique to manage menus, game over screen, etc)
The solution i ended up using is by giving the main window a stack and having it pass all events out to items on the top of the stack.
i.e.
class GameWindow(pyglet.window.Window):
def __init__(self,*args, **kwargs):
pyglet.window.Window.__init__(self, *args, **kwargs)
self.states = [PlayLevel(self)] ## Starting State
def on_draw(self):
if hasattr(self.states[-1],"on_draw"):
self.states[-1].on_draw()
def on_mouse_press(self,*args):
if hasattr(self.states[-1],"on_mouse_press"):
self.states[-1].on_mouse_press(*args)
ect. For all of the other events that i use
i am currently in the process of writing up some functions to go in GameWindow that will manage pushing and popping scenes from the stack

Categories

Resources