Is use of "startWorker" to make a non-GUI-blocking file dialog possible ?

44 views
Skip to first unread message

Michael Freeman

unread,
Sep 12, 2016, 2:14:49 PM9/12/16
to wxPython-users
Hi,

I'm using this code to call wxPython Phoenix from within Blender. I need to call up a file open dialog but keep Blender responsive. The entire Blender script is at the end of the message. The actual file load function is triggered by a key press from Blender Game Engine (full project here: https://blenderartists.org/forum/showthread.php?404099-DJ-turntable-platter-physics-simulation-Link-sound-pitch-to-platter-rotation-speed ).

-----------

class MainWindow(wx.Frame):
    def __init__(self, parent):
        wx.Timer.__init__(self)
        startWorker(self.openFileConsumer, self.openFileWorker)   
   
    def openFileWorker (self):
        print ("openFileWorker started")
        openFileDialog = wx.FileDialog(None, "Open XYZ file", "", "", "XYZ files (*.xyz)|*.xyz", wx.FD_OPEN | wx.FD_FILE_MUST_EXIST)
        if openFileDialog.ShowModal() == wx.ID_CANCEL:
            return     # the user changed idea...
        # proceed loading the file chosen by the user
        # this can be done with e.g. wxPython input streams:
        input_stream = wx.FileInputStream(openFileDialog.GetPath())
        if not input_stream.IsOk():
            wx.LogError("Cannot open file '%s'."%openFileDialog.GetPath())
        return

    def openFileConsumer (self):
        print ("openFilecomsumerStarted")
        print (path)

def openFile (cont):
    print ("File load triggerred")           
    app = wx.App(False)
    win = MainWindow(None)
    app.MainLoop()

------------

Both "openFileWorker" and "openFile" are entered because the print message shows in my console. But after some standard GTK errors on my machine like "(blender:6455): Gtk-WARNING **: Error loading theme icon 'edit-find' for stock: Fatal error reading PNG image file: Invalid IHDR data" (I get that when things are working normally) there's a Segmentation Fault.


------------

# Blender 2.77 (sub 3), Commit date: 2016-08-27 22:07, Hash a5261e0
bpy.ops.transform.translate(value=(0, 0, 0), constraint_axis=(False, False, False), constraint_orientation='GLOBAL', mirror=False, proportional='ENABLED', proportional_edit_falloff='SMOOTH', proportional_size=1, release_confirm=True)  # Operator

# backtrace
./blender(BLI_system_backtrace+0x1d) [0x1984c7d]
./blender() [0x1030939]
/lib/x86_64-linux-gnu/libc.so.6(+0x354a0) [0x7fe6f30374a0]
/home/michaelzfreeman/Downloads/blender-2.77-a5261e0-linux-glibc219-x86_64/2.77/python/lib/python3.5/site-packages/wx/libwx_gtk2u_core-3.0.so.0(_ZNK12wxDialogBase23GetParentForModalDialogEP8wxWindowl+0x57) [0x7fe6cf3f81e7]
/home/michaelzfreeman/Downloads/blender-2.77-a5261e0-linux-glibc219-x86_64/2.77/python/lib/python3.5/site-packages/wx/libwx_gtk2u_core-3.0.so.0(_ZN8wxDialog9ShowModalEv+0x9b) [0x7fe6cf35d2ab]
/home/michaelzfreeman/Downloads/blender-2.77-a5261e0-linux-glibc219-x86_64/2.77/python/lib/python3.5/site-packages/wx/libwx_gtk2u_core-3.0.so.0(_ZN12wxFileDialog9ShowModalEv+0x30) [0x7fe6cf3628d0]
/home/michaelzfreeman/Downloads/blender-2.77-a5261e0-linux-glibc219-x86_64/2.77/python/lib/python3.5/site-packages/wx/_core.cpython-35m-x86_64-linux-gnu.so(+0x48d119) [0x7fe6cfd85119]
./blender(PyCFunction_Call+0xb9) [0x296e189]
./blender(PyEval_EvalFrameEx+0x8766) [0x29e7536]
./blender() [0x29e860e]
./blender(PyEval_EvalCodeEx+0x23) [0x29e86e3]
./blender() [0x294a856]
./blender(PyObject_Call+0x5c) [0x291ffac]
./blender(PyEval_EvalFrameEx+0x1010) [0x29dfde0]
./blender() [0x29e860e]
./blender(PyEval_EvalCodeEx+0x23) [0x29e86e3]
./blender() [0x294a856]
./blender(PyObject_Call+0x5c) [0x291ffac]
./blender(PyEval_EvalFrameEx+0x1010) [0x29dfde0]
./blender(PyEval_EvalFrameEx+0x754c) [0x29e631c]
./blender(PyEval_EvalFrameEx+0x754c) [0x29e631c]
./blender() [0x29e860e]
./blender(PyEval_EvalCodeEx+0x23) [0x29e86e3]
./blender() [0x294a784]
./blender(PyObject_Call+0x5c) [0x291ffac]
./blender() [0x2939e84]
./blender(PyObject_Call+0x5c) [0x291ffac]
./blender(PyEval_CallObjectWithKeywords+0x47) [0x29dece7]
./blender() [0x2a350e2]
/lib/x86_64-linux-gnu/libpthread.so.0(+0x76fa) [0x7fe6f46136fa]
/lib/x86_64-linux-gnu/libc.so.6(clone+0x6d) [0x7fe6f3108b5d]

