wxPython TreeCtrl very slow with millions of nodes (on multi-select tree control) - python-3.x

I am creating a tree with millions of nodes, but when I switched to using multiple-select on a tree control (wx.TR_MULTIPLE), actions on tree become slower, I only click to select a node and it takes me a few seconds. This does not happen when I use the single-select style (wx.TR_SINGLE).
I have tried to not set data for any node and did not use any event but it still slow.
Is there any way to use multiple-select on a tree control and the tree still fast as single-select?
I've pasted the modified demo code in below:
import wx
class MyTree(wx.TreeCtrl):
def __init__(self, parent, id, pos, size, style):
wx.TreeCtrl.__init__(self, parent, id, pos, size, style)
self.Bind(wx.EVT_TREE_SEL_CHANGED, self.item_changed)
def item_changed(self, evt):
print(self.GetItemData(evt.GetItem()))
class TreePanel(wx.Panel):
def __init__(self, parent):
wx.Panel.__init__(self, parent)
self.tree = MyTree(self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TR_HAS_BUTTONS | wx.TR_MULTIPLE)
self.root = self.tree.AddRoot('ROOT')
node1 = self.tree.InsertItem(self.root, 0, 'Node 1', data='node 1')
for i in range(1000000):
self.tree.PrependItem(node1, 'Sub node 1: ' + str(i), data='Sub node 1: ' + str(i))
self.tree.Expand(self.root)
sizer = wx.BoxSizer(wx.VERTICAL)
sizer.Add(self.tree, 1, wx.EXPAND)
self.SetSizer(sizer)
class MainFrame(wx.Frame):
def __init__(self):
wx.Frame.__init__(self, parent=None, title='TreeCtrl Demo')
panel = TreePanel(self)
self.Show()
if __name__ == '__main__':
app = wx.App(redirect=False)
frame = MainFrame()
app.MainLoop()

I also have the same problems. But when I change to the single choice it becomes fast again, but at this time we can not choose multiple node... hmmm.
I think this related with C-code inside the framework

Related

How to correctly position the QDockWidgets, with a size hint

