I am trying to open another wx frame from a main frame UI. I followed the example shown here: https://wiki.wxpython.org/CallAfter but my main UI is still blocked.
Here is the event listener on the main UI:
def testShowGUI(self):
# This process is a long one
# It uses the vtk to read point cloud file and reconstruct the surface
file = "cache/d9c5e8ef-7b7f-485e-8fc8-23098c32afcb.ply"
reader = vtk.vtkPLYReader()
reader.SetFileName(file)
reader.Update()
delaunay = vtk.vtkDelaunay2D()
delaunay.SetAlpha(0.1)
delaunay.SetTolerance(0.0001)
delaunay.SetOffset(1.25)
delaunay.BoundingTriangulationOff()
delaunay.SetInputData(reader.GetOutput())
delaunay.Update()
#Once finish reading and processing the point cloud, pass to the next function for rendering
wx.CallAfter(self.AfterProcess, delaunay)
def AfterProcess(self, data):
meshVisGui = MesVisGUI.MeshVisGui(data)
meshVisGui.Show()
def OnEnter(self, event):
#Event listener when user click on Enter button
my_thread = threading.Thread(target=self.testShowGUI)
my_thread.start()
The code for the separate frame is as below:
class MeshVisGui(wx.Frame):
SPACING = 4
def __init__(self, delaunay, parent=None):
self.delaunayData = delaunay
self.title = "Mesh Visualization"
wx.Frame.__init__(self, None, -1, self.title)
self.Initialize()
def Initialize(self):
self.panel = wx.Panel(self, -1, size=(600, 400), style=wx.BORDER_RAISED)
self.widget3d = wxVTKRenderWindowInteractor(self.panel, -1)
self.widget3d.Enable()
self.render = vtk.vtkRenderer()
self.render.SetBackground(params.BackgroundColor)
self.widget3d.GetRenderWindow().AddRenderer(self.render)
self.interactor = self.widget3d.GetRenderWindow().GetInteractor()
self.interactor.GetInteractorStyle().SetCurrentStyleToTrackballCamera()
self.axesWidget = utils.createAxes(self.interactor)
self.meshActor = utils.build_actor(self.delaunayData)
self.render.AddActor(self.meshActor)
self.render.ResetCamera()
box = wx.BoxSizer(wx.VERTICAL)
box.Add(self.widget3d, 1, wx.EXPAND, self.SPACING)
self.panel.SetSizer(box)
self.Layout()
However, on my main UI, it is still showing a spinning icon and block the UI while it is trying to process the point cloud data. Can someone help me spot what I have done wrong?
Wxpython version: 4.0.1
Python version: 3.6.5
Following on from Mike Driscoll's answer and comment, here is a sample of a threaded task running from another panel.
The thread reports back to the 2nd panel (it's parent) using an event. This allows a progress bar to be updated.
The 2nd panel includes a "Cancel" option for the threaded task, whilst the main frame has a button to test if it is not frozen.
The use of wx.GetApp().Yield() may be slightly old fashioned but I have always found it to be reliable.
import time
import wx
from threading import Thread
import wx.lib.newevent
progress_event, EVT_PROGRESS_EVENT = wx.lib.newevent.NewEvent()
class ThreadFrame(wx.Frame):
def __init__(self, title, parent=None):
wx.Frame.__init__(self, parent=parent, title=title)
panel = wx.Panel(self)
self.btn = wx.Button(panel,label='Stop Long running process', size=(200,30), pos=(10,10))
self.btn.Bind(wx.EVT_BUTTON, self.OnExit)
self.progress = wx.Gauge(panel,size=(300,10), pos=(10,50), range=300)
#Bind to the progress event issued by the thread
self.Bind(EVT_PROGRESS_EVENT, self.OnProgress)
#Bind to Exit on frame close
self.Bind(wx.EVT_CLOSE, self.OnExit)
self.Show()
self.mythread = TestThread(self)
#Enable the GUI to be responsive by briefly returning control to the main App
while self.mythread.isAlive():
time.sleep(0.1)
wx.GetApp().Yield()
continue
try:
self.OnExit(None)
except:
pass
def OnProgress(self, event):
self.progress.SetValue(event.count)
#or for indeterminate progress
#self.progress.Pulse()
def OnExit(self, event):
if self.mythread.isAlive():
self.mythread.terminate() # Shutdown the thread
self.mythread.join() # Wait for it to finish
self.Destroy()
class TestThread(Thread):
def __init__(self,parent_target):
Thread.__init__(self)
self.parent = parent_target
self.stopthread = False
self.time = time.time()
self.start() # start the thread
def run(self):
# A loop that will run for 5 minutes then terminate
while self.stopthread == False:
curr_loop = int(time.time() - self.time)
if curr_loop < 300:
time.sleep(1)
evt = progress_event(count=curr_loop)
#Send back current count for the progress bar
try:
wx.PostEvent(self.parent, evt)
except: # The parent frame has probably been destroyed
self.terminate()
else:
self.terminate()
def terminate(self):
self.stopthread = True
class MyPanel(wx.Panel):
def __init__(self, parent):
wx.Panel.__init__(self, parent)
self.text_count = 0
self.parent=parent
btn = wx.Button(self, wx.ID_ANY, label='Start Long running process', size=(200,30), pos=(10,10))
btn.Bind(wx.EVT_BUTTON, self.Thread_Frame)
btn2 = wx.Button(self, wx.ID_ANY, label='Is the GUI still active?', size=(200,30), pos=(10,50))
btn2.Bind(wx.EVT_BUTTON, self.AddText)
self.txt = wx.TextCtrl(self, wx.ID_ANY, style= wx.TE_MULTILINE, pos=(10,90),size=(400,100))
def Thread_Frame(self, event):
frame = ThreadFrame(title='Threaded Task', parent=self.parent)
def AddText(self,event):
self.text_count += 1
txt = self.txt.GetValue()
txt += "More text " + str(self.text_count)+"\n"
self.txt.SetValue(txt)
class MainFrame(wx.Frame):
def __init__(self):
wx.Frame.__init__(self, None, title='Main Frame', size=(600,400))
panel = MyPanel(self)
self.Show()
if __name__ == '__main__':
app = wx.App(False)
frame = MainFrame()
app.MainLoop()
You do not need threads to open new frames / windows in your wxPython application. You just need to create a sub-class of wx.Frame to hold the code of your other frame. Then from your main application's frame, you can instantiate the other frame and show it. You use the same concept when you create a wx.Dialog or a wx.MessageDialog.
Here is a simple example:
import wx
class OtherFrame(wx.Frame):
"""
Class used for creating frames other than the main one
"""
def __init__(self, title, parent=None):
wx.Frame.__init__(self, parent=parent, title=title)
self.Show()
class MyPanel(wx.Panel):
def __init__(self, parent):
wx.Panel.__init__(self, parent)
btn = wx.Button(self, label='Create New Frame')
btn.Bind(wx.EVT_BUTTON, self.on_new_frame)
def on_new_frame(self, event):
frame = OtherFrame(title='SubFrame',
parent=wx.GetTopLevelParent(self))
class MainFrame(wx.Frame):
def __init__(self):
wx.Frame.__init__(self, None, title='Main Frame')
panel = MyPanel(self)
self.Show()
if __name__ == '__main__':
app = wx.App(False)
frame = MainFrame()
app.MainLoop()
In this example, I set the other frame's parent to the MainFrame instance by using wx.GetTopLevelParent(self). The benefit of setting a parent for the sub-frames is that if I close the main frame, it will cause the other frames to also be closed.
Related
I am using wxPython for the GUI, and I have a different thread that interacts with some I/O. Is there a way to update the GUI to show the I/O status when the thread receives an event from the I/O device?
To be clearer: every 3 seconds the IOthread asks to a parking bar if it is up or down, and I want the GUI to show a red led if it is down or a green led when it is up. So basically I need to communicate the information received by the IOthread to the GUI, which is running in the main thread
You could have a peek at pubsub but as you are using wxPython with a thread, it's easier to use a user defined event.
This adapted code, simply alternates between True and False, obviously you would change that and only post the event, if it changes.
from threading import Thread
import wx
import time
import wx.lib.newevent
progress_event, EVT_PROGRESS_EVENT = wx.lib.newevent.NewEvent()
class WorkThread(Thread):
def __init__(self,parent_target):
"""Init Worker Thread Class."""
Thread.__init__(self)
self.target = parent_target
self.stop_work_thread = 0
self.start() # start the thread
def run(self):
curr_loop = True
while True:
if self.stop_work_thread == 1: # Exit
break
time.sleep(1)
if self.stop_work_thread == 2: # Paused
continue
curr_loop = not curr_loop
evt = progress_event(active=curr_loop,name=self.name)
#Send back current bar position up True, Down False
try:
wx.PostEvent(self.target, evt)
except: # The parent frame has probably been destroyed
self.stop()
return
def stop(self):
self.stop_work_thread = 1
def pause(self):
if self.stop_work_thread == 2:
self.stop_work_thread = 0
self.target.pause_btn.SetLabel('Pause')
else:
self.stop_work_thread = 2
self.target.pause_btn.SetLabel('Paused')
class Progress(wx.Frame):
def __init__(self, parent, title):
super(Progress, self).__init__(parent, title = title,size = (500,300))
left_sizer = wx.BoxSizer(wx.VERTICAL)
middle_sizer = wx.BoxSizer(wx.HORIZONTAL)
self.panel = wx.Panel(self)
self.start_btn = wx.Button(self.panel, label="Start")
self.stop_btn =wx.Button(self.panel, label="Stop")
self.pause_btn =wx.Button(self.panel, label="Pause")
self.quit_btn =wx.Button(self.panel, label="Quit")
self.logger = wx.TextCtrl(self.panel, size=(200,200), style=wx.TE_MULTILINE | wx.TE_READONLY)
self.stop_btn.Disable()
self.pause_btn.Disable()
left_sizer.Add(self.start_btn,0,wx.EXPAND)
left_sizer.Add(self.stop_btn,0,wx.EXPAND)
left_sizer.Add(self.pause_btn,0,wx.EXPAND)
left_sizer.Add(self.quit_btn,0,wx.EXPAND)
middle_sizer.Add(self.logger,0,wx.EXPAND)
self.mainsizer = wx.BoxSizer(wx.HORIZONTAL)
self.mainsizer.Add(left_sizer)
self.mainsizer.Add(middle_sizer)
self.panel.SetSizer(self.mainsizer)
self.Layout()
self.start_btn.Bind(wx.EVT_BUTTON, self.onStart)
self.stop_btn.Bind(wx.EVT_BUTTON, self.onCancel)
self.pause_btn.Bind(wx.EVT_BUTTON, self.onPause)
self.quit_btn.Bind(wx.EVT_BUTTON, self.onExit)
#Bind to the progress event issued by the thread
self.Bind(EVT_PROGRESS_EVENT, self.OnActive)
self.Bind(wx.EVT_CLOSE, self.onExit)
def OnActive(self, event):
if not self.work.is_alive():
return
active = event.active
ident = event.name
if active:
self.logger.Clear()
self.logger.SetBackgroundColour("green")
self.logger.write('\n\n\n\n\n\t\tBar is Up')
else:
self.logger.Clear()
self.logger.SetBackgroundColour("red")
self.logger.write('\n\n\n\n\n\t\tBar is Down')
def onStart(self, event):
self.start_btn.Disable()
self.work = WorkThread(parent_target=self)
self.pause_btn.Enable()
self.stop_btn.Enable()
def onPause(self, event):
if self.work.is_alive():
self.work.pause() # Pause the thread
def onCancel(self, event):
"""Cancel thread process"""
try:
self.work.stop()
self.work.join()
except:
pass
self.onFinish()
def onFinish(self):
"""thread process finished - clean up"""
self.start_btn.Enable()
self.stop_btn.Disable()
self.pause_btn.Disable()
self.pause_btn.SetLabel("Pause")
self.logger.Clear()
self.logger.SetBackgroundColour("white")
def onExit(self, event):
self.onCancel(None)
self.onFinish()
self.Destroy()
app = wx.App()
frame = Progress(None,'Bar Up/Down Display')
frame.Show()
app.MainLoop()
What is the proper way of refreshing a GLCanvas (with heavy 3D geometries) without freezing the GUI?
I'm trying to use a set of GUI elements (buttons, checkboxes) to control a GLCanvas which is refreshed to display a 3D animation. My problem is that the GUI elements are not responsive when the animation loop is on.
I've tried to launch the animation loop in three different ways:
Option 1: within a thread
Option 2: using wx.lib.delayedresult (most likely similar to thread)
Option 3: calling Refresh after the onDraw event
It seems I can get the Option 1 to work, but I need to introduce a sleep delay with time.sleep(xxx) in between the call to the Refresh of the canvas. Otherwise the GUI remains poorly responsive: resizing the window, will end up "graying" the GUI elements, clicking on the checkboxes will trigger events but not the "checking" of the box, the "mouseover" effect over buttons does not work, etc.
I enclose a small working example. In my actual application, I animate a 3D geometry using moderngl, and I noticed that the "sleep" time required varies depending on how heavy the geometry is (as opposed to this example which is extremely lightweight and works with a delay as small as 0.00001s). I'm wondering what I'm missing. Thanks!
import wx
from wx import glcanvas
import wx.lib.delayedresult as delayedresult
from OpenGL.GL import *
import OpenGL.GL.shaders
import time
import numpy as np
from threading import Thread
# --- Option 1: Thread
class TestThread(Thread):
def __init__(self, parent, canvas):
Thread.__init__(self)
self.parent=parent
self.canvas=canvas
self.start() # start the thread
def run(self):
print('Thread running... ')
while self.canvas.animate:
#time.sleep(0.01) # <<<<<<<<<<<< This line needed
self.canvas.Refresh()
print('Tread done ')
class OpenGLCanvas(glcanvas.GLCanvas):
def __init__(self, parent):
glcanvas.GLCanvas.__init__(self, parent, -1, size=(400,400))
self.context = glcanvas.GLContext(self)
self.SetCurrent(self.context)
self.init = False
self.animate = False
self.refreshAfter = False
self.t=0
self.Bind(wx.EVT_PAINT, self.OnPaint)
def OnPaint(self, event):
wx.PaintDC(self)
if not self.init:
self.InitGL()
self.init = True
self.OnDraw()
def InitGL(self):
glEnable(GL_DEPTH_TEST)
def OnDraw(self):
""" Called at every frame"""
glClearColor(0.1, 0.0, np.mod(self.t,1), 1.0)
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
if self.animate:
self.t+=0.0005 # increment time
# ---- Option 3
if self.refreshAfter:
self.Refresh() # Trigger next frame
self.SwapBuffers()
# --- Option 2: delayed results
def onAnimDelayedEnd(self, thread):
""" Consumer """
print('Delayed result done')
jobID = thread.getJobID()
result = thread.get()
def onAnimDelayedStart(self):
print('Delayed result running... ')
while self.animate:
self.Refresh()
class MyPanel(wx.Panel):
def __init__(self, parent):
wx.Panel.__init__(self, parent)
self.canvas = OpenGLCanvas(self)
# GUI
self.rot_btn = wx.Button(self, -1, label="Toggle animation", pos=(430, 10))
self.cbDo = wx.CheckBox (self, -1, label="Do something", pos=(430 ,100))
self.radDo = wx.RadioButton(self, -1, label="Do something", pos=(430,140))
self.radDo2= wx.RadioButton(self, -1, label="Do something", pos=(430,180))
# Binding
self.rot_btn.Bind(wx.EVT_BUTTON, self.toggleAnim)
self.radDo.Bind(wx.EVT_RADIOBUTTON, self.doSomething)
self.radDo2.Bind(wx.EVT_RADIOBUTTON, self.doSomething)
self.cbDo.Bind(wx.EVT_CHECKBOX , self.doSomething)
def toggleAnim(self, event):
if not self.canvas.animate:
self.canvas.animate = True
# --- Option 1: thread
TestThread(self, self.canvas)
# --- Option 2: delayed result
#delayedresult.startWorker(self.canvas.onAnimDelayedEnd, self.canvas.onAnimDelayedStart, jobID=1)
# --- Option 3: refreshloop
#self.canvas.refreshAfter=True
#self.canvas.Refresh() # set the canvas into an "infinite" refresh loop
else:
self.canvas.animate = False
def doSomething(self, event):
print('Do something')
class MyFrame(wx.Frame):
def __init__(self):
wx.Frame.__init__(self, None, title="My wx frame", size=(600,400))
self.Bind(wx.EVT_CLOSE, self.on_close)
self.panel = MyPanel(self)
def on_close(self, event):
self.Destroy()
if __name__ == "__main__":
app = wx.App()
frame = MyFrame().Show()
app.MainLoop()
I am currently training OOP for GUI. I am using the wxPython library to create my windows and customize them.
Right now, I am trying to launch a python script by clicking on a button from an other script.
For that, I have 2 programs, wx_Practicing.py and wx_Practicing_child.py which are in the same folder.
wx_Practicing.py
import wx
import time
import wx_Practicing_child
import threading
import os
import sys
class MainWindow(wx.Frame):
def __init__(self):
wx.Frame.__init__(self, None, wx.ID_ANY, "Test",
wx.DefaultPosition,(1000,850), wx.DEFAULT_FRAME_STYLE, wx.FrameNameStr)
# Click counter and flag variable for the new frame opened
self.click = 0
self.OpenButtonFlag = 0
# Sizer to definit the position of elements
sizer_hori = wx.BoxSizer(wx.HORIZONTAL)
sizer_verti = wx.BoxSizer(wx.VERTICAL)
# Panel
test_panel = PanelMainWindow(self)
test_panel.SetSizer(sizer_verti)
# Button to close the main frame and end the program
btn_quit = wx.Button(test_panel, label ="Quit")
btn_quit.Bind(wx.EVT_BUTTON, self.OnQuit)
sizer_verti.Add(btn_quit)
# Button which displays the number of click done on it since the
# frame is opened
btn_counter = wx.Button(test_panel, label="Click counter")
sizer_verti.Add(btn_counter)
btn_counter.Bind(wx.EVT_LEFT_DOWN, self.OnCount)
# Button which open the child frame from wx_Practicing_child.py
btn_new_frame = wx.Button(test_panel, label = "Open new frame")
sizer_verti.Add(btn_new_frame)
btn_new_frame.Bind(wx.EVT_LEFT_DOWN, self.OnNewFrame)
self.Show()
# Method to quit the frame and close it
def OnQuit(self, event):
self.Close()
#Method to count clicks
def OnCount(self, event):
self.click +=1
print(self.click)
# MEthod which open the child frame
def OnNewFrame(self, event):
if self.OpenButtonFlag == 0 :
print('aaaaaaaa')
os.system('wx_Practicing_child.py')
self.Show()
print("New frame opened")
self.OpenButtonFlag = 1
else :
print("Frame already launched, close it before opening a new one")
class PanelMainWindow(wx.Panel):
def __init__(self, parent):
wx.Panel.__init__(self, parent)
test = wx.App(False)
frame = MainWindow()
test.MainLoop()
wx_Practicing_child.py
import wx
class MainWindow_child(wx.Frame):
def __init__(self):
wx.Frame.__init__(self, None, wx.ID_ANY, "Test",
wx.DefaultPosition, (1000,850), wx.DEFAULT_FRAME_STYLE, wx.FrameNameStr)
self.OpenButtonFlag = 0
# Sizer
sizer_hori = wx.BoxSizer(wx.HORIZONTAL)
sizer_verti = wx.BoxSizer(wx.VERTICAL)
# Panel
test_panel_child = PanelMainWindow_child(self)
test_panel_child.SetSizer(sizer_verti)
# Button to quit the child frame
btn_quit = wx.Button(test_panel_child, label ="Quit")
btn_quit.Bind(wx.EVT_BUTTON, self.OnQuit)
sizer_verti.Add(btn_quit)
self.Show()
# Method used to close the child frame
def OnQuit(self, event):
self.OpenButtonFlag = 0
self.Close()
class PanelMainWindow_child(wx.Panel):
def __init__(self, parent):
wx.Panel.__init__(self, parent)
So basically, the functionning is simple. I launch wx_Practicing.py to open the parent window, then I click on the "Open new frame" button and the child frame from wx_Practicing_child.py appears. If i trigger the button again, it does nothing until I closed the previous child window.
But when I try it, the buttonFlag is set to 1, so it enters in the loop, but the child frame does not appear.
So I would like to know how could I make it work. I am out of option right now.
Thank you.
Welcome to StackOverflow.
The problem is that you are creating the child frame in the wrong way.
You only need to change the line:
os.system('wx_Practicing_child.py')
for:
child = wx_Practicing_child.MainWindow_child()
I am currently learning wxPython library on my own. I got the idea to create a GUI with one main frame which can open a child one.
I know I can do it by compiling both frame into the same code, but for my project, I need to have them separated.
I succeeded in managing the opening of the child frame and its closing, but unfortunately, it creates a new problem in my parent frame.
Here are my codes :
wx_Practicing.py
import wx
import time
import wx_Practicing_child
import threading
import os
import sys
"""Class which defines my main frame."""
class MainWindow(wx.Frame):
def __init__(self):
wx.Frame.__init__(self, None, wx.ID_ANY, "Test", wx.DefaultPosition,
(1000,850), wx.DEFAULT_FRAME_STYLE, wx.FrameNameStr)
# Click counter
self.click = 0
# Init of the opening variable which is set to 1 when a child frame is opened
self.OpenButtonFlag = 0
# Init of the frame child invoked by the parent frame
self.child = wx_Practicing_child.MainWindow_child()
self.child.label = "child"
# Sizers
sizer_hori = wx.BoxSizer(wx.HORIZONTAL)
sizer_verti = wx.BoxSizer(wx.VERTICAL)
# Init of the panel
test_panel = PanelMainWindow(self)
test_panel.SetSizer(sizer_verti)
# Buttons declaration
# Button to quit the main frame
btn_quit = wx.Button(test_panel, label ="Quit")
btn_quit.Bind(wx.EVT_BUTTON, self.OnQuit)
sizer_verti.Add(btn_quit)
# Button counting number of time you trigger it
btn_counter = wx.Button(test_panel, label="Click counter")
sizer_verti.Add(btn_counter)
btn_counter.Bind(wx.EVT_LEFT_DOWN, self.OnCount)
# Button opening the child frame
btn_new_frame = wx.Button(test_panel, label = "Open new frame",
pos=(100,100))
btn_new_frame.Bind(wx.EVT_LEFT_DOWN, self.OnNewFrame)
self.Bind(wx.EVT_CLOSE, self.OnClose)
# Frame displaying
self.Show()
def OnClose(self, event):
self.Destroy(True)
# Method used to close the parent frame
def OnQuit(self, event):
self.Destroy()
print("closed")
# Method used to count number of click
def OnCount(self, event):
self.click +=1
print(self.click)
# Method calling wx_Practicing_child.py to open a child frame
def OnNewFrame(self, event):
if self.child.OpenButtonFlag == 0 :
self.child = wx_Practicing_child.MainWindow_child()
self.child.label = "child"
print("Flag before",self.child.OpenButtonFlag)
self.child.Show()
print("new Frame opened")
self.child.OpenButtonFlag = 1
else :
print("Frame already launched, close the previous one and retry")
print("Flag after", self.child.OpenButtonFlag)
"""Class of the panel"""
class PanelMainWindow(wx.Panel):
def __init__(self, parent):
wx.Panel.__init__(self, parent)
test = wx.App(False)
frame = MainWindow()
test.MainLoop()
and wx_Practicing_child.py
import wx
import time
"""Classe définissant une frame (i.e la zone globale parente). Elle permet
de faire exister le panel et ses attributs."""
class MainWindow_child(wx.Frame):
def __init__(self):
wx.Frame.__init__(self, None, wx.ID_ANY, "Test", wx.DefaultPosition,
(1000,850), wx.DEFAULT_FRAME_STYLE, wx.FrameNameStr)
self.OpenButtonFlag = 0
self.label = "Child"
# Sizers
sizer_hori = wx.BoxSizer(wx.HORIZONTAL)
sizer_verti = wx.BoxSizer(wx.VERTICAL)
# Init of the panel
test_panel_child = PanelMainWindow_child(self)
test_panel_child.SetSizer(sizer_verti)
# Buttons declaration
# Button to quit the frame
btn_quit = wx.Button(test_panel_child, label ="Quit")
btn_quit.Bind(wx.EVT_LEFT_DOWN, self.OnQuit)
sizer_verti.Add(btn_quit)
# Method used to quit the frame
def OnQuit(self, event):
self.OpenButtonFlag = 0
self.Destroy()
print("child print", self.OpenButtonFlag)
"""Class which defines a panel for the child frame"""
class PanelMainWindow_child(wx.Panel):
def __init__(self, parent):
wx.Panel.__init__(self, parent)
So my main problem is that when I click on the "Quit" button or the "x" box on the parent frame (wx_practicing.py), the frame is closed, but the program does not shutdown. After few tries, I have noticed that it seems to be caused by the declaration of self.child in MainWindow.
However, I need this declaration to allow MainWindow to open a MainWindow_child.
I have tried to add a self.child.Close() in my Onquit() method in my MainWindow class but it has been unsuccessful.
After some research, I have seen that I could maybe use CloseEvent handler, but I did not really understand its purpose and how it works.
I hope I have been clear enough.
NOTE: Both programs are in the same folder.
Welcome to StackOverflow
The problem with your code is that you are creating two MainWindow_child instances. You can see this by adding print(self.child) below each line like self.child = wx_Practicing_child.MainWindow_child() in file wx_Practicing.py. If you run your code now and creates a child frame you get something like:
<wx_Practicing_child.MainWindow_child object at 0x102e078b8>
<wx_Practicing_child.MainWindow_child object at 0x1073373a8>
Flag before 0
new Frame opened
Flag after 1
The first two lines above mean that you have two instances of MainWindow_child and the pointer self.child points only to one of them. So when you use self.child.Destroy() or close the child frame in the normal way (red X button) you are destroying only one instance. The other instance keep living forever because you never show it, so you cannot press the close button to destroy it or use self.child.Destroy() because self.child is pointing to a different object. The living forever child frame is the reason why your program never closes.
For more clarity, if you change the OnQuit() and OnClose() methods in wx_Practicing.py to:
def OnClose(self, event):
try:
self.child.Destroy()
except Exception:
pass
self.Destroy()
# Method used to close the parent frame
def OnQuit(self, event):
try:
self.child.Destroy()
except Exception:
pass
self.Destroy()
print("closed")
the program will close if you do not press the Open new frame button because self.child points to the only instance of the child frame. Once you press the button then you have two instance, one of them not shown and without a pointer and the program does not close anymore.
Call the child window with a parent i.e.
self.child = wx_Practicing_child.MainWindow_child(self)
Declare the parent in the child i.e.
class MainWindow_child(wx.Frame):
def __init__(self, parent):
wx.Frame.__init__(self, parent, wx.ID_ANY, "Test", wx.DefaultPosition,
(1000,850), wx.DEFAULT_FRAME_STYLE, wx.FrameNameStr)
Now the child will be destroyed when the parent is destroyed and the program will exit cleanly.
I assume that test_panel_child = PanelMainWindow_child(self) is some sort of editing error, which should really read test_panel_child = wx.Panel(self).
Use self.Destroy() not self.Destroy(True)
Another way to do this is to not perform an initial call to the child frame and move the child open flag into the parent.
To achieve this the child needs to make a note of the parent and ensure that the child open flag is set to false not only on the quit button but also on the window close in the top right-hand corner.
This makes your main program like so:
import wx
import time
import wx_Practicing_child
import threading
import os
import sys
"""Class which defines my main frame."""
class MainWindow(wx.Frame):
def __init__(self):
wx.Frame.__init__(self, None, wx.ID_ANY, "Test", wx.DefaultPosition,
(400,350), wx.DEFAULT_FRAME_STYLE, wx.FrameNameStr)
# Click counter
self.click = 0
# Init of the opening variable which is set to 1 when a child frame is opened
self.OpenButtonFlag = 0
# Init of the frame child invoked by the parent frame
#self.child = wx_Practicing_child.MainWindow_child(self)
#self.child.label = "child"
self.child_open_flag = False
# Sizers
sizer_hori = wx.BoxSizer(wx.HORIZONTAL)
sizer_verti = wx.BoxSizer(wx.VERTICAL)
# Init of the panel
test_panel = PanelMainWindow(self)
test_panel.SetSizer(sizer_verti)
# Buttons declaration
# Button to quit the main frame
btn_quit = wx.Button(test_panel, label ="Quit")
btn_quit.Bind(wx.EVT_BUTTON, self.OnQuit)
sizer_verti.Add(btn_quit)
# Button counting number of time you trigger it
btn_counter = wx.Button(test_panel, label="Click counter")
sizer_verti.Add(btn_counter)
btn_counter.Bind(wx.EVT_LEFT_DOWN, self.OnCount)
# Button opening the child frame
btn_new_frame = wx.Button(test_panel, label = "Open new frame",
pos=(100,100))
btn_new_frame.Bind(wx.EVT_LEFT_DOWN, self.OnNewFrame)
self.Bind(wx.EVT_CLOSE, self.OnClose)
# Frame displaying
self.Show()
def OnClose(self, event):
self.Destroy()
# Method used to close the parent frame
def OnQuit(self, event):
self.Destroy()
print("closed")
# Method used to count number of click
def OnCount(self, event):
self.click +=1
print(self.click)
# Method calling wx_Practicing_child.py to open a child frame
def OnNewFrame(self, event):
if self.child_open_flag:
wx.MessageBox("Frame already launched, close the previous one and retry",'Error', wx.OK | wx.ICON_INFORMATION)
else:
self.child = wx_Practicing_child.MainWindow_child(self)
self.child.label = "child"
self.child.Show()
self.child_open_flag = True
"""Class of the panel"""
class PanelMainWindow(wx.Panel):
def __init__(self, parent):
wx.Panel.__init__(self, parent)
test = wx.App(False)
frame = MainWindow()
test.MainLoop()
and your child program
import wx
import time
"""Classe définissant une frame (i.e la zone globale parente). Elle permet
de faire exister le panel et ses attributs."""
class MainWindow_child(wx.Frame):
def __init__(self, parent):
wx.Frame.__init__(self, parent, wx.ID_ANY, "Test", wx.DefaultPosition,
(200,250), wx.DEFAULT_FRAME_STYLE, wx.FrameNameStr)
# make parent accessible
self.parent = parent
self.label = "Child"
# Sizers
sizer_hori = wx.BoxSizer(wx.HORIZONTAL)
sizer_verti = wx.BoxSizer(wx.VERTICAL)
# Init of the panel
test_panel_child = wx.Panel(self)
test_panel_child.SetSizer(sizer_verti)
# Buttons declaration
# Button to quit the frame
btn_quit = wx.Button(test_panel_child, label ="Quit")
btn_quit.Bind(wx.EVT_LEFT_DOWN, self.OnQuit)
#Reset the open flag if the child is closed not using the button
self.Bind(wx.EVT_CLOSE, self.OnQuit)
sizer_verti.Add(btn_quit)
# Method used to quit the frame
def OnQuit(self, event):
# clear the child open flag in the parent
self.parent.child_open_flag = False
self.Destroy()
I have several calculations to run without any user input and without the user worrying about the program having frozen, so I am trying to show a progress bar popup to keep them aware of what's happening.
Looking around has lead me to believe that I need to use a separate thread to do this, and I came up with this example.
import threading, wx, time
MAX_INT = 10
TEST_TUPLE = [[11, 22],[33,44]]
class mainFrame(wx.Frame):
def __init__(self, parent, ID, title):
wx.Frame.__init__(self, parent, ID, title)
bt = wx.Button(self, wx.ID_OK)
self.Bind(wx.EVT_BUTTON, self.onBt, bt)
def onBt(self,event):
self.dlg = wx.ProgressDialog("title", "message", maximum=MAX_INT)
workThread = threading.Thread(target=self.doWork, args=(TEST_TUPLE,) )
workThread.start()
self.dlg.ShowModal()
def doWork(self, testArg):
# time consuming stuff that affects main GUI
print testArg
for i in range(1, MAX_INT+1):
self.SetPosition((i*4*MAX_INT, i*2*MAX_INT))
time.sleep(1)
print str(i)+" of "+str(MAX_INT)
wx.CallAfter(self.dlg.Update, i, "%i of %i"%(i, MAX_INT))
self.dlg.Destroy()
app = wx.App(False)
fr = mainFrame(None, -1, "Title")
fr.Show()
app.MainLoop()
It seems to work as intended, but is there some housekeeping that I am skipping here?
EDIT: I replaced the dialog with a miniframe so the main window would not freeze, disabled the button so there wouldn't be several frames spawned, and added a crude cancelling method.
import threading, wx, time
MAX_INT = 10
TEST_TUPLE = [[11, 22],[33,44]]
class GaugeFrame(wx.MiniFrame):
def __init__(self, parent, title, maximum):
wx.MiniFrame.__init__(self, parent, title=title, size=(200, 60) )
self.bar = wx.Gauge(self, range=maximum)
self.buCancel = wx.Button(self, label="Cancel")
self.SetBackgroundColour("LTGRAY")
siMainV = wx.BoxSizer(wx.VERTICAL)
siMainV.Add(self.bar)
siMainV.Add(self.buCancel, flag=wx.CENTER)
self.SetSizer(siMainV)
self.Fit()
self.Bind(wx.EVT_BUTTON, self.onCancel, self.buCancel)
def updateGauge(self, value, message=""):
self.bar.SetValue(value)
if message!="":
self.SetTitle(message)
def onCancel(self, e):
self.SetTitle("Cancelling...")
class MainFrame(wx.Frame):
def __init__(self, parent, ID, title):
wx.Frame.__init__(self, parent, ID, title)
self.bt = wx.Button(self, wx.ID_OK)
self.Bind(wx.EVT_BUTTON, self.onBt, self.bt)
def onBt(self, event):
self.gFr = GaugeFrame(self, title="0 of "+str(MAX_INT), maximum=MAX_INT)
self.gFr.Show()
self.gFr.Center()
self.bt.Disable()
workThread = threading.Thread(target=self.doWork, args=(TEST_TUPLE,) )
workThread.start()
def doWork(self, testArg):
# time consuming stuff that affects main GUI
print testArg
for i in range(1, MAX_INT+1):
time.sleep(1)
if self.gFr.GetTitle()=="Cancelling...":
break
print str(i)+" of "+str(MAX_INT)
wx.CallAfter(self.gFr.updateGauge, i, "%i of %i"%(i, MAX_INT))
wx.CallAfter(self.gFr.Destroy)
wx.CallAfter(self.bt.Enable)
app = wx.App(False)
fr = MainFrame(None, -1, "Title")
fr.Show()
app.MainLoop()
Looks pretty good, just a couple of observations.
You should not call ANY window functions on the worker thread. This includes SetPosition and Destroy. You can use wx.CallAfter to invoke these on the main thread just like you are for Update.
You probably should allow the user to cancel the processing.