-------------------

I must admit I'm a bit new to wxPython and Python in general, but at least as far as I can see, it should work. However I have studied https://wiki.wxpython.org/LongRunningTasks and https://wiki.wxpython.org/Non-Blocking%20Gui as well as http://stackoverflow.com/questions/7666368/how-to-implement-a-thread-in-a-wxpython-gui-application ...

... and it suggests that I can't create any wx GUI item in another thread only in the main thread, hence the crash ?

--------------------

# DJ Turntable Physics Simulation, Michael Z Freeman, 2016

# See CHANGELOG.TXT, TODO.TXT & README.TXT in text data blog for changelog.

# Better name "Blender Game Engine DJ Turntable" ?

# This is the main Python file for the project. I decided not to pile everything into a single monolithic Python script without logic blocks. I found the logic blocks helped me think through the problems and keep things fairly simple. I also think the logic blocks make it easier for new comers to understand what is going on. So the script is arranged into "modules" that are referenced in the logic blocks. There is only one at the moment but that will probably change when, for example, file loading is added. Controls are only through the keyboard at the moment and will probably change:

#MOUSE CLICK LEFT/RIGHT: nudge/spin platter left/right.
# SPACE: start/stop.
# ALT: Deck on/off

from bge import logic
import bge
import aud
import bpy
import wave
import contextlib
import wx
from wx.lib.delayedresult import startWorker

backwards_position = None
forwards_position = None
trap = 0
track_path1 = "/home/michaelzfreeman/DJ_Barney_Docs/Docs/Forge/Music/Beat-Research-Forge/DJ Turntable Platter Phyics Simulation/SEEDS_MASTER_3.wav"
track_path2 = "/home/michaelzfreeman/DJ_Barney_Docs/Docs/Forge/Music/Beat-Research-Forge/DJ Turntable Platter Phyics Simulation/Party For Your Right To Fight -- Public Enemy -- It Takes a Nation of Millions to Hold us Back.wav"
device = aud.device()
track = aud.Factory.file(track_path2)
# Find length of track
with contextlib.closing(wave.open(track_path2,'r')) as f:
    frames = f.getnframes()
    rate = f.getframerate()
    track_duration = frames / float(rate)
    print("Duration: ")
    print(track_duration)
# Setup handle to playing track.
# MUST be buffered for reverse. Where is this in documentation ?
track_buffered_reverse = track.buffer()
track_buffered_forward = track.buffer()
track_reverse = track_buffered_reverse.reverse()
track_forward = track_buffered_forward
#track_handle = device.play(track_reverse)
#track_handle.pitch = 1
# make sure its stopped because we have not moved the platter yet.
#track_handle.stop()

#def playTrack (cont):
#global track
#global track_handle
#global track_buffered
#global track_reverse
#global track_handle_forwards
#global track_handle_backwards
track_handle_forwards = device.play(track_forward)
track_handle_forwards.keep = True
track_handle_forwards.pause()
track_handle_forwards.loop_count = 0
track_handle_backwards = device.play(track_reverse)
track_handle_backwards.keep = True
track_handle_backwards.pause()
#reverse_handle.reverse
#track_handle.pitch = 1
#speed = setPitch
#print("Pitch: ")
#print(speed)
#track = track.pitch(speed)
 
