viernes, 18 de noviembre de 2011

wxPython: cómo controlar los PyDeadObject

Hola. En este artículo vamos a ver una manera muy fácil de poder controlar los errores producidos en wxPython cuando se intenta acceder a widgets que han sido destruidos. Y vamos a verlo con el siguiente ejemplo:

# -*- coding: utf-8 -*-

import wx

class mi_frame(wx.Frame):
    def __init__(self, parent):
        wx.Frame.__init__(self, parent)
        self.SetTitle("Ejemplo de PyDeadObject")
        # 2 botones.
        self.boton1 = wx.Button(self, -1, u"Botón 1")
        self.boton2 = wx.Button(self, -1, u"Botón 2")
        sizer = wx.BoxSizer(wx.HORIZONTAL)
        sizer.Add(self.boton1, 0, wx.ALL, 1)
        sizer.Add(self.boton2, 0, wx.ALL, 1)
        self.SetSizer(sizer)
        # Binding.
        self.boton1.Bind(wx.EVT_BUTTON, self.OnBoton1)
        self.boton2.Bind(wx.EVT_BUTTON, self.OnBoton2)
       
    def OnBoton1(self, event):
        self.boton1.SetLabel("Hola Python")
       
    def OnBoton2(self, event):
        self.boton1.SetLabel("Hola wxPython")
       
app = wx.PySimpleApp()
f = mi_frame(None)
f.Show()
app.MainLoop()

Como se puede observar este script es una ventana que contiene dos botones. Al hacer click en cada uno de los botones se va cambiando el Label del primer botón. Esto es:


Hacemos click en el botón de la izquierda:


Hacemos click en el botón de la derecha:


Ahora vamos a hacer una pequeña modificación en el método OnBoton2: vamos a incluir una instrucción para que cuando se haga click en el botón 2 se elimine el botón 1:

    def OnBoton2(self, event):
        self.boton1.SetLabel("Hola wxPython")
        self.boton1.Destroy()


Si volvemos a hacer la misma operativa de arriba, cuando hagamos click en el boton 2 al intentar hacer el SetLabel nos dará un error PyDeadObject, ya que estamos intentando acceder a un widget que ya ha sido eliminado. Es decir:

Hacemos un primer click en el botón derecho:


Y al hacer un segundo click sobre el botón derecho nos da el siguiente error:

wx._core.PyDeadObjectError: The C++ part of the Button object has been deleted, attribute access no longer allowed.
File "c:\Users\Angel Luis\Desktop\ej_pydeadobject.py", line 30, in
  app.MainLoop()
File "c:\Python26\Lib\site-packages\wx-2.8-msw-unicode\wx\_core.py", line 8010, in MainLoop
  wx.PyApp.MainLoop(self)
File "c:\Python26\Lib\site-packages\wx-2.8-msw-unicode\wx\_core.py", line 7306, in MainLoop
  return _core_.PyApp_MainLoop(*args, **kwargs)
File "c:\Users\Angel Luis\Desktop\ej_pydeadobject.py", line 24, in OnBoton2
  self.boton1.SetLabel("Hola wxPython")

File "c:\Python26\Lib\site-packages\wx-2.8-msw-unicode\wx\_core.py", line 14610, in __getattr__
  raise PyDeadObjectError(self.attrStr % self._name)


Que corresponde con la línea wx _core.py: 





¿Solución?
 
En ciertas áreas de nuestro código wxPython que sepamos que son susceptibles de ser eliminadas y después con posibilidad de ser referenciadas de nuevo (por un proceso, por un hilo o por cualquier otra cosa), podemos evaluar primero el widget para ver si realmente existe y luego realizar las operaciones pertinentes. En nuestro ejemplo vamos a cambiar de nuevo el método OnBoton2:


# -*- coding: utf-8 -*-

import wx

class mi_frame(wx.Frame):
    def __init__(self, parent):
        wx.Frame.__init__(self, parent)
        self.SetTitle("Ejemplo de PyDeadObject")
        # 2 botones.
        self.boton1 = wx.Button(self, -1, u"Botón 1")
        self.boton2 = wx.Button(self, -1, u"Botón 2")
        sizer = wx.BoxSizer(wx.HORIZONTAL)
        sizer.Add(self.boton1, 0, wx.ALL, 1)
        sizer.Add(self.boton2, 0, wx.ALL, 1)
        self.SetSizer(sizer)
        # Binding.
        self.boton1.Bind(wx.EVT_BUTTON, self.OnBoton1)
        self.boton2.Bind(wx.EVT_BUTTON, self.OnBoton2)
       
    def OnBoton1(self, event):
        self.boton1.SetLabel("Hola Python")
       
    def OnBoton2(self, event):
       if not self.boton1:
            wx.MessageBox(u"El botón se destruyó")

       else:
            self.boton1.SetLabel("Hola wxPython")
            self.boton1.Destroy()
       
app = wx.PySimpleApp()
f = mi_frame(None)
f.Show()
app.MainLoop()

¿Por qué?

Pues porque cuando un widget se elimina pasa a ser un _wxPyDeadObject el cual gana un método __nonzero__ que siempre devuelve False.

Si volvemos a ejecutar nuestro nuevo script y hacemos dos veces click en el botón de la derecha tendremos el siguiente resultado:


Espero que este artículo os sirva para escribir aplicaciones wxPython más robustas.

Un saludo.

viernes, 4 de noviembre de 2011