How to correctly position a different Dockwidgets inside a QMainwindow based on the size hint given to each of the dockwidgets. I tried to position based on addDockWidget and setAllowedAreas, but seems I am missing up the concept. And turn off the tabified when moving the QDockwidgets (prevent them from group in tabs).
from PyQt5.QtWidgets import *
from PyQt5 import QtCore, QtGui
from PyQt5.QtGui import *
from PyQt5.QtCore import *
import sys
class Dock_1(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
def sizeHint(self):
return QSize(.2*self.width(), .7*self.height())
class Dock_2(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
def sizeHint(self):
return QSize(.2*self.width(), .3*self.height())
class Dock_3(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
def sizeHint(self):
return QSize(.6*self.width(), .7*self.height())
class Dock_4(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
def sizeHint(self):
return QSize(.6*self.width(), .3*self.height())
class Dock_5(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
def sizeHint(self):
return QSize(.1*self.width(), self.height())
class Window(QMainWindow):
def __init__(self):
super().__init__()
self.setGeometry(200, 200, 800, 800)
self.UiComponents()
self.show()
def UiComponents(self):
dock1 = QDockWidget("Dock_1", self)
dock2 = QDockWidget("Dock_2", self)
dock3 = QDockWidget("Dock_3", self)
dock4 = QDockWidget("Dock_4", self)
dock5 = QDockWidget("Dock_5", self)
dock1.setAllowedAreas(Qt.LeftDockWidgetArea)
dock2.setAllowedAreas(Qt.BottomDockWidgetArea)
dock3.setAllowedAreas(Qt.TopDockWidgetArea)
dock4.setAllowedAreas(Qt.BottomDockWidgetArea)
dock5.setAllowedAreas(Qt.RightDockWidgetArea)
w_1 = Dock_1(self)
w_2 = Dock_2(self)
w_3 = Dock_3(self)
w_4 = Dock_4(self)
w_5 = Dock_5(self)
dock1.setWidget(w_1)
dock2.setWidget(w_2)
dock3.setWidget(w_3)
dock4.setWidget(w_4)
dock5.setWidget(w_5)
self.addDockWidget(Qt.LeftDockWidgetArea, dock1)
self.addDockWidget(Qt.LeftDockWidgetArea, dock2)
self.addDockWidget(Qt.RightDockWidgetArea, dock3)
self.addDockWidget(Qt.RightDockWidgetArea, dock4)
self.addDockWidget(Qt.RightDockWidgetArea, dock5)
App = QApplication(sys.argv)
window = Window()
sys.exit(App.exec())
Size hints are almost useless for dock widgets, since precedence is given to the central widget and the main window layout, but your approach is in any case wrong for two reasons:
the size hint is called before resizing, and returning a hint based on the current size wouldn't be valid since it could cause some sort of recursion (this doesn't normally happen as sizeHint() is generally called only when the layout structure is changed and then cached, but that's not the point);
when any widget is initialized it has a default size (100x30 if the parent is in the constructor, otherwise 640x480), so the results from your implementation would be invalid anyway;
Since what you want completely depends on the size of the main window, the only possibility is to resize the docks in the resizeEvent().
Also note that:
in order to have dock widgets side by side you must use splitDockWidget();
to have dock 3 and 4 vertically aligned with 1 and 2 (or 5) you can only put them in (and allow) the left or right dock area;
the allowed areas should match the area in which the dock is added;
creating the "inner" widget of a dock with the main window as a parent is pointless, since the dock will take ownership of that widget;
Considering the above, you should remove all sizeHint() overrides, set the proper allowed areas and then lay out the docks as required.
class Window(QMainWindow):
def __init__(self):
super().__init__()
self.setGeometry(200, 200, 800, 800)
self.UiComponents()
self.show()
def UiComponents(self):
dock1 = QDockWidget("Dock_1", self)
dock2 = QDockWidget("Dock_2", self)
dock3 = QDockWidget("Dock_3", self)
dock4 = QDockWidget("Dock_4", self)
dock5 = QDockWidget("Dock_5", self)
dock1.setAllowedAreas(Qt.LeftDockWidgetArea)
dock2.setAllowedAreas(Qt.LeftDockWidgetArea)
dock3.setAllowedAreas(Qt.RightDockWidgetArea)
dock4.setAllowedAreas(Qt.RightDockWidgetArea)
dock5.setAllowedAreas(Qt.RightDockWidgetArea)
w_1 = Dock_1()
w_2 = Dock_2()
w_3 = Dock_3()
w_4 = Dock_4()
w_5 = Dock_5()
dock1.setWidget(w_1)
dock2.setWidget(w_2)
dock3.setWidget(w_3)
dock4.setWidget(w_4)
dock5.setWidget(w_5)
self.addDockWidget(Qt.LeftDockWidgetArea, dock1)
self.addDockWidget(Qt.LeftDockWidgetArea, dock2)
self.addDockWidget(Qt.RightDockWidgetArea, dock3)
self.addDockWidget(Qt.RightDockWidgetArea, dock4)
self.addDockWidget(Qt.RightDockWidgetArea, dock5)
self.splitDockWidget(dock1, dock2, Qt.Vertical)
self.splitDockWidget(dock3, dock5, Qt.Horizontal)
self.splitDockWidget(dock3, dock4, Qt.Vertical)
self.docks = dock1, dock2, dock3, dock4, dock5
def resizeEvent(self, event):
super().resizeEvent(event)
side = self.width() // 5 # 2 / 10
center = side * 3 # 6 / 10
widths = side, side, center, center, side
self.resizeDocks(self.docks, widths, Qt.Horizontal)
vUnit = self.height() // 10
top = vUnit * 7
bottom = vUnit * 3
heights = top, bottom, top, bottom, top + bottom
self.resizeDocks(self.docks, heights, Qt.Vertical)

QTreeWidget and QPainter combination

I am a bit lost with the QTreeWidget and I were not able to suck the relevant information from the found topics (like: how to set QTreeView background image with QStyle::StandardPixmap in stylesheet method? or Python: PyQt QTreeview example - selection or Styling Qt QTreeView with CSS or Displaying tabular data in Qt5 ModelViews).
I have two files, one is the gui, one is the working class:
gui:
class Ui_Dialog(object):
def setupUi(self, Dialog):
Dialog.setObjectName("Dialog")
self.tree = QtWidgets.QTreeWidget(Dialog)
self.tree.setGeometry(QtCore.QRect(10, 60, 760, 480))
self.tree.setHeaderLabels(['circ', 'state', 'test'])
self.tree.setSortingEnabled(True)
worker:
class AppWindow(QDialog):
def __init__(self, fullscreen=False):
super().__init__()
self.ui = Ui_Dialog()
self.ui.setupUi(self)
self.show()
self.timer = QTimer()
self.timer.setInterval(500)
self.timer.timeout.connect(self.refresh_gui)
self.timer.start()
def refresh_gui(self):
self.painter = QPainter(self)
tmp = {0: {"state": 1, "info": "hello"}, 1: {"state": 0, "info": "world"}}
for i in tmp:
if tmp[i]["state"] == 0:
painter.setPen(QPen(Qt.red, 8, Qt.SolidLine))
else:
painter.setPen(QPen(Qt.green, 8, Qt.SolidLine))
circ = painter.drawEllipse(2,2,20,20)
item = QtWidgets.QTreeWidgetItem(self.ui.tree, [circ, tmp[i]["state"], "empty"])
item.setText(2, "circ painted")
I want to achive, that if state == 0 that a red circle is shown in the first column anf if state == 1 a green one. I do not know how to hand the QTreeWidgetItem a PyQt5.QtGui.QPainter object instead of a string.
Also, I do get the error:
QStandardPaths: XDG_RUNTIME_DIR not set, defaulting to '/tmp/runtime-root'
QWidget::paintEngine: Should no longer be called
QPainter::begin: Paint device returned engine == 0, type: 1
QPainter::setPen: Painter not active
QPainter::end: Painter not active, aborted
and some lines lower (because of it): QPainter::setPen: Painter not active
because I call self.painter = QPainter(self) which is discussed in this git issue from matplotlib but I fail to fix it in my code. I found this QPainter tutorial which paints on a QPixmap, which also works for me but is not what I am looking for here.
The painting of a widget is not done in any method, but the paintEvent method must be overridden, which is called whenever it is necessary by Qt or using update or repaint by the developer. But in the case of classes that handle a model (that is, a lot of organized information) that inherit from QAbstractItemView, a delegate must be used if an item is going to be painted.
Considering the above, the solution is:
class Delegate(QtWidgets.QStyledItemDelegate):
def paint(self, painter, option, index):
state = index.data(QtCore.Qt.UserRole)
color = (
QtGui.QColor(QtCore.Qt.red) if state == 0 else QtGui.QColor(QtCore.Qt.green)
)
painter.setPen(QtGui.QPen(color, 4, QtCore.Qt.SolidLine))
diameter = min(option.rect.width(), option.rect.height())
rect = QtCore.QRect(0, 0, diameter // 2, diameter // 2)
rect.moveCenter(option.rect.center())
painter.drawEllipse(rect)
class AppWindow(QtWidgets.QDialog):
def __init__(self, parent=None):
super().__init__(parent)
self.ui = Ui_Dialog()
self.ui.setupUi(self)
self.show()
delegate = Delegate(self.ui.tree)
self.ui.tree.setItemDelegateForColumn(0, delegate)
self.timer = QtCore.QTimer(interval=500, timeout=self.refresh_gui)
self.timer.start()
#QtCore.pyqtSlot()
def refresh_gui(self):
tmp = [{"state": 1, "info": "hello"}, {"state": 0, "info": "world"}]
for d in tmp:
item = QtWidgets.QTreeWidgetItem(self.ui.tree, ["", str(d["state"]), "empty"])
item.setData(0, QtCore.Qt.UserRole, d["state"])
item.setText(2, "circ painted")

How to Switch Between Panels whitch Buttons

I can not navigate between the panels of my App through the buttons. I followed a lot of tutorials like this one but none solve my problem.
Thank for Help.
####################-----Principal Panel (first Panel)
import wx
from OtherPanel import OtherPanel
class MyPanel(wx.Panel):
def __init__(self, parent):
"""Constructor"""
wx.Panel.__init__(self, parent = parent, size=(600, 500))
self.SetBackgroundColour(wx.Colour(250, 250, 250))
parent.Center()
self.histoPanel = OtherPanel(self)
self.home()
self.panel = wx.Panel(self)
def home(self):
self.panel = wx.Panel(self)
self.panel.SetBackgroundColour(wx.Colour(250, 250, 250))
bS =wx.Button(self, -1,"Go some where ",(200,30), size=(150,150))
self.Bind(wx.EVT_BUTTON, self.Sy, bS)
bOthe = wx.Button(self, -1,"Go to OtherPanel",(400,30),(150,150))
self.Bind(wx.EVT_BUTTON, self.onOpenFrame,bOthe)
def onOpenFrame(self, event):
"""
Opens secondary frame
"""
self.Hide()
self.histoPanel.Show()
def Sy(self, event):
"""
I have another panel (panel 3)
"""
self.Hide()
#self.panel3.Show()
--Second Panel (another Panel)
this panel is displayed when you click on the button of the first panel
import wx
class OtherPanel(wx.Panel):
def __init__(self, parent):
"""Constructor"""
wx.Panel.__init__(self, parent = parent)
self.panel = wx.Panel(self)
self.SetBackgroundColour(wx.Colour(250, 250, 250))
self.Center()
self.homeHist()
self.Show()
def homeHist(self):
bBack = wx.Button(self, -1, "", pos=(20,30), size=(50,50))
self.Bind(wx.EVT_BUTTON, self.onClose, bBack)
def onClose(self, event):
self.Close()
------My Frame---.
here is my Frame/Windows. he must wear all the panels in turn
import wx
from MyPanel import MyPanel
from OtherPanel import OtherPanel
class MyFrame(wx.Frame):
""""""
def __init__(self):
"""Constructor"""
wx.Frame.__init__(self, None, size = (600,500))
self.myPanel = MyPanel(self)
self.otherpanel = OtherPanel(self)
self.otherpanel.Hide()
self.menu()
self.CreateStatusBar(style= wx.BORDER_NONE)
self.SetBackgroundColour("gray")
self.SetStatusText("\tthank")
def menu(self):
menubar = wx.MenuBar()
fileMenu = wx.Menu()
menubar.SetBackgroundColour(wx.Colour(1, 1, 6))
fileMenu.AppendSeparator()
live = wx.MenuItem(fileMenu, wx.ID_EXIT, '&Quit\tCtrl+Q', '\tlive my app')
fileMenu.Append(live)
self.Bind(wx.EVT_MENU, self.Onexit, live)
menubar.Append(fileMenu, '\t&Menu')
self.SetMenuBar(menubar)
def Onexit(self, event):
self.Close(True)
def Bouton(self, event):
if self.myPanel.IsShown():
self.SetTitle("Panel Two Showing")
self.myPanel.Hide()
self.otherpanel.Show()
else:
self.SetTitle("Panel One Showing")
self.myPanel.Show()
self.otherpanel.Hide()
self.Layout()
if __name__ == "__main__":
app = wx.App(False)
frame = MyFrame()
frame.Show()
app.MainLoop()
In your MyPanel.__init__ you are creating an instance of OtherPanel assigned to self.histoPanel as a child of the MyPanel, and then MyPanel's event handlers are hiding itself, which in turn hides its children including the self.histoPanel. You're also creating some panels assigned to self.panel which don't seem to be used for anything.
The way you are doing it in the MyFrame class is much better. It creates both panels and hides one. Then in MyFrame.Bouton it hides one and shows the other, depending on the current state. So what you need to do is get rid of all the extra panels in MyPanel (self.histoPanel and also the various self.panels that are never used for anything) and then you just need to have the button event ask the parent frame to swap the panels instead of trying to do it itself.
Here is an example of what Robin is suggesting in his answer:
import wx
class MyPanel(wx.Panel):
def __init__(self, parent):
super().__init__(parent)
self.parent = parent
self.panel = wx.Panel(self)
self.btn = wx.Button(self.panel, label="Panel 1", size=(250,75))
self.btn.Bind(wx.EVT_BUTTON, self.switch)
vbox1 = wx.BoxSizer(wx.VERTICAL)
vbox1.Add(self.btn)
self.panel.SetSizer(vbox1)
vbox = wx.BoxSizer(wx.VERTICAL)
vbox.Add(self.panel)
self.SetSizer(vbox)
self.Show()
def switch(self, event):
self.parent.Swap()
class MyOtherPanel(wx.Panel):
def __init__(self, parent):
super().__init__(parent)
self.parent = parent
self.panel = wx.Panel(self)
self.btn = wx.Button(self.panel, label="Panel 2", size=(175,250))
self.btn.Bind(wx.EVT_BUTTON, self.switch)
vbox1 = wx.BoxSizer(wx.VERTICAL)
vbox1.Add(self.btn)
self.panel.SetSizer(vbox1)
vbox = wx.BoxSizer(wx.VERTICAL)
vbox.Add(self.panel)
self.SetSizer(vbox)
self.Show()
self.panel.Hide()
def switch(self, event):
self.parent.Swap()
class PanelSwitcher(wx.Frame):
def __init__(self):
super().__init__(None)
vbox = wx.BoxSizer(wx.VERTICAL)
self.panel1 = MyPanel(self)
self.panel2 = MyOtherPanel(self)
vbox.Add(self.panel1)
vbox.Add(self.panel2)
self.SetSizer(vbox)
self.Show()
def Swap(self):
if self.panel1.panel.IsShown():
self.panel1.panel.Hide()
self.panel2.panel.Show()
else:
self.panel2.panel.Hide()
self.panel1.panel.Show()
self.Layout()
if __name__ == "__main__":
app = wx.App()
PanelSwitcher()
app.MainLoop()

Open another wx.python frame cause the main UI to be blocked

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.

How Can I Safely Manage wxPython Progress Dialog Threading?

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.

Resources