wxPython Panel Text Not Rendering - text

The problem I'm having is that when I set the static text inside a panel (see #Case 2) it only renders the first letter. The code below is a super stripped down version of my actual code but it produces an identical result:
import wx
import time
class TestInterface(wx.Frame):
testStatusFlag = 0
def __init__(self, *args, **kw):
super(TestInterface, self).__init__(*args, **kw)
self.pnl = wx.Panel(self)
self.SetSize((450, 225))
self.SetTitle('example')
self.Centre()
self.Show(True)
self.indicatorFullTest = None
self.buttonFullTest = None
self.setTestStatus(status=1)
def runTest(self, ev=None):
self.setTestStatus(status=2)
#the test is a bunch of functions that take a bunch of time to run
#they're all located in separate files but all access a single piece of hardware
#so multithreading is effectively impossible (I don't want to spend days re-writing stuff to accomodate it)
time.sleep(10)
self.setTestStatus(status=3)
return 0
def setTestStatus(self, ev=None, status=None):
#Handle the optional status argument
if (status in [1,2,3]):
self.testStatusFlag = status
#Remove any old stuff since if we're calling this function they have to get removed
if (self.indicatorFullTest != None):
self.indicatorFullTest.Hide()
if (self.buttonFullTest != None):
self.buttonFullTest.Hide()
#Case 1
if (self.testStatusFlag == 1):
self.buttonFullTest = wx.Button( self.pnl, label='Run Test', pos=(125, 100), size=(250, 50))
self.buttonFullTest.Bind(wx.EVT_BUTTON, self.runTest)
#Case 2
elif (self.testStatusFlag == 2):
self.indicatorFullTest = wx.Panel( self.pnl, pos=(125, 100), size=(250, 50))
wx.StaticText(self.indicatorFullTest, wx.ID_ANY, "Full-Board Test now in progress\nAllow up to 6 min to finish...",
style=wx.ALIGN_CENTRE_HORIZONTAL, pos=(18,7))
self.indicatorFullTest.SetBackgroundColour( 'Tan' )
self.Update()
#Case 3
elif (self.testStatusFlag == 3):
self.buttonFullTest = wx.Button( self.pnl, label='Test Complete\nPress to reset GUI',
pos=(125, 100), size=(250, 50) )
self.buttonFullTest.SetBackgroundColour( (130,255,130) )
self.buttonFullTest.Bind(wx.EVT_BUTTON, self.resetGUI)
#Resets the GUI after a test is complete
def resetGUI(self, ev=None):
self.setTestStatus(status=1) #Reset the fullTest button/indicator thing
if __name__ == '__main__':
ex = wx.App()
gui = TestInterface(None)
ex.MainLoop()
Basically, how do I make the UI fully render the text? I imagine it has something to do with not going back to wx's main loop after changing that indicator, but I feel like calling self.Update() should make that unnecessary. It may also have something to do with how I'm switching between using a button and using a panel (which is probably bad but I'm not sure how else to do it). I know that I could solve this by making my test function run in a separate thread but the problem is that the test function calls separate libraries which I literally do not have the time to re-write.
Thanks

This is a bit easier when you use wxPython's sizers because then you can just Show and Hide widgets. You can see a good example in this tutorial. I would recommend learning sizers just to make your UI more dynamic when you resize the frame.
But regardless, the answer to this conundrum is pretty simple. Instead of calling self.Update() in Case #2, you need to call wx.Yield(). I swapped that one line change in and it worked for me on Linux.

Related

Clickable QLabel

I'm fairly new to Python, but I've tried dozens of variations to get this to work, no luck so far.
Background: I'm writing a simple number slide game, as a practice project, using PyQt5. The gui displays 15 number tiles, and I want the player to be able to click a tile and it will move into the blank spot (none of the game logic is here yet, I'm still prototyping the gui). If it matters, I'm coding in PyCharm 2022.1.2 (community).
I'm not getting any errors when I run the code, but when I click a tile it doesn't move and then the game exits with "Process finished with exit code -1073740791" (no errors)
I know clickable QLabels are not really a thing, you have to do an override. But I'm not sure where my code is failing. I suspect it has something to do with the override (ClicableQLabel), or my move_tile function. Or even how I'm doing QGridLayout. I tried all of the answers in this question but none of them worked for me.
class ClickableQLabel(QLabel):
def __init__(self, when_clicked, parent=None):
QLabel.__init__(self, parent)
self._when_clicked = when_clicked
def mousePressEvent(self, ev):
self._when_clicked(ev)
class NewGame(QWidget):
def __init__(self):
super().__init__()
self.setWindowTitle("Number Slide Game")
self.gameTiles = []
for tile_no in range(0, 15):
tile_no_img_num = tile_no + 1
num_img = QPixmap('images\\numTile{0}.png'.format(str(tile_no_img_num)))
num_label = ClickableQLabel(self.move_tile)
num_label.setPixmap(num_img)
self.gameTiles.append(num_label)
random.shuffle(self.gameTiles)
game_layout = QGridLayout()
game_layout.setHorizontalSpacing(0)
game_layout.setVerticalSpacing(0)
tile_num = 0
for rowNum in range(0, 4):
for colNum in range(0, 4):
if tile_num < 15:
game_layout.addWidget(self.gameTiles[tile_num], rowNum, colNum)
tile_num += 1
self.setLayout(game_layout)
def move_tile(self, tile_to_move):
game_tile_to_move = self.gameTiles[tile_to_move]
game_tile_to_move.anim = QPropertyAnimation(game_tile_to_move, b"pos")
game_tile_to_move.anim.setEndValue(QPoint(220, 221))
game_tile_to_move.anim.setDuration(200)
game_tile_to_move.anim.start()
if __name__ == "__main__":
app = QApplication(sys.argv)
gameWindow = NewGame()
gameWindow.show()
sys.exit(app.exec_())