def setPitch (cont):
    global track
    global track_handle
    global track_buffered
    global track_reverse
    global track_forward
    global duration
    global trap
   
    #global backwards_position
    #global forwards_position
    #forwards_position = track_handle_forwards.position   
    #backwards_position = track_handle_backwards.position
    # Get object
    platter = bge.logic.getCurrentScene().objects['Platter']
    rot_speed = platter.worldAngularVelocity.z
    #print("Rotation speed: ")
    #print(rot_speed);
    speed = rot_speed * -1
    #print("Pitch: ")
    #print(speed)
    if speed >= 0:
        track_handle_backwards.pause()
        forwards_position = track_handle_forwards.position
        backwards_position = track_handle_backwards.position
        #backwards_position = track_handle_backwards.position
        #track_handle_forwards.position = forwards_position
        while (trap == 1):
            print("Forward trap triggered")
            track_handle_forwards.position = track_duration - backwards_position
            trap = 0    
        track_handle_forwards.resume()
        # playing/paused/stop status only ever shows as true ?
        #print("track_handle_backwards.status: ")
        #print(track_handle_backwards.status)
        track_handle_forwards.pitch = speed
        #print("track_handle_forwards.pitch: ")
        #print(track_handle_forwards.pitch)
        print("track_handle_forwards.position: ")
        print(track_handle_forwards.position)
    else:
        track_handle_forwards.pause()
        forwards_position = track_handle_forwards.position
        backwards_position = track_handle_backwards.position
        # Only set once until direction changes
        while (trap == 0):
            print("Backwards trap triggered")
            #print("Reverse Set: ")
            #print(backwards_position - track_duration + forwards_position)
            track_handle_backwards.position = track_duration - forwards_position
            trap = 1
        #forwards_position = track_handle_forwards.position
        #track_handle_backwards.position = backwards_position
        track_handle_backwards.resume()
        track_handle_backwards.pitch = speed * -1
        #print("track_handle_backwards.pitch: ")
        #print(track_handle_backwards.pitch)
        print("track_handle_backwards.position: ")
        print(track_handle_backwards.position)  
    #track = track.pitch(speed)
    # rot_speed * -1
    #track = track.pitch(rot_speed)   
    # Old code for Actuator
    #sound.pitch = rot_speed * -1
    #print("Pitch: ")
    #print(track.pitch)
    #return rot_speed;
   
class MainWindow(wx.Frame):
    def __init__(self, parent):
        wx.Timer.__init__(self)
        startWorker(self.openFileConsumer, self.openFileWorker)   
   
    def openFileWorker (self):
        print ("openFileWorker started")
        openFileDialog = wx.FileDialog(None, "Open XYZ file", "", "", "XYZ files (*.xyz)|*.xyz", wx.FD_OPEN | wx.FD_FILE_MUST_EXIST)
        #if openFileDialog.ShowModal() == wx.ID_CANCEL:
        #    return     # the user changed idea...
        # proceed loading the file chosen by the user
        # this can be done with e.g. wxPython input streams:
        #input_stream = wx.FileInputStream(openFileDialog.GetPath())
        #if not input_stream.IsOk():
        #    wx.LogError("Cannot open file '%s'."%openFileDialog.GetPath())
        #return

    def openFileConsumer (self):
        print ("openFilecomsumerStarted")
        print (path)

def openFile (cont):
    print ("File load triggerred")           
    app = wx.App(False)
    win = MainWindow(None)
    app.MainLoop()        

Michael Freeman

unread,
Sep 12, 2016, 2:17:51 PM9/12/16
to wxPython-users
Apologies, did not see message about attaching code. Attached.
startWorker_message.txt

Robin Dunn

unread,
Oct 4, 2016, 6:52:16 PM10/4/16
to wxpytho...@googlegroups.com
Michael Freeman wrote:
Apologies, did not see message about attaching code. Attached.


This is at least part of the problem:


class MainWindow(wx.Frame):
    def __init__(self, parent):
        wx.Timer.__init__(self)
        startWorker(self.openFileConsumer, self.openFileWorker)   

You are not creating a frame, you are creating a timer in place of a class that thinks it is a frame. In the backtrace it shows that GetParentForModalDialog is on the stack so it is looking for a top-level window to use as a parent for the dialog.  The rest of the problem is probably due to trying to create and use the dialog from the thread started by startWorker as you've guessed.  The thread that creates the wx.App is the main UI thread[1] and *everything* UI-related except for a very small set of exceptions *must* be done in the context of that thread.


[1] It doesn't affect your case, but for the record there are some implementation details on OSX that require that the main UI thread also the the process's main thread too, so you can't do tricky things like make a thread and have it be the one that creates the wx.App.

--
Robin Dunn
Software Craftsman
http://wxPython.org
Reply all
Reply to author
Forward
0 new messages