I am trying to display the first page of the stacked widget ( the welcome page ) and after 3 seconds replace it by the second page ( the Menu page ) automatically.
I tried this approach but it doesn't work..
..........
self.stackedWidget.setCurrentIndex(0)
time.sleep(3)
self.stackedWidget.setCurrentIndex(1)
QtCore.QMetaObject.connectSlotsByName(smartUpdaterUI)
..............
Qt uses a GUI event loop to handle events, and update the UI. Any updates to the GUI won't be visible until control is handed over to the event loop and that update is picked up and processed.
The code you write in Python happens in the same thread as the GUI. So while your code is running, the event loop is not, and changes are not being processed.
Side note: this is why your application can 'hang' if you try and do something long-running like accessing an API, without using a separate thread.
In your code, you're setting the current index to 0, then using Python time.sleep() to wait, before updating to index 1.
self.stackedWidget.setCurrentIndex(0)
time.sleep(3) # no event loop running here
self.stackedWidget.setCurrentIndex(1)
While the time.sleep(3) is happening, the execution is held at this point. This means control is not handed back to the Qt event loop, and the first change is not processed. Once the timeout completes, the second index is set, and only then your function returns control back to Qt.
The event loop now applies both changes, but immediately one after another. So all you see is index being set to 1, without first showing 0 and without any delay.
To avoid this, you need to return control back to the event loop after setting the initial index. The simplest way to do this is to just set it, and then trigger the subsequent update using an asynchronous QTimer.
self.stackedWidget.setCurrentIndex(0)
QTimer.singleShot(3000, lambda: self.stackedWidget.setCurrentIndex(1))
The lambda: is used as an anonymous function, so we can pass 1 to setCurrentIndex by delaying execution until the timer is triggered. If you're only ever jumping to a single page you could do this instead:
def go_to_page_1(self):
self.stackedWidget.setCurrentIndex(1)
self.stackedWidget.setCurrentIndex(0)
QTimer.singleShot(3000, go_to_page_1)
void QTimer::singleShot(int msec, const QObject *receiver, const char *member)
This static function calls a slot after a given time interval.
Try it:
import sys
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
class stackedExample(QWidget):
def __init__(self):
super(stackedExample, self).__init__()
self.leftlist = QListWidget()
self.leftlist.insertItem (0, 'Contact' )
self.leftlist.insertItem (1, 'Personal' )
self.leftlist.insertItem (2, 'Educational' )
self.stack1 = QWidget()
self.stack2 = QWidget()
self.stack3 = QWidget()
self.stack1UI()
self.stack2UI()
self.stack3UI()
self.stackWidget = QStackedWidget(self)
self.stackWidget.addWidget(self.stack1)
self.stackWidget.addWidget(self.stack2)
self.stackWidget.addWidget(self.stack3)
hbox = QHBoxLayout(self)
hbox.addWidget(self.leftlist)
hbox.addWidget(self.stackWidget)
self.setLayout(hbox)
self.leftlist.currentRowChanged.connect(self.display)
self.setGeometry(300, 50, 10, 10)
self.setWindowTitle('StackedWidget demo')
self.stackWidget.setCurrentIndex(0)
QTimer.singleShot(3000, lambda: self.display(1))
QTimer.singleShot(6000, lambda: self.display(2))
QTimer.singleShot(9000, lambda: self.display(0))
self.show()
def stack1UI(self):
layout = QFormLayout()
layout.addRow("Name", QLineEdit())
layout.addRow("Address", QLineEdit())
#self.setTabText(0,"Contact Details")
self.stack1.setLayout(layout)
def stack2UI(self):
layout = QFormLayout()
sex = QHBoxLayout()
sex.addWidget(QRadioButton("Male"))
sex.addWidget(QRadioButton("Female"))
layout.addRow(QLabel("Sex"),sex)
layout.addRow("Date of Birth",QLineEdit())
self.stack2.setLayout(layout)
def stack3UI(self):
layout = QHBoxLayout()
layout.addWidget(QLabel("subjects"))
layout.addWidget(QCheckBox("Physics"))
layout.addWidget(QCheckBox("Maths"))
self.stack3.setLayout(layout)
def display(self, i):
self.stackWidget.setCurrentIndex(i)
def main():
app = QApplication(sys.argv)
ex = stackedExample()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
Related
I have an event listener on left double click inside a wx.Grid that opens a dialog box to confirm the change. If the user clicks on "Yes" I call a function that takes around 6 seconds to execute, meanwhile the window freezes. What I want to do is to open a progress bar so the user can wait until the function finishes its task, or better, have the progress bar in the dialog box.
I have no idea where to start as I've never needed a progress bar until now.
A lot of the solutions I've looked at suggest threading, but I'm pretty inexperienced with threads in Python.
I'm hoping someone will be able to give me a hand displaying the progress for a running task using wxPython.
Here is my code so far:
def OnCellLeftDClick(self, evt):
if evt.GetCol() == 17:
dlg = wx.MessageDialog(None, "Do you want to change " + self.GetCellValue(evt.GetRow(), 1) + " bid?",'Updater',wx.YES_NO | wx.ICON_QUESTION)
result = dlg.ShowModal()
if result == wx.ID_YES:
from chanbeBid import changeBidTb
changeBidTb(self.GetCellValue(evt.GetRow(), 1), self.GetCellValue(evt.GetRow(), 16))
evt.Skip()
Thank you,
You can use the _thread module to create the new thread and pubsub to handle the communication between the threads. One way to do it is shown below. I added a few comments to the code.
import wx
import time
import _thread
from pubsub import pub
seconds = 10
class MyFrame(wx.Frame):
def __init__(self):
"""
Just a button to start counting
"""
super().__init__(None, title='Counting until...')
self.panel = wx.Panel(self)
self.button = wx.Button(self.panel, label='Count', pos=(50, 50))
self.button.Bind(wx.EVT_BUTTON, self.OnCount)
## This will subscribe the window to the message 'Finish counting'.
## Thus, everytime the message is broadcast the self.Pass method
## will be executed.
pub.subscribe(self.Pass, 'Finish counting')
def OnCount(self, event):
dlg = wx.MessageDialog(None, "Do you want to count?",
style=wx.YES_NO|wx.ICON_QUESTION)
if dlg.ShowModal() == wx.ID_YES:
self.count()
else:
pass
def count(self):
## Creates and starts a new thread that will execute the self.wait
## method. Notice that the thread is started before the ProgressDialog
## because the ProgressDialog will keep the main thread busy
_thread.start_new_thread(self.wait, (seconds,))
## ProgressDialog that will keep running until maxV is reached or
## until self.keepGoing is set to False
maxV = 100
dlg = wx.ProgressDialog("Progress dialog example",
"An informative message",
maximum = maxV,
parent=self,
style = 0
| wx.PD_APP_MODAL
#| wx.PD_CAN_ABORT
#| wx.PD_CAN_SKIP
| wx.PD_ELAPSED_TIME
#| wx.PD_ESTIMATED_TIME
#| wx.PD_REMAINING_TIME
#| wx.PD_AUTO_HIDE
)
self.keepGoing = True
count = 0
while self.keepGoing and count < maxV:
count += 1
wx.MilliSleep(250)
wx.SafeYield()
(keepGoing, skip) = dlg.Update(count)
dlg.Destroy()
def wait(self, secs):
## This is the function that is executed by the new thread
## when it finishs the wx.CallAfter method broadcast the message
## 'Finish counting' that triggers self.Pass as mentioned above.
time.sleep(secs)
wx.CallAfter(pub.sendMessage, 'Finish counting')
def Pass(self):
## Changes self.keepGoing to false so the ProgressDialog is destroyed.
self.keepGoing = False
if __name__ == "__main__":
app = wx.App()
frame = MyFrame()
frame.Show()
app.MainLoop()
Regarding having the progress bar in the dialog window asking to proceed. This can be done but you need to build your custom dialog window and probably use wx.Gauge instead of wx.ProgressDialog. Also most probably you will want this custom window to be modal (freeze all other windows in the program so the user has to wait for the function running in the thread to finish). For this add the following method to the custom dialog class,
def ShowModal(self):
"""
wx.Dialog behavior
"""
self._disabler = wx.WindowDisabler(self)
self.Show()
self.eventLoop = wx.GUIEventLoop()
self.eventLoop.Run()
and show the custom dialog in the same way as normal wx.Dialog, custom_dialog.ShowModal()
The following code draw single random lines every merry one second. What I would like to do is to keep each line already drawn. What is the best way to do that ?
I know that I need to use a QTimer to do a responsive user interface but first I need to know how to draw more and more lines...
Maybe one way would be to draw all lines hidden and to show more and more lines... Or must I use a QGraphicsView ?
from random import random
import sys
from time import sleep
from PyQt5.QtWidgets import QWidget, QApplication
from PyQt5.QtGui import QPainter
from PyQt5.QtCore import QTimer
LINES = [
(500*random(), 500*random(), 500*random(), 500*random())
for _ in range(50)
]
class Interface(QWidget):
def __init__(self):
super().__init__()
self.max = len(LINES)
self.cursor = 0
self.painter = QPainter()
self.setFixedSize(500, 500)
self.show()
def paintEvent(self, e):
self.painter.begin(self)
self.drawsetpbystep()
self.painter.end()
def drawsetpbystep(self):
if self.cursor < self.max:
self.painter.drawLine(*LINES[self.cursor])
self.update()
sleep(0.25)
self.cursor += 1
if __name__ == '__main__':
app = QApplication(sys.argv)
interface = Interface()
sys.exit(app.exec_())
It is not recommended to use sleep in a GUI, and in the case of PyQt it is very dangerous, because Qt offers alternatives to create the same effect as QTimer, QEventLoop, etc.
Another error is that the QPainter has a very large life cycle, it should only be created and called in paintEvent.
And the last mistake is wanting to pause the task of paintEvent since you're doing it through the drawsetpbystep method. the paintEvent method not only will you use it but actually uses the application whenever you need it, the right thing to do is use a flag to indicate when you should paint as shown below:
LINES = [
(500*random(), 500*random(), 500*random(), 500*random())
for _ in range(50)
]
class Interface(QWidget):
def __init__(self):
super().__init__()
self.max = len(LINES)
self.cursor = 0
self.show()
self.paint = False
timer = QTimer(self)
timer.timeout.connect(self.onTimeout)
timer.start(250)
def paintEvent(self, e):
painter = QPainter(self)
if self.paint:
self.drawsetpbystep(painter)
def onTimeout(self):
self.paint = True
self.update()
def drawsetpbystep(self, painter):
if self.cursor < self.max:
painter.drawLine(*LINES[self.cursor])
self.cursor += 1
self.paint = False
if __name__ == '__main__':
app = QApplication(sys.argv)
interface = Interface()
sys.exit(app.exec_())
Using time.sleep in PyQt applications is not recommended because it blocks execution of the Qt event loop which is responsible for handling user input (via keyboard and mouse) and actually drawing the application window.
Instead, you should use QTimer to schedule execution of a specified method at the times you want. In this case, you probably want to use multiple QTimer.singleShot calls. Likely the first method called by the timer will draw one point/line and then set up a timer to call another method which will draw one point/line and set up a timer to call another method...etc. etc.
EDIT: There are a number of similar posts on PyQt4 progress bars not updating. They all focus on the issue of threads & where the program actually updates the window. Although helpful, my code was so structured that the replies were not practical. The accepted answer given here is simple, to the point & works.
I am using Python 2.7 and PyQT 4 on a Win 7 x64 machine.
I am trying to clear my window of one widget, an 'Accept' button, see code, and replace it with a progress bar.
Even though I close the 'Accept' button & add the progress bar before the processing loop is entered into. The window is only updated after the loop has finished & the progress bar jumps straight to 100%.
My code,
from PyQt4 import QtCore, QtGui
import sys
import time
class CentralWidget(QtGui.QWidget):
def __init__(self, parent=None):
super(CentralWidget, self).__init__(parent)
# set layouts
self.layout = QtGui.QVBoxLayout(self)
# Poly names
self.pNames = QtGui.QLabel("Import file name", self)
self.polyNameInput = QtGui.QLineEdit(self)
# Polytype selection
self.polyTypeName = QtGui.QLabel("Particle type", self)
polyType = QtGui.QComboBox(self)
polyType.addItem("")
polyType.addItem("Random polyhedra")
polyType.addItem("Spheres")
polyType.addItem("Waterman polyhedra")
polyType.activated[str].connect(self.onActivated)
# Place widgets in layout
self.layout.addWidget(self.pNames)
self.layout.addWidget(self.polyNameInput)
self.layout.addWidget(self.polyTypeName)
self.layout.addWidget(polyType)
self.layout.addStretch()
# Combobox choice
def onActivated(self, text):
if text=="Random polyhedra":
self.randomPolyhedra(text)
if text=="Spheres": # not implementaed yet
self.polyTypeName.setText("Not implemented yet.")
self.polyTypeName.adjustSize()
if text=="Waterman polyhedra": # not implementaed yet
self.polyTypeName.setText("Not implemented yet.")
self.polyTypeName.adjustSize()
# New options for random polyhedra choice
def randomPolyhedra(self, text):
self.polyNumberLbl = QtGui.QLabel("How many: ", self)
self.polyNumber = QtGui.QLineEdit(self)
self.acceptSeed = QtGui.QPushButton('Accept') # Accept button created
self.acceptSeed.clicked.connect(lambda: self.ranPolyGen())
self.layout.addWidget(self.polyNumberLbl)
self.layout.addWidget(self.polyNumber)
self.layout.addWidget(self.acceptSeed) # Accept button in layout
self.randFlag = True
self.polyTypeName.setText(text)
self.polyTypeName.adjustSize()
# Act on option choices for random polyhedra
def ranPolyGen(self):
polyCount = int(self.polyNumber.text())
self.progressBar = QtGui.QProgressBar() # Progress bar created
self.progressBar.setMinimum(1)
self.progressBar.setMaximum(polyCount)
self.acceptSeed.close() # Accept button closed
self.layout.addWidget(self.progressBar) # Add progressbar to layout
for poly in range(1, polyCount+1):
time.sleep(1) # Calls to main polyhedral generating code go here
print poly
self.progressBar.setValue(poly)
self.doneLbl = QtGui.QLabel("Done", self)
self.layout.addWidget(self.doneLbl)
# Creates GUI
class Polyhedra(QtGui.QMainWindow):
def __init__(self):
super(Polyhedra, self).__init__()
# Place central widget in layout
self.central_widget = CentralWidget(self)
self.setCentralWidget(self.central_widget)
# Set up window
self.setGeometry(500, 500, 300, 300)
self.setWindowTitle('Pyticle')
self.show()
# Combo box
def onActivated(self, text):
self.central_widget.onActivated(text)
def main():
app = QtGui.QApplication(sys.argv)
poly = Polyhedra()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
Below is a picture of during loop execution & after completion.
I dont think I have got my head around the addWidget() method. I was under the impression that this would add another widget to the present layout (a vbox layout here) & that the .close() method removed a widget when directed to do so.
What am I missing?
You can add:
from PyQt4.QtGui import QApplication
Then in your for loop:
QApplication.processEvents()
Your app is actually becoming unresponsive, you need to call processEvents() to process the events and redraw the gui. I am not overly familiar with pyqt but I imagine another alternative is using a thread.
I'm trying to create a gui application using wxpython and I got some issues with the TextCtrl element. The effect that I am trying to achieve is that the user will enter a command to a text field (command) and the command might pop a message that appears in the `(out) field. After some time (0.7 seconds on this example) the message will get back to a default message ("OutPut"). I have two problems:
The message doesn't always appear.
The program sometime crashes due to a segmentation fault and I don't get any Error message to handle that.
I guess that the two related in some way, but I don't know why. In the following example, I only type "test" and wait until the original message appear. Both problems happen on that scenario.
I post here two files that serve as smallest working example. File number 1, creates the GUI,
import wx
import os.path
import os
from threading import Thread
from time import sleep
from MsgEvent import *
class MainWindow(wx.Frame):
def __init__(self):
super(MainWindow, self).__init__(None, size=(400,200),)
#style=wx.MAXIMIZE)
self.CreateInteriorWindowComponents()
self.CreateKeyBinding()
self.command.SetFocus()
self.Layout()
def Test(self):
self.command.SetValue('open')
self.ParseCommand(None)
def PostMessage(self,msg):
'''For its some reason, this function is called twice,
the second time without any input. I could'nt understand why.
For that, the test :if msg == None'''
if msg == None: return
worker = MessageThread(self,msg,0.7,'OutPut')
worker.start()
def CreateKeyBinding(self):
self.command.Bind(wx.EVT_CHAR,self.KeyPressed)
def KeyPressed(self,event):
char = event.GetUniChar()
if char == 13 and not event.ControlDown(): #Enter
if wx.Window.FindFocus() == self.command:
self.ParseCommand(event)
else:
event.Skip()
def ParseCommand(self,event):
com = self.command.GetValue().lower() #The input in the command field
self.PostMessage(com)
def CreateInteriorWindowComponents(self):
''' Create "interior" window components. In this case it is just a
simple multiline text control. '''
self.panel = wx.Panel(self)
font = wx.SystemSettings_GetFont(wx.SYS_SYSTEM_FONT)
font.SetPointSize(12)
self.vbox = wx.BoxSizer(wx.VERTICAL)
#Out put field
self.outBox = wx.BoxSizer(wx.HORIZONTAL)
self.out = wx.TextCtrl(self.panel, style=wx.TE_READONLY|wx.BORDER_NONE)
self.out.SetValue('OutPut')
self.out.SetFont(font)
self.outBox.Add(self.out,proportion=1,flag=wx.EXPAND,border=0)
self.vbox.Add(self.outBox,proportion=0,flag=wx.LEFT|wx.RIGHT|wx.EXPAND,border=0)
#Setting the backgroudn colour to window colour
self.out.SetBackgroundColour(self.GetBackgroundColour())
#Commands field
self.commandBox = wx.BoxSizer(wx.HORIZONTAL)
self.command = wx.TextCtrl(self.panel, style=wx.TE_PROCESS_ENTER)
self.command.SetFont(font)
self.commandBox.Add(self.command, proportion=1, flag=wx.EXPAND)
self.vbox.Add(self.commandBox, proportion=0, flag=wx.LEFT|wx.RIGHT|wx.EXPAND, border=0)
self.panel.SetSizer(self.vbox)
return
#Close the window
def OnExit(self, event):
self.Close() # Close the main window.
app = wx.App()
frame = MainWindow()
frame.Center()
frame.Show()
app.MainLoop()
And file number 2, called MsgThread.py handle the events.
import wx
import threading
import time
myEVT_MSG = wx.NewEventType()
EVT_MSG = wx.PyEventBinder(myEVT_MSG,1)
class MsgEvent(wx.PyCommandEvent):
""" event to signal that a message is ready """
def __init__(self,etype,eid,msg='',wait=0,msg0=''):
""" create the event object """
wx.PyCommandEvent.__init__(self,etype,eid)
self._msg = unicode(msg)
self._wait_time = wait
self._reset_message = unicode(msg0)
def GetValue(self):
""" return the value from the event """
return self._msg
class MessageThread(threading.Thread):
def __init__(self,parent,msg='',wait=0,msg0=''):
"""
parent - The gui object that shuold recive the value
value - value to handle
"""
threading.Thread.__init__(self)
if type(msg) == int:
msg = unicode(msg)
self._msg = msg
self._wait_time = wait
self._reset_message = msg0
self._parent = parent
print self._msg
def run(self):
""" overide thread.run Don't call this directly, its called internally when you call Thread.start()"""
self._parent.out.SetValue(unicode(self._msg))
time.sleep(self._wait_time)
self._parent.out.SetValue(self._reset_message)
self._parent.MessageFlag = False
event = MsgEvent(myEVT_MSG,-1,self._msg)
wx.PostEvent(self._parent,event)
What is faulty?
WX Python is not thread safe except for 3 functions (wx.CallAfter, wx.CallLater, wx.PostEvent)
So basically what you have to do is ensure you never call any widget directly from within the subthread. You may use events or CallAfter.
You also might want to check this:
Nice writeup about threads and wxpython
This might help as well
Lokla
In my application, I have a call to an external module which spawns some threads, does some stuff, then returns a value. I'm trying to get a QMessageBox to show before and a QLabel to update after this is complete, but I'm stumped. The code goes something like this (called from QObject.connect on a button):
def _process(self):
self._message_box.show()
for i in range(3):
rv = external_module_function_with_threads() // blocking function call
label = getattr(self, "label%d" % (i + 1))
label.setText(rv)
When I click the button and the function is called, the message box only shows after the loop completes. The labels only update after the loop completes as well. I tried calling label.repaint() in the loop, but all that seems to do is make the message box show up earlier (but with no text in it).
I know I'm not violating the "GUI operations from outside the main thread" rule (...right?), so is there a way to force an update?
For your message box use self._message_box.exec_(). From my understanding of your question, I think this will do what you want.
from PySide.QtCore import *
from PySide.QtGui import *
import sys
import time
class Main(QWidget):
def __init__(self, parent=None):
super(Main, self).__init__(parent)
layout = QVBoxLayout(self)
button = QPushButton("Press me")
self.label = QLabel("Run #")
map(layout.addWidget, [button, self.label])
button.pressed.connect(self.buttonPressed)
self.messageBox = QMessageBox()
def buttonPressed(self):
self.messageBox.exec_()
Thread().run(self.label)
class Thread(QThread):
def run(self, label):
for x in range(5):
self.updateLabel(label)
app.processEvents()
time.sleep(.5)
def updateLabel(self, label):
try:
number = int(label.text().split(" ")[-1])
number += 1
except ValueError:
number = 0
label.setText("Run %i" % number)
app = QApplication([])
main = Main()
main.show()
sys.exit(app.exec_())