Dialog is not modal when ProgressDialog and Dialog are opened in sequence - python

I'm running wxPython 4.0.1 msw (phoenix) with Python 3.6.5 on a Windows7 machine, as well as wxPython 2.9.4 with Python 2.7.
I'm observing an issue with a modal dialog, which doesn't block the access to its parent window behind. This only occurs if I run a progress dialog followed by a modal dialog. This behavior is somehow related to custom dialogs. Integrated dialogs like wx.MessageDialog doesn't seem to have this issue.
To isolate the issue, I've written an example. The first two buttons open either the progress or the modal dialog and work properly. The third button opens both dialogs in sequence. In this case the modal functionality of the custom dialog doesn't work and I'm able to access and close the mainframe. Which causes multiple issues.
Dialog is not modal, the main window can be accessed and closed
import wx
class SomeDialog(wx.Dialog):
def __init__(self, parent):
wx.Dialog.__init__(self, parent, title='SomeDialog',
style=wx.DEFAULT_DIALOG_STYLE)
self.button_ok = wx.Button(self, wx.ID_OK, size=(120,-1))
hsizer = wx.BoxSizer(wx.HORIZONTAL)
hsizer.Add(self.button_ok, 0, wx.ALL|wx.ALIGN_CENTER, 10)
self.SetSizer(hsizer)
self.SetSize(self.BestSize)
self.Layout()
class TestFrame(wx.Frame):
def __init__(self):
wx.Frame.__init__(self, None, -1, size=(400, 400))
self.button_progress = wx.Button(self, -1, 'Show Progress')
self.button_modal = wx.Button(self, -1, 'Show Modal')
self.button_both = wx.Button(self, -1, 'Show Both')
self.Bind(wx.EVT_BUTTON, self.on_button, self.button_progress)
self.Bind(wx.EVT_BUTTON, self.on_button, self.button_modal)
self.Bind(wx.EVT_BUTTON, self.on_button, self.button_both)
sizer = wx.BoxSizer()
sizer.Add(self.button_progress)
sizer.Add(self.button_modal)
sizer.Add(self.button_both)
self.SetSizer(sizer)
def on_button(self, event):
if event.EventObject is self.button_progress:
self._show_progress_dialog()
elif event.EventObject is self.button_modal:
self._show_modal_dialog()
else:
self._show_progress_dialog()
self._show_modal_dialog()
def _show_progress_dialog(self):
max = 10
dlg = wx.ProgressDialog('Progress dialog example', 'Some message',
maximum=max, parent=self,
style=wx.PD_APP_MODAL|wx.PD_AUTO_HIDE)
keepGoing = True
count = 0
while keepGoing and count < max:
count += 1
wx.MilliSleep(250)
wx.Yield()
(keepGoing, skip) = dlg.Update(count)
dlg.Destroy()
def _show_modal_dialog(self):
with SomeDialog(self) as dlg:
dlg.CenterOnParent()
dlg.ShowModal()
if __name__ == '__main__':
app = wx.App()
frame = TestFrame()
frame.Show()
app.MainLoop()
In case this is an issue in the wxpython framework and not an issue with my implementation, it would be great if someone could provide me a workaround to show such dialogs in sequence.

This looks like a bug to me. I'm not sure why its happening, but one workaround would be to use wx.CallLater
changing _show_modal_dialog to:
def _show_modal_dialog(self):
def _make_dialog():
with SomeDialog(self) as dlg:
dlg.CenterOnParent()
dlg.ShowModal()
wx.CallLater(50, _make_dialog) # 50 mils is arbitrary
Seems to resolve the issue of the dialog not acting modalish. The problem with this workaround is that it will be non-blocking, meaning that any code that has to wait for the dialog to return has to be moved into the dialog class or passed to the dialog as a callback.

