PySide QWidget immediate update - python

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_())

Related

Can't invoke button click function() outside pyQT based GUI

I am trying to enhance the existing pyQT based GUI framework. The GUI is a multithread GUI based out of pyQT. I have taken some portion of it here to explain the problem
GUI.py - main python script that invokes function to create Tab on GUI
from TabWidgetClass import ConstructGUITabs
class mainWindow(QtGui.QMainWindow):
def __init__(self,**kwargs):
super().__init__()
self.dockList = []
#Lets build&run GUI
self.BuildRunGUI() ### initialize GUI
def BuildRunGUI(self):
### frame, status definition
### tab definition
self.mainWidget = QtGui.QTabWidget(self)
self.setCentralWidget(self.mainWidget)
self.GUIConnect = ConstructGUITabs(docklist=self.dockList, parent_widget=self.mainWidget,mutex=self.mutex)
self.show()
if __name__ == "__main__":
try:
mW = mainWindow()
mW.setGeometry(20, 30, 1920*0.9,1080*0.9)
mW.setTabPosition(QtCore.Qt.AllDockWidgetAreas, QtGui.QTabWidget.North)
#mW.show()
TabWidgetClass.py - Script to create tabs & widgets
from SerialInterface import SerialInterface
from BackQThread import BackQThread
class ConstructGUITabs():
def __init__():
super().__init__() #init!!
#Thread Variables
self.main_thread = None
self.background_thread = None
# Let's define each tab here
self.ADD_CALIBRATE_TAB()
############## Start of ADD_CONNECT_TAB##################################################
def ADD_CALIBRATE_TAB(self, tec_layout):
#Create threads here. Main to handle update in GUI & Background thread for few specific
#time consuming operation
self.main_thread = SerialInterface()
self.background_thread = BackQThread()
#Get button connect to get_value through main thread
self.get_button = QtGui.QLabel(self)
self.set_button = QtGui.QPushButton('Set')
self.set_button.clicked.connect(self.set_value)
#Set button connect to Set_value through background_thread
self.get_button = QtGui.QLabel(self)
self.get_button = QtGui.QPushButton('Get')
self.get_button.clicked.connect(self.get_new_value)
self.pwr_disp_val = QtGui.QLineEdit(self)
self.pwr_disp_val.setText('')
#add the layout
#connect signal for backgrund thread
self.background_thread.read_info_flash.connect(self.update_info_from_flash)
def set_value(self):
self.main_thread.setvalue()
def get_new_value(self):
self.background_thread.call_update_info_bg_thread(flag = 'ReadSignalInfo',
list=[var1, var2])
def update_info_from_flash(self,flashData):
self.pwr_disp_val.setText(flashData)
The 2 library modules SerialInterface & BackQThread are as below:
SerialInterface.py
import pyftdi.serialext
class LaserSerialInterface(object):
def __init__():
self.port = pyftdi.serialext.serial_for_url('COM1',
baudrate=9600,
timeout=120,
bytesize=8,
stopbits=1,
parity='N',
xonxoff=False,
rtscts=False)
def setValue(self, value):
self.port.write(value)
def readvalue(self,value):#nITLA
return (value * 100)
BackQThread.py
from SerialInterface import SerialInterface
class BackQThread(QtCore.QThread):
signal = QtCore.pyqtSignal(object)
read_info_flash = QtCore.pyqtSignal(object)
def __init__(self):
self.serial = SerialInterface()
def call_update_info_bg_thread(self,flag,**kwargs): #nITLA
self.flag = flag
self.kwargs = kwargs
def update_info_bg_thread(self, Signallist): #nITLA
flashData = []
for index in range(0, len(Singnallist)):
flashData.append(self.serial.readvalue(Signallist[index]))
self.read_info_flash.emit(flashData)
def run(self):
while True:
time.sleep(0.1)
if self.flag == None:
pass
elif self.flag == 'ReadSignalInfo':
self.update_info_bg_thread(Signallist = self.kwargs['Signallist'])
self.flag = False
All above interface works without issues.
The new requirement is to do automation on GUI. In such way that, execute GUI options without manually doing clicks. Script should get an access to ConstructGUITabs() class, to invoke the click events.
Here is sample test script which works fine, if I invoke click events.
GUI.py - script is modified to declare GUIConnect as global variables using 'import globfile'
from TabWidgetClass import ConstructGUITabs
import globfile
class mainWindow(QtGui.QMainWindow):
def __init__(self,**kwargs):
super().__init__()
self.dockList = []
#Lets build&run GUI
self.BuildRunGUI() ### initialize GUI
def BuildRunGUI(self):
### frame, status definition
### tab definition
self.mainWidget = QtGui.QTabWidget(self)
self.setCentralWidget(self.mainWidget)
globfile.GUIConnect = ConstructGUITabs(docklist=self.dockList, parent_widget=self.mainWidget,mutex=self.mutex)
........
Test_Script.py
import globfile
globfile.GUIConnect.get_button.click() #this invokes get_new_value
globfile.GUIConnect.set_button.click()# this set_value
As you see here, through *click() function we are again doing mouse click events. I dont want to do this.
So i tried
globfile.GUIConnect.set_value()# this set_value
# This set_Value() would work as we are using only single & main thread
globfile.GUIConnect.get_new_value() #this invokes get_new_value
# This function call wont' get completed. It would stuck at 'self.read_info_flash.emit(flashData)' in BackQThread.py script. Hence, background thread fails to run update function 'update_info_from_flash'.
Can anyone tell me, what is the wrong in invoking directly functions rather than click() events. Why multithread alone fails to complete.

