I have an application that works kind of like a slide show: I have a button that changes the background picture when clicked. I also have a second button that helps go back to the previous picture. The problem is, that the first button gets another function at the end of the show, but after that I am not able to change the function back to the previous one when I click the back button.
My code looks somewhat like this, I hope this makes my problem clear:
class SecondWindow(TemplateBaseClass):
def back(self):
self.first.n = self.first.n-2
self.hide()
self.first.show()
self.first.nextPicture()
def __init__(self):
TemplateBaseClass.__init__(self)
self.ui = WindowTemplate()
self.ui.setupUi(self)
self.first = MainWindow(self)
self.first.showFullScreen()
self.ui.pushButton.clicked.connect(lambda x:self.back())
class MainWindow(QtGui.QMainWindow, Ui_MainWindow):
def showSecond(self):
#QTimer.singleShot(25, MainWindow)
self.second.showFullScreen()
self.hide()
def back(self):
if self.n >= 2:
self.n = self.n-2
self.notBack = False
self.nextPicture()
# I tried in several places like here, but it does not work
self.ui.end_button.clicked.connect(lambda x:self.nextPicture())
def nextPicture(self):
print(self.n)
if self.n == 0:
self.ui.bg_widget.setStyleSheet("background-image: url(:/ux/img0.png);\nbackground-repeat:no-repeat;")
elif self.n ==1 :
self.ui.bg_widget.setStyleSheet("background-image: url(:/ux/img1.png);\nbackground-repeat:no-repeat;")
elif self.n == 2:
self.ui.bg_widget.setStyleSheet("background-image: url(:/ux/img2.png);\nbackground-repeat:no-repeat;")
if self.notBack:
self.ui.end_button.clicked.connect(lambda x:self.showSecond())
else:
self.ui.end_button.clicked.connect(lambda x:self.nextPicture())
self.n +=1
self.notBack = True
def __init__(self, second):
QtGui.QMainWindow.__init__(self)
Ui_MainWindow.__init__(self)
self.second = second
self.setWindowTitle('pyqtgraph example: Qt Designer')
self.ui=uic.loadUi(uiFile, self)
self.setupUi(self)
self.setWindowFlags(QtCore.Qt.CustomizeWindowHint)
self.showFullScreen()
self.n = 1
self.notBack = True
self.ui.end_button.clicked.connect(lambda x:self.nextPicture())
self.ui.backButton.clicked.connect(lambda x:self.back())
A simple disconnect() solves the issue:
def back(self):
if self.n >= 2:
self.n = self.n-2
self.notBack = False
self.nextPicture()
self.ui.end_button.disconnect()
self.ui.end_button.clicked.connect(lambda x:self.nextPicture())
Related
I am looking for a way to implement a time picker in a tkinter application.
I was able to implement this (probably not in the best way) using the spinbox widget and also using #PRMoureu's wonderful answer for validation. What I have right now is this -
import tkinter as tk
class App(tk.Frame):
def __init__(self,parent):
super().__init__(parent)
self.reg=self.register(self.hour_valid)
self.hourstr=tk.StringVar(self,'10')
self.hour = tk.Spinbox(self,from_=0,to=23,wrap=True,validate='focusout',validatecommand=(self.reg,'%P'),invalidcommand=self.hour_invalid,textvariable=self.hourstr,width=2)
self.reg2=self.register(self.min_valid)
self.minstr=tk.StringVar(self,'30')
self.min = tk.Spinbox(self,from_=0,to=59,wrap=True,validate='focusout',validatecommand=(self.reg2,'%P'),invalidcommand=self.min_invalid,textvariable=self.minstr,width=2)
self.hour.grid()
self.min.grid(row=0,column=1)
def hour_invalid(self):
self.hourstr.set('10')
def hour_valid(self,input):
if (input.isdigit() and int(input) in range(24) and len(input) in range(1,3)):
valid = True
else:
valid = False
if not valid:
self.hour.after_idle(lambda: self.hour.config(validate='focusout'))
return valid
def min_invalid(self):
self.minstr.set('30')
def min_valid(self,input):
if (input.isdigit() and int(input) in range(60) and len(input) in range(1,3)):
valid = True
else:
valid = False
if not valid:
self.min.after_idle(lambda: self.min.config(validate='focusout'))
return valid
root = tk.Tk()
App(root).pack()
root.mainloop()
This seems like a pretty common requirement in GUI applications so I think there must be a more standard way to achieve this. How can I implement a user picked time widget in a cleaner way?
I am asking this because the tiny feature I want implemented is when incrementing/decrementing the minute-spinbox, if it loops over, the hour-spinbox should accordingly increase/decrease.
I thought of achieving this by setting a callback function, but I would not come to know which button of the spinbox exactly was triggered (up or down).
You can trace the changes on your minutes and act accordingly. Below sample shows how to automatically increase hour when minutes increases pass 59; you can adapt and figure out how to do the decrease part.
import tkinter as tk
class App(tk.Frame):
def __init__(self, parent):
super().__init__(parent)
self.hourstr=tk.StringVar(self,'10')
self.hour = tk.Spinbox(self,from_=0,to=23,wrap=True,textvariable=self.hourstr,width=2,state="readonly")
self.minstr=tk.StringVar(self,'30')
self.minstr.trace("w",self.trace_var)
self.last_value = ""
self.min = tk.Spinbox(self,from_=0,to=59,wrap=True,textvariable=self.minstr,width=2,state="readonly")
self.hour.grid()
self.min.grid(row=0,column=1)
def trace_var(self,*args):
if self.last_value == "59" and self.minstr.get() == "0":
self.hourstr.set(int(self.hourstr.get())+1 if self.hourstr.get() !="23" else 0)
self.last_value = self.minstr.get()
root = tk.Tk()
App(root).pack()
root.mainloop()
Thanks Henry for your code, it is excellant. Here is my extension for Seconds:
# You can trace the changes on your minutes and act accordingly. Below sample shows how to automatically increase hour when minutes increases pass 59 and similarly Seconds increase pass 59; you can adapt and figure out how to do the decrease part.
import tkinter as tk
class App(tk.Frame):
def __init__(self, parent):
super().__init__(parent)
self.hourstr=tk.StringVar(self,'10')
self.hour = tk.Spinbox(self,from_=0,to=23,wrap=True,textvariable=self.hourstr,width=2,state="readonly")
self.minstr=tk.StringVar(self,'30')
self.min = tk.Spinbox(self,from_=0,to=59,wrap=True,textvariable=self.minstr,width=2) # ,state="readonly"
self.secstr=tk.StringVar(self,'00')
self.sec = tk.Spinbox(self,from_=0,to=59,wrap=True,textvariable=self.secstr,width=2)
self.last_valueSec = ""
self.last_value = ""
self.minstr.trace("w",self.trace_var)
self.secstr.trace("w",self.trace_varsec)
self.hour.grid()
self.min.grid(row=0,column=1)
self.sec.grid(row=0,column=2)
def trace_var(self,*args):
if self.last_value == "59" and self.minstr.get() == "0":
self.hourstr.set(int(self.hourstr.get())+1 if self.hourstr.get() !="23" else 0)
self.last_value = self.minstr.get()
def trace_varsec(self,*args):
if self.last_valueSec == "59" and self.secstr.get() == "0":
self.minstr.set(int(self.minstr.get())+1 if self.minstr.get() !="59" else 0)
if self.last_value == "59":
self.hourstr.set(int(self.hourstr.get())+1 if self.hourstr.get() !="23" else 0)
self.last_valueSec = self.secstr.get()
root = tk.Tk()
App(root).pack()
root.mainloop()
I am designing a program to edit DICOMs. Specifically, I am having issues appropriately interacting with my PyQt UI.
I want to be able to click on a "pause" and on a "stop" button to either pause or stop my editing function. My editing function takes a significant amount of time to process / loop through. Depending on the number of files that it is editing, it can take anywhere from 30 seconds to over an hour. Because of this, I decided to throw my editing function into its own thread using the native threading capabilities of Qt. I was able to get the thread working ie: from my MainWindow class I can click a button that initializes my editing class (class edit(QThread), however interacting with the GUI still crashes the program and I'm not sure why! Below I have added a sample of the general code structure / set up that I am using.
class anonymizeThread(QThread):
def __init__(self):
QThread.__init__(self)
def __del__(self):
self.wait()
#def sendAnon(self, progress_val):
# self.completed = 0
# return self.completed
def run(self):
# while self.completed < 100:
# self.completed += 0.00001
# self.emit(QtCore.SIGNAL('PROGRESS'), self.completed)
# ANONYMIZE FUNCTION!
i = 0
#flag = self.stop_flag
while i < 10000000: # and self.stop_flag is not 1:
print(i)
i+=1
print('i didnt enter the loop')
class MainWindow(QMainWindow, Ui_MainWindow):
def __init__(self):
super(MainWindow, self).__init__()
self.setupUi(self)
# connect the buttons
self.worker = anonymizeThread()
self.anonbtn.clicked.connect(self.anonymize)
self.open_directory.clicked.connect(self.open_dir)
self.pause.clicked.connect(self.paused)
self.stopbtn.clicked.connect(self.stopped)
# block button signals to start
self.pause.blockSignals(True)
self.stopbtn.blockSignals(True)
self.dir_name = None
self.pause_flag = None
self.stop_flag = None
self.anon_flag = None
# This is how we quit from the main menu "File" option
extractAction = self.actionQuit_Ctrl_Q
extractAction.setShortcut("Ctrl+Q")
extractAction.setStatusTip('Leave The App')
extractAction.triggered.connect(self.close_application)
def updateProgressBar(self,val):
self.progressBar.setValue(val)
def close_application(self):
choice = QMessageBox.question(self, 'Just had to check...', "Are you sure you want to exit?", QMessageBox.Yes | QMessageBox.No)
if choice == QMessageBox.Yes:
sys.exit()
else:
pass
def anonymize(self):
self.pause.blockSignals(False)
self.stopbtn.blockSignals(False)
self.worker.start()
# check if directory chosen
# self.progressBar.setMaximum(len(dcm)
# start our anon thread!
def paused(self):
#only if running
if self.pause_flag is 0:
self.pause_flag = 1
self.pause.setText('Start')
elif self.pause_flag is 1:
self.pause_flag = 0
self.pause.setText('Pause')
else:
pass
def stopped(self): # need a self.stop() for anonThread
choice = QMessageBox.question(self,'Stop', "Are you sure you want to stop? You will not be able to pick up exactly where you left off.",
QMessageBox.Yes | QMessageBox.No)
if choice == QMessageBox.Yes:
self.stop_flag = 1
#self.stopbtn.blockSignals(True)
#self.paused.blockSignals(True)
else:
pass
def open_dir(self):
self.dir_name = str(QFileDialog.getExistingDirectory(self, "Select Directory"))
if len(self.dir_name) is not 0:
self.anon_flag = 0
def main():
app = QApplication(sys.argv)
main_window = MainWindow()
main_window.show()
sys.exit(app.exec_())
if __name__ == "__main__":
main()
It is advisable not to access the flags directly, it is better to do it through the functions to make use of it transparently, for this the same class should verify the tasks.
Also it is good to give a small delay so that the application can deal with the graphic part, another possible improvement is to avoid usat sys.exit, you could call the close method that closes the window.
In the following code I have implemented the stop and pause methods.
class anonymizeThread(QThread):
def __init__(self):
QThread.__init__(self)
self.onRunning = True
self.onStop = False
def __del__(self):
self.wait()
def stop(self):
self.onStop = True
def pause(self):
if self.isRunning():
self.onRunning = not self.onRunning
def run(self):
i = 0
#flag = self.stop_flag
while i < 10000000:
if self.onRunning: # and self.stop_flag is not 1:
print(i)
i+=1
if self.onStop:
break
QThread.msleep(10)
print('i didnt enter the loop')
class MainWindow(QMainWindow, Ui_MainWindow):
def __init__(self):
super(MainWindow, self).__init__()
self.setupUi(self)
# connect the buttons
self.worker = anonymizeThread()
self.anonbtn.clicked.connect(self.anonymize)
self.pause.clicked.connect(self.paused)
self.stopbtn.clicked.connect(self.stopped)
# block button signals to start
self.pause.blockSignals(True)
self.stopbtn.blockSignals(True)
def close_application(self):
choice = QMessageBox.question(self, 'Just had to check...', "Are you sure you want to exit?", QMessageBox.Yes | QMessageBox.No)
if choice == QMessageBox.Yes:
self.close()
def anonymize(self):
self.pause.blockSignals(False)
self.stopbtn.blockSignals(False)
self.worker.start()
def paused(self):
self.worker.pause()
def stopped(self): # need a self.stop() for anonThread
choice = QMessageBox.question(self,'Stop', "Are you sure you want to stop? You will not be able to pick up exactly where you left off.",
QMessageBox.Yes | QMessageBox.No)
if choice == QMessageBox.Yes:
self.worker.stop()
Thanks to #eyllansec and #ekhumoro..
In the above code, all instances of self.stop_flag = ... should have been self.worker.stop_flag = ... as it is changing the variable that is to be used in the worker class/thread. My mistake was assuming both classes inherited the same "self".
If there are other errors and or better explanations of what I did incorrectly, please do post an answer and I'll accept it!
I'm writing an application that allows the user to add items to a scene. I dont want any new items being drawn over items that have already been drawn and to do that I decided to use the collidesWithItem() function to detect collision. With my code i still can draw over added items even though there is obviously collision and I debugged the program and the collidesWithItem() function keeps returning "False".
The items are added by clicking on the toolbar of the Form.
Down below is my code:
class graphicsScene(QtGui.QGraphicsScene, QtGui.QWidget):
def __init__(self, parent=None):
super(graphicsScene, self).__init__(parent)
self.i = 0
self.setSceneRect(-180, -90, 360, 180)
global overlapped
overlapped = 0
def mousePressEvent(self, event):
global host_cs
global overlapped
if host_cs == 1:
if len(hostItem_list) == 0:
self.host_item = host_Object()
hostItem_list.append(self.host_item.host_pixItem)
else:
self.host_item = host_Object()
for host in hostItem_list:
if self.host_item.host_pixItem.collidesWithItem(host):
print 'collision'
overlapped = 1
break
elif self.host_item.host_pixItem.collidesWithItem(host) == False:
overlapped = 0
if overlapped == 0:
hostItem_list.append(self.host_item.host_pixItem)
def mouseReleaseEvent(self, event):
global host_cs
if host_cs == 1:
if overlapped == 0:
self.addItem(self.host_item.host_pixItem)
self.host_item.host_pixItem.setPos(event.scenePos())
self.i += 1
host_list.append('h' + str(self.i))
class host_Object(QtGui.QGraphicsPixmapItem, QtGui.QWidget):
def __init__(self, parent=None):
super(host_Object, self).__init__(parent)
pixmap = QtGui.QPixmap("host.png")
self.host_pixItem = QtGui.QGraphicsPixmapItem(pixmap.scaled(30, 30, QtCore.Qt.KeepAspectRatio))
self.host_pixItem.setFlag(QtGui.QGraphicsPixmapItem.ItemIsSelectable)
self.host_pixItem.setFlag(QtGui.QGraphicsPixmapItem.ItemIsMovable)
class Form(QtGui.QMainWindow, QtGui.QWidget):
def __init__(self):
super(Form, self).__init__()
self.ui = uic.loadUi('form.ui')
self.ui.actionHost.triggered.connect(self.place_host)
self.scene = graphicsScene()
self.ui.view.setScene(self.scene)
def place_host(self):
host_cs = 1
if __name__ == '__main__':
app = QtGui.QApplication(sys.argv)
form = Form()
form.ui.show()
app.exec_()
Multiple inheritance is source of all evil things in application design.
Also it is forbidden in Qt to double inherit QObject.
So all your classes with multiple inheritance has big flaws.
Even class host_Object(QtGui.QGraphicsPixmapItem, QtGui.QWidget) is wrong and problematic since item can't be a QWidget and QGraphicsItem at the same time.
I would recommend you to avoid double inheritance as a general rule not only for Qt framework but for any language.
[edit]
It seems I solved the problem... In fact, I now do that:
class Gameboard(QGraphicsScene):
def deletePawn(self, num):
pawnToDelete = self.pawns.pop(num)
pawnToDelete.delete()
class Pawn(QGraphicsItem):
def delete(self):
child.prepareGemotryChange()
child.setParent(None)
#idem for each child
self.gameboard.removeItem(self)
self.gameboard = None
[/edit]
What is the good way to implement references in a pyqt QGraphicsScene?
I made a class Gameboard which inherits QGraphicsScene, and this gameboard contains a variable number of pawns (wich inherit QGraphicsPolygonItem)
Pawns can be created or deleted dynamically, creation is ok but deletion sometimes crash...
from PyQt4.QtCore import *
from PyQt4.QtGui import *
from PyQt4 import QtOpenGL
class Gameboard(QGraphicsScene):
def __init__(self):
super(Gameboard, self).__init__()
self.pawns = {}
self.currentPawn = None
def createSquares(self):
#create the polygons of the squares and affect their coordinates x,y
def createPawn(self):
pawn = Pawn(self)
num = len(self.pawns)
self.pawns[num] = pawn
self.addItem(self.pawns[num])
def deletePawn(self, num):
self.currentPawn = None
self.pawns[num].beforeDelete()
self.pawns[num].prepareGeometryChange()
self.removeItem(self.pawns[num])
del self.pawns[num]
def selectPawn(self, pawn):
#a pawn is selected, by click for example
self.currentPawn = pawn
def keyPressEvent(self, event):
ekey = event.key()
if ekey == Qt.Key_Delete and self.currentPawn != None:
num = self.currentPawn.number
self.deletePawn(num)
class Pawn(QGraphicsItem):
def __init__(self, gameboard):
super(Pawn, self).__init__()
self.gameboard = gameboard
self.number = 0
self.pos = (-1,-1)
self.name = ""
def create(self, x, y, num):
#create a QGraphicsPolygonItem, a QGraphixPixmapItem and a QGraphicsTextItem which are child of the QGraphicsItem Pawn, and set position
self.number = num
self.pos = (x,y)
self.gameboard.squares[(x,y)].occupiedBy = self
def move(self, newPos):
self.gameboard.squares[self.pos].occupiedBy = None
self.pos = newPos
self.gameboard.squares[self.pos].occupiedBy = None
def beforeDelete(self):
#a function I add trying to get rid of the crash
self.gameboard = None
self.graphicsPolygon.setParent = None
self.graphicsPix.setParent = None
self.text.setParent = None
def mousePressEvent(self, event):
super(Pawn, self).mousePressEvent(event)
if event.button() == 1:
self.gameboard.currentPawn = self
event.accept()
class Square(QGraphicsPolygonItem):
def __init__(self, gameboard):
self.coordinates = (x,y)
self.occupiedBy = None
What is the proper way to proceed, should I use deleteLater?
Or maybe something with the weakref lib?
Is it because of the variable gameboard in Pawn?
Code:
class Text(QtGui.QLabel):
def __init__(self,parent):
QtGui.QLabel.__init__(self,parent)
def mouseReleaseEvent(self,evt):
pos = evt.pos()
if pos.x() < 0 or pos.y() < 0 or pos.x() > self.width() or pos.y() > self.height():
return
self.parent().update()
self.emit(QtCore.SIGNAL('clicked()'))
def updateText(self, txt):
self.setText(txt)
class Window(QtGui.QMainWindow):
def createNews(self):
self.a1 = Text(self)
self.a1.updateText("Test")
self.a1.setFixedWidth(200)
self.a1.move(23, 173)
self.a1.show()
self.connect(self.a1, QtCore.SIGNAL("clicked()"), self.OpenURL)
def OpenURL(self, URL):
QtGui.QDesktopServices.openUrl(QtCore.QUrl(str(URL)))
I'm trying to make a "clickable" label, so that if I click it, it will open a URL passed in the argument via self.connect(self.a1, QtCore.SIGNAL("clicked()"), self.OpenURL).
The question is, if it's possible to pass an argument (url) to self.OpenURL() so it could open the URL?
Firstly, you should get rid of the old-style signal syntax, and define a custom clicked signal, like this:
class Text(QtGui.QLabel):
clicked = QtCore.pyqtSignal()
def mouseReleaseEvent(self, evt):
...
self.clicked.emit()
Then use a lambda to send the argument with the signal:
self.a1.clicked.connect(lambda: self.OpenURL(url))
More information about new-style signal/slot syntax can be found here.