In the meantime I found a workaround myself which I like to share.
I added a handler catching the windows close event.
class TestFrame(wx.Frame):
def __init__(self):
#...
self.Bind(wx.EVT_CLOSE, self.on_close)
This event function checks if some child dialog is open and modal and performs a veto.
def on_close(self, event):
# In case any modal dialog is open, prevent the frame from closing.
for children in (c for c in self.Children if isinstance(c, wx.Dialog)):
if children.IsModal():
event.Veto()
return
event.Skip()
This is also only a workaround, but I seems to work for my use cases.

Related

wxPython/wxGtk Platform Issue: Cannot Force Refresh/Update on Opening Modal Dialog Panel

System info:
Linux: Lubuntu/Ubuntu Jammy 21.04.1 x86_64
wxPython: python3-wxgtk4.0 4.0.7
wxWidgets: libwxgtk3.0-gtk3-0
Gtk: libgtk-3-0 3.24.33
I think I am having a platform related issue. I create a wx.Dialog but cannot force the layout to update consistently on the Modal window when it is opened.
Code:
class Dialog(wx.Dialog):
def __init__(self, parent):
wx.Dialog.__init__(self, parent, wx.ID_ANY, "A Dialog Window",
parent.GetPosition(), wx.Size(640, 480),
wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER)
tabs = wx.Notebook(self)
# create first page with centered text
panel1 = wx.Panel(tabs, wx.ID_ANY)
layout1 = wx.BoxSizer(wx.VERTICAL)
layout1.AddStretchSpacer()
layout1.Add(wx.StaticText(panel1, label="Centered"), 1,
wx.ALIGN_CENTER)
layout1.AddStretchSpacer()
panel1.SetSizer(layout1)
panel1.SetAutoLayout(True)
panel1.Layout()
# create second page with non-centered text
panel2 = wx.Panel(tabs, wx.ID_ANY)
layout2 = wx.BoxSizer(wx.VERTICAL)
layout2.Add(wx.StaticText(panel2, label="Not Centered"), 1)
panel2.SetSizer(layout2)
panel2.SetAutoLayout(True)
panel2.Layout()
tabs.AddPage(panel1, "Page 1")
tabs.AddPage(panel2, "Page 2")
class Window(wx.Frame):
def __init__(self):
wx.Frame.__init__(self, None, wx.ID_ANY, "Test", wx.Point(50, 50),
wx.Size(200, 200))
btn = wx.Button(self, wx.ID_ANY, "PUSH ME")
btn.Bind(wx.EVT_BUTTON, self.onButton)
def onButton(self, evt):
dia = Dialog(self)
dia.ShowModal()
#dia.Destroy()
I open & close the dialog multiple times but the text is not always centered or fully drawn. The times that it is centered & drawn correctly the system shows this message (error?): gtk_box_gadget_distribute: assertion 'size >= 0' failed in GtkNotebook
If I don't call Destroy() on the dialog, once it is aligned correctly on opening it will be aligned subsequently.
I have tried to force redrawing by calling Refresh() & Update() on the dialog, the notebook, & the panel in the constructor & on the dialog after instantiation & before calling ShowModal(). The only thing that has worked is to instantiate the dialog as a class member in the main window constructor (& omit calling wx.Dialog.Destroy()):
...
self.dia = Dialog(self)
def onButton(self, evt):
self.dia.ShowModal()
Am I experiencing a limitation with the Gtk UI that cannot be circumvented? All the suggestions I have come across say to use Refresh() & Update(). But these are not working for me. I have also tried using wx.GetApp().Yield() before calling ShowModal().
How do you force refresh of a wx.Panel?
Python GUI does not update until entire process is finished
https://discuss.wxpython.org/t/problem-updating-widget-immediately-with-layout-and-update/34452
Edit: Here is a screenshot of the dialog showing how the text is off center & not fully displayed:
If I remove the call to panel1.Layout() The text is fully displayed but still not centered. If I use SetSizerAndFit instead of SetSizer & SetAutoLayout some space is allocated above for the spacer, but text is still not centered:
...
#panel1.SetSizer(layout1)
#panel1.SetAutoLayout(True)
#panel1.Layout()
panel1.SetSizerAndFit(layout1)
...
The problem is within the version of wxPython (4.0.7). It works correctly with 4.1.1 (installed via PyPI as Ubuntu repos do not provide a later version yet).