PyQT5 - Add line one by one using a pause

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.

Keep menu open after clicking on the button it is launched with

I have a QToolButton with a menu. When the QToolButton is clicked, the menu appears. The default behavior is that when an action is clicked from the menu, the menu disappears. How can I make it so that the menu stays open until the user clicks elsewhere?
Here is minimal code that shows the behavior:
from PyQt4 import QtGui, QtCore
import sys, os
if __name__ == '__main__':
app = QtGui.QApplication(sys.argv)
toolButton = QtGui.QToolButton()
toolButton.setText('Select')
toolMenu = QtGui.QMenu()
for i in range(3):
action = toolMenu.addAction(str(i))
action.setCheckable(True)
toolButton.setMenu(toolMenu)
toolButton.setPopupMode(QtGui.QToolButton.InstantPopup)
toolButton.show()
sys.exit(app.exec_())
Shamelessly porting this code from this c++ answer:
from PyQt4 import QtGui, QtCore
import sys, os
if __name__ == '__main__':
app = QtGui.QApplication(sys.argv)
toolButton = QtGui.QToolButton()
toolButton.setText('Select')
toolMenu = QtGui.QMenu()
for i in range(3):
checkBox = QtGui.QCheckBox(str(i), toolMenu)
checkableAction = QtGui.QWidgetAction(toolMenu)
checkableAction.setDefaultWidget(checkBox)
toolMenu.addAction(checkableAction)
toolButton.setMenu(toolMenu)
toolButton.setPopupMode(QtGui.QToolButton.InstantPopup)
toolButton.show()
sys.exit(app.exec_())
I made a PyQt5 version based on #three_pineapples's answer and solved what #Space Hornet tried to solve--get the states of checkboxes.
According to the doc of QWidgetAction:
Note that it is up to the widget to activate the action, for example
by reimplementing mouse event handlers and calling QAction::trigger().
So I think one needs to connect the checkbox's stateChanged signal to the action's trigger method.
I also added the a text to the action therefore action.text() gives the same text label as the checkbox. May not be necessary though.
Complete code below:
import sys
from PyQt5 import QtWidgets
from PyQt5.QtCore import pyqtSlot
#pyqtSlot(QtWidgets.QAction)
def menuTriggered(action):
print('state change=',action.text())
return
#pyqtSlot(QtWidgets.QMenu)
def buttonTriggered(menu):
actions=menu.findChildren(QtWidgets.QWidgetAction)
for actii in actions:
wii=actii.defaultWidget()
stateii=wii.isChecked()
print('action', actii.text(), 'is checked:',stateii)
return
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
toolButton = QtWidgets.QToolButton()
toolButton.setText('Select')
toolMenu = QtWidgets.QMenu()
for i in range(3):
checkBox = QtWidgets.QCheckBox(str(i), toolMenu)
checkableAction = QtWidgets.QWidgetAction(toolMenu)
checkableAction.setDefaultWidget(checkBox)
# Add a text to action, for easier handling in slot
checkableAction.setText(str(i))
# Connect the checkbox's stateChanged to QAction.trigger
checkBox.stateChanged.connect(checkableAction.trigger)
toolMenu.addAction(checkableAction)
toolMenu.triggered.connect(menuTriggered)
toolButton.setMenu(toolMenu)
toolButton.setPopupMode(QtWidgets.QToolButton.MenuButtonPopup)
# NOTE that toolButton.clicked work, toolButton.triggered not
toolButton.clicked.connect(lambda: buttonTriggered(toolMenu))
toolButton.show()
sys.exit(app.exec_())
The easiest solution I've managed to find is to make an addition to actionEvent:
class myMenu(QtGui.QMenu):
def actionEvent(self, event):
super().actionEvent(event)
self.show()
I was looking for the exact same thing and used the code from three_pineapples, but I had trouble connecting it the way I wanted. I thought I'd share my solution in case anyone else finds it useful.
The button function is very similar but my code includes my solution for connecting the checkboxes to a function. Also, since they are stored in a list one can connect them individually or in a loop if that's easier.
from PyQt4.QtCore import *
from PyQt4.QtGui import *
import sys, os
##### main window class #####
class main_window(QMainWindow):
def __init__(self):
super(main_window, self).__init__()
self.resize(300, 200)
wdgMain = QWidget()
self.setCentralWidget(wdgMain)
layMain = QGridLayout(wdgMain)
wdgMain.setLayout(layMain)
## checkable tool button ##
tlbToolButton1 = QToolButtonChx("Check Me Out!")
layMain.addWidget(tlbToolButton1, 0, 0)
tlbToolButton1.addItems(["Item" + str(n) for n in range(8)])
## connect tool button checkboxes ##
for i in range(tlbToolButton1.length()):
tlbToolButton1.index(i).stateChanged.connect(self.checkbox_tester)
def checkbox_tester(self, choice):
objSender = self.sender()
strObjectName = objSender.objectName()
print "Action Checker::", strObjectName, ":", choice
##### end of main window class #####
##### checkable tool button class #####
class QToolButtonChx(QToolButton):
def __init__(self, strText=""):
super(QToolButtonChx, self).__init__()
self.setText(strText)
tlbMenu = QMenu(self)
self.setMenu(tlbMenu)
self.setPopupMode(QToolButton.MenuButtonPopup)
self.lstchxItems = []
def addItem(self, strItem):
self.lstchxItems.append(QCheckBox(strItem, self.menu()))
actCheckItem = QWidgetAction(self.menu())
actCheckItem.setDefaultWidget(self.lstchxItems[-1])
self.lstchxItems[-1].setObjectName('chx' + strItem)
self.menu().addAction(actCheckItem)
def addItems(self, lstItems):
for strItem in lstItems:
self.lstchxItems.append(QCheckBox(strItem, self.menu()))
actCheckItem = QWidgetAction(self.menu())
actCheckItem.setDefaultWidget(self.lstchxItems[-1])
self.lstchxItems[-1].setObjectName('chx' + strItem)
self.menu().addAction(actCheckItem)
def index(self, intIndex):
return self.lstchxItems[intIndex]
def length(self):
return len(self.lstchxItems)
##### end of checkable tool button class #####
if __name__ == '__main__':
app = QApplication(sys.argv)
winMain = QMainWindow()
gui = main_window()
gui.show()
sys.exit(app.exec_())