Automating repetitive code in Python 3 (PyQt5)

I'm new to PyQt5 and pretty new to Python in general. I needed a keyboard layout, and in stead of manually creating a QPushButton for every letter and setting the text, coordinates and size for each i tried to automate it. My idea was to iterate through a dictionary to create a new name for each QPushButton. I then had to use something else than self.dict[x] as text for the QPushButton, because self.dict[x] was a QPushButton itself. I created a list with all the characters and used list[x] in stead. I would use the coords list to tweak the coordinates for each QPushButton through the iterations. My attempt looked like this:
from PyQt5 import QtWidgets
from PyQt5.QtWidgets import QApplication, QMainWindow
import sys
class Window(QMainWindow):
coords = [0, 200]
size = 50
count = 0
list = [chr(x) for x in range(65, 91)]
dict = {}
for x in range(len(list)):
dict[x] = list[x]
def __init__(self):
super(Window, self).__init__()
self.setGeometry(100, 100, 800, 500)
self.setWindowTitle('')
self.makeButtons()
def makeButtons(self):
for x in range(len(self.dict)):
self.dict[x] = QtWidgets.QPushButton(self)
self.dict[x].resize(self.size, self.size)
self.dict[x].move(self.coords[0], self.coords[1])
self.dict[x].setText(self.list[x])
self.coords[0] += self.size
self.count += 1
if self.count == 9:
self.coords = [0, 250]
if self.count == 18:
self.coords = [25, 300]
def window():
app = QApplication(sys.argv)
win = Window()
win.show()
sys.exit(app.exec_())
window()
which does create the keyboard layout i want. However, I got stuck trying to create a method for each QPushButton, so I can't add any functionalty to the buttons. Is it possible to implement the automation of creating other metods in the makeButton method like I did with the QPushButtons themselves, or do i need another strategy to automate it?
tl;dr
Use lambda and emit signals manually:
class Window(QMainWindow):
keyClicked = QtCore.pyqtSignal(object)
# ...
def makeButtons(self):
# ...
for x in range(len(self.dict)):
self.dict[x] = button = QtWidgets.QPushButton(self)
# ...
button.clicked.connect(lambda _, key=self.list[x]: self.keyClicked.emit(key))
# ...
Explanation
Whenever you have lots of widgets that behave in the same way, you should provide a common interface for their behavior. lambda functions can help you with that, but you have to be aware about the scope the variables belong to (remember, as soon as a function exits/returns, any "anonymous" variable that has no persistent relation to other objects will be garbage collected by python, which practically means "deleted").
As you can see in the example above, I'm using two arguments.
The first is the one provided whenever the signal is emitted (any QAbstractButton descendant emits a clicked signal with a bool arguments that reports the new "checked" status, so if the button is checkable the argument will be the button checked state, otherwise it will always be False). We can just ignore that, but it has to be added as an argument of the lambda function.
The latter keyword argument is provided to keep consistence with the scope of the current cycle. In this way we can ensure that the key argument of the lambda function will always be that of the current cycle (otherwise it will always be the last for value assigned to that for cycle).
Addendum
This part of the answer is totally unrequested and unnecessary for the actual purpose of this question; nonetheless, I'm assuming you're new not only to Python and PyQt in general, but to the UX side that programming almost always has to relate to. As programmers, we have to be aware about that, since the majority of users that will finally use our products are not programmers.
Creating a keyboard GUI is not an easy task.
People are accustomed to the keyboards they use everyday (guess what, smartphones). Choosig an alphabet-order based layout for a keyboard that uses a standard "three rows" layout is not only an unpopular choice, but also a wrong one. We're (luckily) far away from that gap-time that was before the 2010's, where almost nobody knew how to type at all, but we also have to consider the results of this aspect.
To add even more mess to that, consider that even if QWERTY layout is almost standard, there are other layouts that differ partially (or completely) from it. For example, French people use AZERTY, while some Central Europe countries use QWERTZ, not to mention non-latin based writings.
And all of this only accounts for the standard alphabet letters.
Finally, the code
from string import ascii_uppercase
from PyQt5 import QtCore, QtWidgets
class Keyboard(QtWidgets.QWidget):
ASCII, QWERTY, DVORAK = 0, 1, 2
KeyLayoutLetters = {
ASCII: [ascii_uppercase[r*9:r*9+9] for r in range(3)],
QWERTY: ['QWERTYUIOP', 'ASDFGHJKL', 'ZXCVBNM'],
DVORAK: ['PYFGCRL', 'AOEUIDHTNS ', 'QJKXBMWVZ'],
}
# some default special stretch set for specific keyboard layouts
KeyStretch = {
QWERTY: [(1, 1), (1, 1), (1, 2)],
DVORAK: [(2, 1), (1, 2), (3, 1)],
}
keySize = 50
spacing = 2
letterClicked = QtCore.pyqtSignal(object)
def __init__(self):
super(Keyboard, self).__init__()
self.setKeyboardLayout()
def setKeyboardLayout(self, keyLayout=None):
keyLayout = keyLayout if keyLayout is not None else self.ASCII
if self.layout() is not None:
QtWidgets.QWidget().setLayout(self.layout())
layout = QtWidgets.QVBoxLayout(self)
layout.setSpacing(self.spacing)
stretches = self.KeyStretch.get(keyLayout, [(1, 1)] * 3)
for row, rowChars in enumerate(self.KeyLayoutLetters.get(keyLayout, self.KeyLayoutLetters[self.ASCII])):
rowLayout = QtWidgets.QHBoxLayout()
rowLayout.setSpacing(self.spacing)
layout.addLayout(rowLayout)
stretchLeft, stretchRight = stretches[row]
rowLayout.addStretch(stretchLeft)
for letter in rowChars:
if not letter.strip():
spacer = QtWidgets.QWidget()
rowLayout.addWidget(spacer)
spacer.setFixedSize(self.keySize * .5, self.keySize)
continue
letterButton = QtWidgets.QPushButton(letter)
rowLayout.addWidget(letterButton, stretch=0)
letterButton.setFixedSize(self.keySize, self.keySize)
letterButton.clicked.connect(lambda _, key=letter: self.letterClicked.emit(key))
rowLayout.addStretch(stretchRight)
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
central = QtWidgets.QWidget()
self.setCentralWidget(central)
layout = QtWidgets.QVBoxLayout(central)
# just a QLineEdit widget to test the text intertion
self.lineEdit = QtWidgets.QLineEdit()
layout.addWidget(self.lineEdit)
self.keyboard = Keyboard()
layout.addWidget(self.keyboard)
self.keyboard.setKeyboardLayout(Keyboard.DVORAK)
self.keyboard.letterClicked.connect(self.lineEdit.insert)
self.keySelector = QtWidgets.QComboBox()
layout.addWidget(self.keySelector)
self.keySelector.addItems(['ASCII', 'QWERTY', 'DVORAK'])
self.keySelector.currentIndexChanged.connect(self.keyboard.setKeyboardLayout)
if __name__ == '__main__':
import sys
app = QtWidgets.QApplication(sys.argv)
keyboard = MainWindow()
keyboard.show()
sys.exit(app.exec_())
Keep in mind that this is a really simple and raw implementation of what you're probably looking for. There is a moltitude of aspects you'd need to take care of (mostly related to special characters that are typical of the user's language).

Retrive date from DateTimeCtrl WXPython

Hey all im having a difficult time with something I believe should be rather simple. Im using WXpython(3.X) to make my GUI and i'm using DatePickerCtrl to form my control. All im trying to do is retrieve the value when I change it. I used the GetValue() method but that only returns the date that currently shows not the one that I changed it to. Then I tried using DateEvent.GetDate but I keep getting an error.
def getStartDate(self):
a = wx.adv.DateEvent(self, self.date_Begin.GetValue(), Event.SetTimestamp()).GetDate
print(a)
return a
and the error that shows up is
**Event.SetTimestamp(): first argument of unbound method must have type 'Event'**
You have declared def getStartDate(self): rather than def getStartDate(self,event):, so you aren't allowing for the event sent to the function.
There is only one event emitted by this function EVT_DATE_CHANGED.
import wx
import datetime
import wx.adv
#----------------------------------------------------------------------
class TestPanel(wx.Frame):
def __init__(self):
wx.Frame.__init__(self,None)
sizer = wx.BoxSizer(wx.VERTICAL)
self.SetSizer(sizer)
dpc1 = wx.adv.DatePickerCtrl( self, wx.ID_ANY, wx.DefaultDateTime)
self.Bind(wx.adv.EVT_DATE_CHANGED, self.OnDateChanged, dpc1)
sizer.Add(dpc1, 0, wx.ALL, 50)
# In some cases the widget used above will be a native date
# picker, so show the generic one too.
dpc2 = wx.adv.GenericDatePickerCtrl(self, size=(120,-1),
style = wx.TAB_TRAVERSAL
| wx.adv.DP_DROPDOWN
| wx.adv.DP_SHOWCENTURY
| wx.adv.DP_ALLOWNONE )
self.Bind(wx.adv.EVT_DATE_CHANGED, self.OnDateChanged, dpc2)
sizer.Add(dpc2, 0, wx.LEFT, 50)
now = wx.DateTime.Now()
print (wx.DateTime.FormatISODate(now))
print (wx.DateTime.Format(now))
dpc2.SetValue(wx.DateTime.Now())
def OnDateChanged(self, evt):
sel_date = evt.GetDate()
print (sel_date.Format("%d-%m-%Y"))
#----------------------------------------------------------------------
if __name__ == '__main__':
app = wx.App()
frame = TestPanel()
frame.Show()
app.MainLoop()
Above I reformat the date to dd/mm/yyyy, as I am not from the USA.
If you want more control use wx.adv.CalendarCtrl
You should be able to bind your DatePickerCtrl widget to wx.adv.EVT_DATE_CHANGED
Then in your event handler, you would do something like this:
def OnDateChanged(self, evt):
new_date = evt.GetDate()
print(new_date)
return new_date
Check out the wxPython demo as it has a good example of this widget as well as most of the others.

wx.DirDialog not closing

Using wxPython version 4.0.1 (pheonix) with python 3.6.5
I use a wxPython DirDialog to allow my user to input a start directory. It correctly selects and populates my "working directory" variable (using GetPath()) but then doesn't ever close the directory dialog prompt box.
I read through the wxPython-user google pages and the only related question I found referred to this as being "intended behavior," implying it would happen later in execution (https://groups.google.com/forum/#!searchin/wxpython-users/close%7Csort:date/wxpython-users/ysEZK5PVBN4/ieLGEWc6AQAJ).
Mine, however, doesn't close until the entire script has completed running (which takes a fair amount of time), giving me the spinning wheel of death. I have tried a combination of calls to try to force the window to close.
app = wx.App()
openFileDialog = wx.DirDialog(None, "Select", curr, wx.DD_DIR_MUST_EXIST)
openFileDialog.ShowModal()
working_directory = openFileDialog.GetPath()
openFileDialog.EndModal(wx.CANCEL) #also wx.Close(True) and wx.Destroy()
openFileDialog.Destroy()
openFileDialog=None
I have also tried creating a window, passing it as the parent of the DirDialog, and then closing the window and it gives the same behavior.
You don't mention which operating system you are on or version of wx but in the partial code that you supplied there is no MainLoop, which is what was mentioned by Robin Dunn in his answer, in your link.
Try this and see if it works the way you would expect.
import wx
from os.path import expanduser
import time
class choose(wx.Frame):
def __init__(self, parent):
wx.Frame.__init__(self, parent, -1, "Dialog")
panel = wx.Panel(self,-1)
text = wx.StaticText(panel,-1, "Place holder for chosen directory")
self.Bind(wx.EVT_CLOSE, self.OnClose)
self.Show()
curr = expanduser("~")
dlg = wx.DirDialog(None, message="Choose a directory", defaultPath = curr,
style=wx.DD_DEFAULT_STYLE|wx.DD_DIR_MUST_EXIST)
if dlg.ShowModal() == wx.ID_OK:
text.SetLabel(dlg.GetPath())
dlg.Destroy()
b = wx.BusyInfo("I'm busy counting",parent=None)
wx.Yield()
for i in range(30):
time.sleep(1)
del b
def OnClose(self, event):
self.Destroy()
if __name__ == '__main__':
my_app = wx.App()
choose(None)
my_app.MainLoop()

OSX: Dialog.Destroy() hangs program in wxPython 3.0

We are working on bringing our application up to date with wxPython 3.0.2, however this is one of two major bug that is still around.
Background: on program start, we spawn a custom dialog telling the user that some things are loading. This dialog has an animation, so it's important to keep the main thread with the GUI clear while we load data in the background thread. When that is done, we sent a command to a callback that Destroy()s the dialog, and the program is able to function as normal.
This works well in 2.8, but it seems to hang our app in 3.0. The dialog message disappears, but we cannot close the program or interact with the GUI, almost as if the GUI was still locked under Modal.
Here's a test script that demonstrates, being as close to the original program as possible in the logic path it takes:
import wxversion
wxversion.select('3.0')
import wx
import time
import threading
class OpenThread(threading.Thread):
def __init__(self, callback):
threading.Thread.__init__(self)
self.callback = callback
self.start()
def run(self):
time.sleep(0.5) # Give GUI some time to finish drawing
for i in xrange(5):
print i
time.sleep(.3)
print "ALL DONE"
wx.CallAfter(self.callback)
class WaitDialog(wx.Dialog):
def __init__(self, parent, title = "Processing"):
wx.Dialog.__init__ (self, parent, id=wx.ID_ANY, title = title, size=(300,30),
style=wx.NO_BORDER)
mainSizer = wx.BoxSizer( wx.HORIZONTAL )
self.SetBackgroundColour(wx.WHITE)
txt = wx.StaticText(self, wx.ID_ANY, u"Waiting...", wx.DefaultPosition, wx.DefaultSize, 0)
mainSizer.Add( txt, 1, wx.ALL|wx.ALIGN_CENTER_VERTICAL, 0 )
self.SetSizer( mainSizer )
self.Layout()
self.Bind(wx.EVT_CLOSE, self.OnClose)
self.CenterOnParent()
def OnClose(self, event):
pass
class MainFrame(wx.Frame):
def __init__(self):
wx.Frame.__init__(self, None, wx.ID_ANY, "Test")
self.waitDialog = None
mainSizer = wx.BoxSizer( wx.HORIZONTAL )
choice = wx.Choice(self, wx.ID_ANY, style=0)
choice.Append("No Selection", 0)
choice.Append("Selection 1", 1)
choice.Append("Selection 2", 2)
mainSizer.Add( choice , 1, wx.ALL|wx.ALIGN_CENTER_VERTICAL, 0 )
self.SetSizer( mainSizer )
self.Show()
self.doThing()
def doThing(self):
self.waitDialog = WaitDialog(self, title="Opening previous fits")
OpenThread(self.closeWaitDialog)
self.waitDialog.ShowModal()
def closeWaitDialog(self):
self.waitDialog.Destroy()
test = wx.App(False)
MainFrame()
test.MainLoop()
You can comment out the self.waitDialog bits and see that it is the dialogs giving trouble. There are other places in the program that this happens in, always after we close out of a Dialog. Is there something I'm missing? Is there a workaround? We also have a few more dialogs that we utilize, so a workaround would ideally be a small fix rather than a huge refactoring
wx.CallAfter basically just puts the event into a queue, so I wonder if the event queue is getting blocked somehow such that it isn't processing the event that would call your event handler to destroy the dialog.
I found the following that might help:
wxPython wx.CallAfter - how do I get it to execute immediately?
Basically you could use wx.WakeUpIdle() or wx.GetApp().ProcessIdle() or maybe even ProcessPendingEvents.
This might also be helpful:
http://wxpython.org/Phoenix/docs/html/window_deletion_overview.html
I also found this useful StackOverflow answer to a similar problem in that you may just need to call the dialog's EndModal method before Destroying it.

Resources