Using accelerator table without menuitem

I have a wxpython desktop application and I am using python 2.7 and wxpython 2.8.
I know how to add an accelerator table to a menuitem but I would like to fire an event when a user press a certain combination of keys without having a menuitem.
The user could have the focus on any field in my UI but when he press (for instance) CTRL-L an event should be fired. How to do this ?
Thanks for any help
You always need to bind your accelerator table items to wx.EVT_MENU, but wxPython doesn't require that you use a menu item object. Here's a simple example:
import wx
class MyForm(wx.Frame):
#----------------------------------------------------------------------
def __init__(self):
wx.Frame.__init__(self, None, wx.ID_ANY, "Tutorial", size=(500,500))
# Add a panel so it looks the correct on all platforms
panel = wx.Panel(self, wx.ID_ANY)
randomId = wx.NewId()
self.Bind(wx.EVT_MENU, self.onKeyCombo, id=randomId)
accel_tbl = wx.AcceleratorTable([(wx.ACCEL_CTRL, ord('Q'), randomId )])
self.SetAcceleratorTable(accel_tbl)
text = wx.TextCtrl(panel)
text.SetFocus()
#----------------------------------------------------------------------
def onKeyCombo(self, event):
""""""
print "You pressed CTRL+Q!"
# Run the program
if __name__ == "__main__":
app = wx.App(False)
frame = MyForm()
frame.Show()
app.MainLoop()
In this example, we just create a random id, bind that id to an event handler and then create an accelerator that will fire that handler, which in this case is CTRL+Q. To make things more interesting, I added a text control widget and set the focus to that. Then if you press CTRL+Q, you should see the event handler fire and some text appear in your console window.
You can read more about accelerators here:
http://www.blog.pythonlibrary.org/2010/12/02/wxpython-keyboard-shortcuts-accelerators/

wxPython event starts up at startup

So I am a complete beginner at python and usually code in C/C++ or Java. In the process of developing a GUI my events keep getting called at the start of the program running instead of when I click the button. (i know my indents are wrong because I needed to put it into a code block). How do I make my events only get called when I left click the button?
def __init__(self, parent, title):
super(QuadDash, self).__init__(parent, title=title, size=(1024, 780))
self.SetBackgroundColour("white")
pnl = wx.Panel(self)
cbtn = wx.Button(pnl, label='Start Quad', pos=(20,30))
self.Bind(wx.EVT_LEFT_DOWN, self.start_quad_event(), cbtn)
self.Show(True)
def start_quad_event(self):
dlg = wx.MessageDialog(self, "Test", "ABC", wx.YES_NO | wx.ICON_QUESTION)
dlg.ShowModal()
if __name__ == '__main__':
app = wx.App()
qd = QuadDash(None, title='QuadCopter Dashboard')
app.MainLoop()
The offending line is:
self.Bind(wx.EVT_LEFT_DOWN, self.start_quad_event(), cbtn)
You actually call start_quad_event() and pass the result to bind().
The correct line is:
self.Bind(wx.EVT_LEFT_DOWN, self.start_quad_event, cbtn)
Note: No parentheses. Here you actually pass the function to bind().

wxPython change mouse cursor to notify a long running operation