Need help in connecting Connections in a class

I have created a window with QTableWidget having a cell with 2 buttons.
Buttons are created in seperate class where i am passing QTableWidget instance from main procedure.
I am not able to get the button events, which are connected in button Creation class. My code snippet is as below
class Buttons():
def __init__(self,tab):
buttonLayout = QtGui.QHBoxLayout()
buttonLayout.setContentsMargins(0,0,0,0)
self.saveButtonItem = QtGui.QPushButton('Save')
self.deleteButtonItem = QtGui.QPushButton('Delete')
buttonLayout.addWidget(self.saveButtonItem)
buttonLayout.addWidget(self.deleteButtonItem)
cellWidget = QtGui.QWidget()
cellWidget.setLayout(buttonLayout)
tab.insertRow(tab.rowCount())
tab.setCellWidget(tab.rowCount() - 1,0,cellWidget)
self.setconncection()
def setconncection(self):
self.saveButtonItem.clicked.connect(self.btnSaveClicked)
self.deleteButtonItem.clicked.connect(self.btnDeleteClicked)
print 'connections are set'
def btnSaveClicked(self):
print 'save clicked'
def btnDeleteClicked(self):
print 'delete clicked'
class testing(QtGui.QTableWidget):
def __init__(self):
super(testing,self).__init__()
self.setColumnCount(1)
for i in xrange(3):
self.r = Buttons(self)
if __name__ == "__main__" :
import sys
app = QtGui.QApplication (sys.argv)
win = testing ()
win.show()
sys.exit(app.exec_())
My window at run time is as below
After the __init__ of testing, the reference to Buttons instance is lost and the object is destroyed. (Variable r is affected but not used.)
Keeping a link to it (see last line in following code snippet) makes it work.
class testing(QtGui.QTableWidget):
def __init__(self):
super(testing,self).__init__()
self.setColumnCount(1)
self.setRowCount(1)
self.buttons = []
for i in xrange(3):
self.buttons.append(Buttons(self))

placing values on TextCtrl in subthread cause doesn't always work and cause random segmentation faults

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

Categories

Resources