how to dynamically create 2 to 4 QTimers based on attributes in a json file
-
Hi all
I'm creating a pyQt5 window to show several boards one after another for displaying shapes for users to click one target shape on each board.My question is how to dynamically create 2 to 4 QTimers for shapes based on the "entry_time" attributes in a loaded json file. I also want to start QTimers simultaneously and link them with timeout events.
Each shape has its own entryTimer AND exitTimer. For example I have 2 boards. The entryTimer is started at the same time for all shapes on both board 1 AND 2. The entrytime value for shapes on board 2 has a value higher than board 1 entrytime + duaration. (duration indicates the time (in seconds) that the shapes appear.)
So if there are 16 shapes, and 2 boards, there will be a total of 32 entrytimes. The first 16 entrytimes are defined by the entrytime for board_1 and the second 16 entrytimes are defined by the entrytime for board_2 . The second 16 entrytimes values will be higher than the first 16 entrytimes + duration so these shapes only appear after the first 16 shapes are cleared by the exitTimer.The exitTimer is started at the same time for all shapes on the SAME board. When it times out, the clearboard() method removes all shapes to make room for shapes on the next board. Additionally, if a user correctly clicks the target shape before the duration expires, the clearboard() method is also called. It updates the next board entrytime to 0.5 seconds for the shapes that appear on the following board to prevent waiting for the remainder of the duration to expire.
For example, if the duration is set to 60 seconds and a user correctly clicks the target shape in 3 seconds. The clearboard() resets the entrytime to 0.5 seconds to prevent any delays for the shape appearing on the following board.
Note: clearboard() only reset timers without actually manipulating shapes
The JSON file may contain 2 to 4 boards, each with its own entry_time attribute applicable to all shapes on the board. Any help is appreciated!
class MyApp(QMainWindow): def __init__(self): # some Initialization for the main window self.ui.NextButton.clicked.connect(self.showPage1) def showPage1(self): file = QFile("./practice_scripts/OUCH_0_1_1_2.json") file.open(QFile.ReadOnly | QFile.Text) if file.isOpen(): data = file.readAll() qjson = self.jsonParse(data) self.ui.startBtn.clicked.connect(lambda: self.showTrial1(qjson)) def showTrial1(self, qjson): #Trial1 has two boards boards = qjson["boards"].toArray() timerList = [] for b in boards: board = b.toObject() timerList.append(board["entry_time"].toDouble()) for t in timerList: #currently I store entry_time into a list and create a QTimer for each item in the list, but I don't know how to start them at the same time and if I need to use threads boardEntrytimer = QTimer(self) print("BoardEntrytimer Started at " + getTimestamp()) boardEntrytimer.singleShot(int(t), self.updateBoard(board)) def updateBoard(self, board): print("updated! " + getTimestamp()) bd = Board(board) class Board(QWidget): def __init__(self, board, parent=None): #some initialization of the board self.entry_time = board["entry_time"].toInt() self.Entrytimer = QTimer(self) self.Entrytimer.singleShot(self.entry_time * 1000, self.drawShapes) self.Exittimer = QTimer(self) print("prepare to clear board ~ 20s " + getTimestamp()) self.Exittimer.singleShot(self.duration * 1000, self.clearBoard) def drawShapes(self): # code to draw shapes on a board def clearBoard(self): # code to clear shapes on a board
-
Hi,
To start your list of timers, the simple solution is to iterate through it and start them all. That should be fast enough for your needs.
Note that in your code, you seem to create new boards without making anything with them.
What you should rather do is build all your boards, and then when time comes, iterate through your boards and start the appropriate timers from there. Here you seem to duplicate the timers and then you won't start the right one.
-
Be careful creating signal/slot connections in a Loop. At least in PyQt5 I always ran into the bug where all were connected to the last slot or something. Solved by more explicit lambda function capture parameters or something.
There's a 95% chance you'll also hit this bug.
-
Thank you both @SGaist and @enjoysmath !
I restructured code as SGaist suggested, created a list of boards and iterated over the board list.
Like enjoysmath said, I also encountered the issue of only the last board was updated in the singleShot() method. Using a lambda function and grab argument fixed the issue.Seems like for entry timers I have a working solution now at least on my machine. Below is my code if anyone would like to take a look: Note that it is not a Minimal, Reproducible Example because my app includes some other functions and event handlers.
def showTrial1(self, qjson): self.ui.stackedWidget.setCurrentWidget(self.ui.trial1) rulesets = qjson["rulesets"].toObject() boardArr = qjson["boards"].toArray() for b in boardArr: board = b.toObject() bd = Board(rulesets, board, self.geometry()) self.boards.append(bd) self.ui.stackedWidget.addWidget(bd) for bd in self.boards: boardEntrytimer = QTimer(self) boardEntrytimer.singleShot(int(bd.entry_time)*1000, lambda arg = bd: self.createBoard(arg)) def createBoard(self,board): # print("updated! " + getTimestamp()) self.ui.stackedWidget.setCurrentWidget(board)
-
-
@enjoysmath said in how to dynamically create 2 to 4 QTimers based on attributes in a json file:
Be careful creating signal/slot connections in a Loop. At least in PyQt5 I always ran into the bug where all were connected to the last slot or something. Solved by more explicit lambda function capture parameters or something.
That's unrelated to PyQt, and not a bug. Captures bind to variables, not the value they hold at the time of capture.
i = 0 function = lambda: print(i) i = 1 function()
Output:
1
-
@htjane said in how to dynamically create 2 to 4 QTimers based on attributes in a json file:
Thank you both @SGaist and @enjoysmath !
I restructured code as SGaist suggested, created a list of boards and iterated over the board list.
Like enjoysmath said, I also encountered the issue of only the last board was updated in the singleShot() method. Using a lambda function and grab argument fixed the issue.Seems like for entry timers I have a working solution now at least on my machine. Below is my code if anyone would like to take a look: Note that it is not a Minimal, Reproducible Example because my app includes some other functions and event handlers.
for bd in self.boards: boardEntrytimer = QTimer(self) boardEntrytimer.singleShot(int(bd.entry_time)*1000, lambda arg = > ```
This code creates an extra QTimer on each loop iteration. QTimer.singleShot() is a static function.
-
@jeremy_k said in how to dynamically create 2 to 4 QTimers based on attributes in a json file:
That's unrelated to PyQt, and not a bug.
@enjoysmath will be thinking of is the way Python (nothing to do with PyQt, and agreed not a "bug") works for the following pattern:
for i in range(10): list_of_buttons[i].clicked.connect(lambda: print(i))
I and others started out writing this (posts in this forum and stackoverflow), expecting it have the lambda-slot tell us which button was clicked. Like:
for (int i = 0; i < 10; i++) connect(this, list_of_buttons[i], [i]() { qDebug() << i; });
which is what people expect it to do.
I guess Python passes something like
&i
would do in C++. The Python user needs:list_of_buttons[i].clicked.connect(lambda ii=i: print(ii))
which people do not realise at first, and I imagine @enjoysmath has this in mind.
-
Hi all,
Sorry I'm new to Qt. Here again asking a follow-up question about reading board instance attributes that have just been modified by keypress event. Not sure if I should post it here or open a new thread. Please Lmk.My question is in the KeyPressEvent of Board class, I set the self.finish attribute to True to indicate the target shape has been clicked and a key pressed, then in MyApp class I want to get the finish attribute of board instance using an if-condition to check if the board has been finished. But the code in MyApp executes before "finish" has been modified by KeyPressEvent, without hanging or waiting for it and "finish" is False whenever I print it in MyApp class. Specifically, I want to read "finish" of each board in MyApp and if it's True, set the entry_time of the next board to 0.5s to prevent waiting this board's duration to expire. Could you guys suggest some solutions?
class MyApp(QMainWindow): # constructor and some functions... def showTrial1(self, qjson): self.ui.stackedWidget.setCurrentWidget(self.ui.trial1) rulesets = qjson["rulesets"].toObject() boardArr = qjson["boards"].toArray() for b in boardArr: # create boards board = b.toObject() bd = Board(rulesets, board, self.geometry()) self.boards.append(bd) self.ui.stackedWidget.addWidget(bd) for bd in self.boards: #call a static timer to draw board based on entry_time QTimer.singleShot(int(bd.entry_time) * 1000, lambda arg=bd: self.createBoard(arg)) def createBoard(self, board): board.drawShapes() self.ui.stackedWidget.setCurrentWidget(board) class Board(QWidget): def __init__(self, rulesets, board, size, parent=None): super().__init__(parent) self.rules = rulesets self.lastLeftPoint = QPoint(0, 0) self.id = board["id"].toInt() self.duration = 10 # board["duration"] self.entry_time = board["entry_time"].toDouble() self.stimuli = board["stimuli"] self.shapes = [] self.click = False self.boardSize = size self.timeout = False self.finish = False def drawShapes(self): stiArr = self.stimuli.toArray() for s in stiArr: stimuli = s.toObject() ID = stimuli["id"].toInt() # print(ID) color = QtGui.QColor(stimuli["color"].toObject()["color"].toString()) text = stimuli["text"].toString() x = math.floor( stimuli[ "column"].toInt() * self.boardSize.width() / 12.0 - self.boardSize.width() / 24.0) # 3862 * 2122 y = math.floor(stimuli["row"].toInt() * self.boardSize.height() / 12.0 - self.boardSize.height() / 24.0) pos = QtCore.QPoint(x, y) target = stimuli["target"] response = stimuli["response"] # hue = clr["hue"].toInt() # saturation = clr["saturation"].toInt() * 255 # value = clr["value"].toInt() * 255 # alpha = clr["alpha"].toInt() * 255 if stimuli["shape"] == "circle": self.shapes.append(Circle(ID, 30, pos, color, text, target, response)) elif stimuli["shape"] == "cross": self.shapes.append(Cross(ID, 30, pos, color, text, target, response)) elif stimuli["shape"] == "star": self.shapes.append(Star(ID, 30, pos, color, text, target, response)) elif stimuli["shape"] == "hexagon": self.shapes.append(Hexagon(ID, 30, pos, color, text, target, response)) elif stimuli["shape"] == "octagon": self.shapes.append(Octagon(ID, 30, pos, color, text, target, response)) elif stimuli["shape"] == "pentagon": self.shapes.append(Pentagon(ID, 30, pos, color, text, target, response)) # self.update() def paintEvent(self, event): painter = QPainter(self) painter.setRenderHint(QPainter.Antialiasing) if self.timeout: print("cleared! " + getTimestamp()) painter.eraseRect(0, 0, self.boardSize.width(), self.boardSize.height()) return for shape in self.shapes: shape.paint(painter) if self.click: painter.setPen(QPen(QColor("green"), 4, Qt.SolidLine)) painter.drawRect(self.lastLeftPoint.x() - 15, self.lastLeftPoint.y() - 15, 30, 30) painter.end() def mousePressEvent(self, event): # on left mouse click, it returns the coordinates if event.button() == Qt.LeftButton: self.lastLeftPoint = event.pos() for shape in self.shapes: # iterate through shapes if shape.position.x() - 15 <= self.lastLeftPoint.x() <= shape.position.x() + 15 and shape.position.y() - 15 <= self.lastLeftPoint.y() <= shape.position.y() + 15: if shape.target.toBool(): # if is target print("clicked the target!") self.click = True # set click to true else: print("not this one") self.update() def keyPressEvent(self, e): rules = self.rules["responserules"].toArray() # get response rules of the board if self.click: if "any" == rules[0].toString() and type(e.key()) is int: # if rule is any and pressed any key self.finish = True print("updated") # verified that this is hit by printing statement else: myList = [Qt.Key_Q, Qt.Key_W, Qt.Key_E, Qt.Key_R, Qt.Key_T] if e.text() in myList: print("In My List") # if clicked a key in the set