I am building a Python program that searches things on a remote website.
Sometimes the operation takes many seconds and I believe that the user will not notice the status bar message "Search Operation in progress".
Therefore, I would like to change the mouse cursor to highlight that the program is still waiting for a result.
This is the method I am using:
def OnButtonSearchClick( self, event ):
"""
If there is text in the search text, launch a SearchOperation.
"""
searched_value = self.m_search_text.GetValue()
if not searched_value:
return
# clean eventual previous results
self.EnableButtons(False)
self.CleanSearchResults()
operations.SearchOperation(self.m_frame, searched_value)
I tried two different approaches, both before the last line:
wx.BeginBusyCursor()
self.m_frame.SetCursor(wx.StockCursor(wx.CURSOR_WAIT))
None of them are working.
I am using KDE under GNU/Linux. This does not work under Gnome, too
Any hints? Thank you
I asked Robin Dunn, the maker of wxPython about this, and it looks like this should work, but doesn't. However, if you call the panel's SetCursor(), it DOES work or so I'm told. Here's an example you can try:
import wx
########################################################################
class MyForm(wx.Frame):
#----------------------------------------------------------------------
def __init__(self):
wx.Frame.__init__(self, None, wx.ID_ANY, "Tutorial")
# Add a self.panel so it looks the correct on all platforms
self.panel = wx.Panel(self, wx.ID_ANY)
btn = wx.Button(self.panel, label="Change Cursor")
btn.Bind(wx.EVT_BUTTON, self.changeCursor)
sizer = wx.BoxSizer(wx.HORIZONTAL)
sizer.Add(btn)
self.panel.SetSizer(sizer)
#----------------------------------------------------------------------
def changeCursor(self, event):
""""""
myCursor= wx.StockCursor(wx.CURSOR_WAIT)
self.panel.SetCursor(myCursor)
#----------------------------------------------------------------------
# Run the program
if __name__ == "__main__":
app = wx.PySimpleApp()
frame = MyForm().Show()
app.MainLoop()

How to make something like a log box in wxPython

I'm assuming this is possible with a multiline text box, but not sure how to do it. What I'm looking to do is make a log box in my wxPython program, where I can write messages to it when certain actions happen. Also, i need to write the messages not only when an event happens, but certain times in the code. How would i get it to redraw the window so the messages appear at that instant?
I wrote an article on this sort of thing a couple years ago:
http://www.blog.pythonlibrary.org/2009/01/01/wxpython-redirecting-stdout-stderr/
If you would like just a log dialog in wxpython, use wx.LogWindow:
import wx
class MainWindow(wx.Frame):
def __init__(self, parent=None):
wx.Frame.__init__(self, parent, wx.NewId(), 'Logging')
self.log_window = wx.LogWindow(self, 'Log Window', bShow=True)
box_sizer = wx.BoxSizer(orient=wx.VERTICAL)
show_log_button = wx.Button(self, wx.NewId(), 'Show Log')
show_log_button.Bind(wx.EVT_BUTTON, self._show_log)
log_message_button = wx.Button(self, wx.NewId(), 'Log Message')
log_message_button.Bind(wx.EVT_BUTTON, self._log_message)
box_sizer.AddMany((show_log_button, log_message_button))
self.SetSizer(box_sizer)
self.Fit()
self.Bind(wx.EVT_CLOSE, self._on_close)
def _show_log(self, event):
self.log_window.Show()
def _log_message(self, event):
wx.LogError('New error message')
def _on_close(self, event):
self.log_window.this.disown()
wx.Log.SetActiveTarget(None)
event.Skip()
if __name__ == '__main__':
app = wx.PySimpleApp()
dlg = MainWindow()
dlg.Show()
app.MainLoop()
Where bShow in wx.LogWindow is if it's initially shown or not. This will log nicely all your wx.LogX messages that you can trigger, and it still passes it on to any other handlers.
Another method you could use would be to log with python and then, upon opening a frame/window with a text control in it, use LoadFile to open the log file:
import logging
LOG_FILENAME = 'example.log'
logging.basicConfig(filename=LOG_FILENAME,level=logging.DEBUG)
logging.debug('This message should go to the log file')
Then, when creating a wx.TextCtrl somewhere:
log_control = wx.TextCtrl(self, wx.NewId(), style=wx.TE_MULTILINE|wx.TE_READONLY)
log_control.LoadFile('example.log')
EDIT:
This now works with the _on_close event! Thanks Fenikso

Categories

Resources