14.4. Visualizadores de Celda (CellRenderer)

14.4.1. Introducción

Las columnas de vista de árbol (TreeViewColumn) y los Visualizadores o Intérpretes de Celda (CellRenderer) colaboran en la visualización de una columna de datos de una vista de árbol (TreeView). La clase TreeViewColumn proporciona el título de columna y un espacio vertical para que los CellRenderer muestren una porción de los datos de los que contiene el almacén del TreeView. Un CellRenderer maneja la visualización de los datos de cada fila y columna dentro de los confines de una TreeViewColumn. Una TreeViewColumn puede contener más de un CellRenderer para proporcionar una visualización de fila similar a la de una HBox. Un uso habitual con múltiples CellRenderer es la combinación de un Visualizador de Imágenes en Celda (CellRendererPixbuf) y un Visuallizador de Texto en Celda (CellRendererText) en la misma columna.

El ejemplo Figura 14.2, “TreeViewColumns con CellRenderers” muestra un ejemplo que ilustra la composición de dos TreeViewColumn, una con dos CellRenderer y la otra con uno sólo:

Figura 14.2. TreeViewColumns con CellRenderers

TreeViewColumns con CellRenderers

La aplicación de cada CellRenderer se indica mediante un color de fondo diferenciado: amarillo en el caso de un CellRendererPixbuf, cian para un CellRendererText, y rosa para el otro CellRendererText. Hay que resaltar que el CellRendererPixbuf y el primer CellRendererText están en la misma columna, encabezada con el texto "Pixbuf and Text". El color de fondo del CellRendererText que muestra "Print File" es el color predeterminado que muestra el área de la aplicación en una fila.

Figura 14.2, “TreeViewColumns con CellRenderers” se creó con el programa treeviewcolumn.py.

14.4.2. Tipos de Visualizadores CellRenderer

El tipo del CellRenderer que se necesita para cada caso viene determinado por el tipo de visualización requerida por los datos del modelo de árbol usado. PyGTK posee tres CellRenderer predefinidos:

CellRendererPixbuf

visualiza imágenes de píxeles (pixbuf) que pueden haber sido creadas por el programa, o tratarse de una imagen de serie predefinida.

CellRendererText

visualiza cadenas de texto, así como números que pueden ser convertidos en cadenas (enteros, reales y booleanos incluidos).

CellRendererToggle

visualiza un valor booleano como un botón biestado o como un botón de exclusión

14.4.3. Propiedade de un CellRenderer

Las propiedades de un CellRenderer determinan la forma en que se visualizarán los datos:

"mode"Lectura-EscrituraEl modo de edición del CellRenderer. Es uno de estos: gtk.CELL_RENDERER_MODE_INERT (inerte), gtk.CELL_RENDERER_MODE_ACTIVATABLE (activable) o gtk.CELL_RENDERER_MODE_EDITABLE (editable)
"visible"Lectura-EscrituraSi es TRUE se muestra la celda.
"xalign"Lectura-EscrituraLa fracción de espacio libre a la izquierda de la celda dentro del intervalo 0.0 a 1.0.
"yalign"Lectura-EscrituraLa fracción de espacio libre sobre la celda dentro del intervalo 0.0 a 1.0.
"xpad"Lectura-EscrituraLa cantidad de margen a la derecha e izquierda de la celda.
"ypad"Lectura-EscrituraLa cantidad de margen sobre y bajo la celda.
"width"Lectura-EscrituraLa anchura fija de la celda.
"height"Lectura-EscrituraLa altura fija de la celda.
"is-expander"Lectura-EscrituraSi es TRUE la fila tiene descendientes (hijas)
"is-expanded"Lectura-EscrituraSi es TRUE la fila tiene descendientes y se expande para mostrarlas.
"cell-background"EscrituraEl color de fondo de la celda indicada como cadena.
"cell-background-gdk"Lectura-EscrituraEl color de fondo indicado como gtk.gdk.Color.
"cell-background-set"Lectura-EscrituraSi es TRUE el color de fondo de la celda lo determina este visualizador

Las propiedades anteriores están disponibles para todas las subclases de CellRenderer. Pero los distintos tipos de CellRenderer también tienen propiedades exclusivas.

Los CellRendererPixbuf tienen estas propiedades:

"pixbuf"Lectura-EscrituraEl pixbuf que se visualizará (es anulada por "stock-id")
"pixbuf-expander-open"Lectura-EscrituraPixbuf para el expansor cuando está desplegado.
"pixbuf-expander-closed"Lectura-EscrituraPixbuf para el expansor cuando está cerrado.
"stock-id"Lectura-EscrituraEl stock ID del icono de serie que se visualizará.
"stock-size"Read-WriteEl tamaño del icono representado.
"stock-detail"Lectura-EscrituraDetalle de visualización que se proporcionará al motor de temas.

Los CellRendererText tienen un gran número de propiedades que tratan fundamentalmente con la especificación de estilos:

"text"Lectura-EscrituraTexto que se visualizará.
"markup"Lectura-EscrituraTexto con marcas que se visualizará.
"attributes"Lectura-EscrituraUna lista de atributos de estilo que se aplicarán al texto del visualizador.
"background"EscrituraColor de fondo, como cadena de texto.
"foreground"EscrituraColor de primer plano, como cadena de texto.
"background-gdk"Lectura-EscrituraColor de fondo, como gtk.gdk.Color
"foreground-gdk"Lectura-EscrituraColor de primer plano, como gtk.gdk.Color
"font"Lectura-EscrituraDescripción de la fuente, como cadena de texto.
"font-desc"Lectura-EscrituraDescripción de la fuente, como pango.FontDescription.
"family"Lectura-EscrituraNombre de la familia de la fuente, p.e. Sans, Helvetica, Times, Monospace.
"style"Lectura-EscrituraEstilo de fuente.
"variant"Lectura-EscrituraVariante de la fuente.
"weight"Lectura-EscrituraPeso de la fuente.
"stretch"Lectura-EscrituraEstirado de la fuente.
"size"Lectura-EscrituraTamaño de la fuente.
"size-points"Lectura-EscrituraTamaño de la fuente en puntos.
"scale"Lectura-EscrituraFactor de escala de la fuente.
"editable"Lectura-EscrituraSi es TRUE el texto puede ser cambiado por el usuario.
"strikethrough"Lectura-EscrituraSi es TRUE se tacha el texto
"underline"Lectura-EscrituraEstilo de subrayado del texto.
"rise"Lectura-EscrituraElevación del texto por encima de la línea base (o por debajo si el valor es negativo)
"language"Lectura-EscrituraEl idioma del texto, como código ISO. Pango puede utilizarlo como pista al representar el texto. Si no se entiende este parámetro... probablemente es que no se necesita. Solamente disponible a partir de GTK+ 2.4.
"single-paragraph-mode"Lectura-EscrituraSi es TRUE, se deja todo el texto en un único párrafo. Solamente disponible a partir de GTK+ 2.4.
"background-set"Lectura-EscrituraSi es TRUE se aplica el color de fondo.
"foreground-set"Lectura-EscrituraSi es TRUE se aplica el color de primer plano.
"family-set"Lectura-EscrituraSi es TRUE se aplica la familia de la fuente.
"style-set"Lectura-EscrituraSi es TRUE se aplica el estilo de la fuente.
"variant-set"Lectura-EscrituraSi es TRUE se aplica la variante de la fuente.
"weight-set"Lectura-EscrituraSi es TRUE se aplica el peso de la fuente.
"stretch-set"Lectura-EscrituraSi es TRUE se aplica el estirado de la fuente.
"size-set"Lectura-EscrituraSi es TRUE se aplica el tamaño de fuente.
"scale-set"Lectura-Escriturasi es TRUE se escala la fuente.
"editable-set"Lectura-EscrituraSi es TRUE se aplica la editabilidad del texto.
"strikethrough-set"Lectura-EscrituraSi es TRUE se aplica el tachado.
"underline-set"Lectura-EscrituraSi es TRUE se aplica el subrayado de texto.
"rise-set"Lectura-EscrituraSi es TRUE se aplica la elevación del texto.
"language-set"Lectura-EscrituraSi es TRUE se aplica el idioma usado para mostrar el texto. A partir de GTK+ 2.4.

Casi cada una de las propiedades de CellRendererText posee una propiedad booleana asociada (con el suffijo "-set") que indica si se aplica dicha propiedad. Esto permite fijar globalmente una propiedad y activar o desactivar su aplicación selectivamente.

Los CellRendererToggle poseen las siguientes propiedades:

"activatable"Lectura-EscrituraSi es TRUE, el botón biestado se puede activar.
"active"Lectura-EscrituraSi es TRUE, el botón está activo.
"radio"Lectura-EscrituraSi es TRUE, se dibuja el botón como un botón de exclusión.
"inconsistent"Lectura-EscrituraSi es TRUE, el botón está en un estado inconsistente. A partir de GTK+ 2.2.

Las propiedades se pueden fijar para todas las filas utilizando el método gobject.set_property(). Véase el programa treeviewcolumn.py como ejemplo del uso de este método.

14.4.4. Atributos de un CellRenderer

Un atributo asocia una columna de un modelo de árbol a una propiedad de un CellRenderer. El CellRenderer fija la propiedad en función del valor de una columna de la fila antes de representar la celda. Esto permite personalizar la visualización de la celda utilizando los datos del modelo de árbol. Se puede añadir un atributo al conjunto actual con:

  treeviewcolumn.add_attribute(cell_renderer, attribute, column)

donde la propiedad especificada por attribute se fija para el cell_renderer en la columna column. Por ejemplo:

  treeviewcolumn.add_attribute(cell, "cell-background", 1)

establece el fondo del CellRenderer al color indicado por la cadena de la segunda columna del almacén de datos.

Para eliminar todos los atributos y establecer varios atributos nuevos de una vez se usa:

  treeviewcolumn.set_attributes(cell_renderer, ...)

donde los atributos de cell_renderer se determinan mediante pares clave-valor: propiedad=columna. Por ejemplo, en el caso de un CellRendererText:

  treeviewcolumn.set_attributes(cell, text=0, cell_background=1, xpad=3)

indica, para cada fila, el texto en la primera columna, el color de fondo en la segunda y el margen horizontal desde la cuarta columna. Véase el programa treeviewcolumn.py para ver ejemplos del uso de estos métodos.

Los atributos de un CellRenderer se pueden limpiar utilizando:

  treeviewcolumn.clear_attributes(cell_renderer)

14.4.5. Función de Datos de Celda

Si no es suficiente el uso de atributos para cubrir nuestras necesidades, también es posible indicar una función que será llamada en cada fila y que determine las propiedades del CellRenderer utilizando:

  treeviewcolumn.set_cell_data_func(cell_renderer, func, data=None)

donde func tiene la signatura:

  def func(column, cell_renderer, tree_model, iter, user_data)

donde column es la TreeViewColumn que contiene el visualizador cell_renderer, tree_model es el almacén de datos e iter es un iterador TreeIter que apunta a una fila en tree_model. user_data es el valor de data que se pasó a set_cell_data_func().

En func se establecen las propiedades que se deseen para cell_renderer. Por ejemplo, el siguiente fragmento de código establece la propiedad de texto de manera que muestre los objetos de PyGTK como una cadena de identificación ID.

  ...
  def obj_id_str(treeviewcolumn, cell_renderer, model, iter):
      pyobj = model.get_value(iter, 0)
      cell.set_property('text', str(pyobj))
      return
  ...
  treestore = gtk.TreeStore(object)
  win = gtk.Window()
  treeview = gtk.TreeView(treestore)
  win.add(treeview)
  cell = CellRendererText()
  tvcolumn = gtk TreeViewColumn('Object ID', cell)
  treeview.append_column(tvcolumn)
  iter = treestore.append(None, [win])
  iter = treestore.append(iter, [treeview])
  iter = treestore.append(iter, [tvcolumn])
  iter = treestore.append(iter, [cell])
  iter = treestore.append(None, [treestore])
  ...

El resultado debería ser algo como Figura 14.3, “Función de Datos de Celda”:

Figura 14.3. Función de Datos de Celda

Función de Datos de Celda

Otro posible uso de la función de datos de celda es el control del formato de visualización de un texto numérico, p.e. un valor real. Un CellRendererText hará una conversión de forma automática del valor real a una cadena, pero con el formato predeterminado "%f".

Con funciones de datos de celda se pueden generar incluso los datos de las celdas a partir de datos externos. Por ejemplo, el programa filelisting.py usa un almacén ListStore con una única columna que contiene una lista de nombres de archivos. La TreeView muestra columnas que incluyen una imagen pixbuf, el nombre de archivo y su tamaño, modo y fecha del último cambio. Los datos son generados por las siguientes funciones de datos de celda:

    def file_pixbuf(self, column, cell, model, iter):
        filename = os.path.join(self.dirname, model.get_value(iter, 0))
        filestat = statcache.stat(filename)
        if stat.S_ISDIR(filestat.st_mode):
            pb = folderpb
        else:
            pb = filepb
        cell.set_property('pixbuf', pb)
        return

    def file_name(self, column, cell, model, iter):
        cell.set_property('text', model.get_value(iter, 0))
        return

    def file_size(self, column, cell, model, iter):
        filename = os.path.join(self.dirname, model.get_value(iter, 0))
        filestat = statcache.stat(filename)
        cell.set_property('text', filestat.st_size)
        return
 
    def file_mode(self, column, cell, model, iter):
        filename = os.path.join(self.dirname, model.get_value(iter, 0))
        filestat = statcache.stat(filename)
        cell.set_property('text', oct(stat.S_IMODE(filestat.st_mode)))
        return
 
 
    def file_last_changed(self, column, cell, model, iter):
        filename = os.path.join(self.dirname, model.get_value(iter, 0))
        filestat = statcache.stat(filename)
        cell.set_property('text', time.ctime(filestat.st_mtime))
        return

Estas funciones obtienen la información de los archivos utilizando el nombre, extraen los datos necesarios y establecen las propiedades de celda 'text' o 'pixbuf' con los datos. Figura 14.4, “Ejemplo de Listado de Archivos Utilizando Funciones de Datos de Celda” muestra el programa de ejemplo en acción:

Figura 14.4. Ejemplo de Listado de Archivos Utilizando Funciones de Datos de Celda

Ejemplo de Listado de Archivos Utilizando Funciones de Datos de Celda

14.4.6. Etiquetas de Marcado en CellRendererText

Un CellRendererText puede utilizar etiquetas de marcado de Pango (estableciendo la propiedad "markup") en vez de una cadena de texto sencilla para codificar diversos atributos de texto y proporcionar una visualización rica, con múltiples cambios de estilos de fuente. Véase la referencia Pango Markup en el Manual de Referencia de PyGTK para obtener más detalles sobre el lenguaje de marcado de Pango.

El siguiente fragmento de código ilustra el uso de la propiedad "markup":

  ...
  liststore = gtk.ListStore(str)
  cell = gtk.CellRendererText()
  tvcolumn = gtk.TreeViewColumn('Pango Markup', cell, markup=0)
  ...
  liststore.append(['<span foreground="blue"><b>Pango</b></span> markup can'
' change\n<i>style</i> <big>size</big>, <u>underline,'
 <s>strikethrough</s></u>,\n'
'and <span font_family="URW Chancery L"><big>font family '
'e.g. URW Chancery L</big></span>\n<span foreground="red">red'
' foreground and <span background="cyan">cyan background</span></span>'])
  ...

produce un resultado semejante a Figura 14.5, “Etiquetas de Marcado para CellRendererText”:

Figura 14.5. Etiquetas de Marcado para CellRendererText

Etiquetas de Marcado para CellRendererText

Si se crean etiquetas de marcado sobre la marcha es preciso tener cuidado y sustituir los caracteres con especial significado en el lenguaje de marcas: "<", ">", "&". La función de la biblioteca de Python cgi.escape() permite hacer estas conversiones básicas.

14.4.7. Celdas de Texto Editables

Las celdas CellRendererText pueden hacerse editalbes de forma que una usuaria pueda editar los contenidos de la celda que seleccione haciendo clic en ella o pulsando las teclas Return, Enter, Space o Shift+Space. Se hace editable un CellRendererText en todas sus filas estableciendo su propiedad "editable" a TRUE de la siguiente manera:

  cellrenderertext.set_property('editable', True)

Se pueden establecer individualmente celdas editables añadiendo un atributo a la TreeViewColumn utilizando un CellRendererText parecido a:

  treeviewcolumn.add_attribute(cellrenderertext, "editable", 2)

que establece que el valor de la propiedad "editable" se indica en la tercera columna del almacén de datos.

Una vez que la edición de la celda termina, la aplicación debe gestionar la señal "edited" para obtener el nuevo texto y establecer los datos asociados del almacén de datos. De otro modo, el valor de la celda recuperará su valor inicial. La signatura del manejador de llamada de la señal "edited" es:

  def edited_cb(cell, path, new_text, user_data)

donde cell es el CellRendererText, path es el camino de árbol (como cadena) a la fila que contiene la celda editada, new_text es el texto editado y user_data son datos de contexto. Puesto que se necesita el TreeModel para usar el camino path y establecer new_text en el almacén de datos, probablemente se quiera pasar el TreeModel como user_data en el método connect():

  cellrenderertext.connect('edited', edited_cb, model)

Si se tienen dos o más celdas editables en una fila, se podría pasar el número de columna del TreeModel como parte de los datos adicionales user_data así como el modelo TreeModel:

  cellrenderertext.connect('edited', edited_cb, (model, col_num))

Así, se puede establecer el nuevo texto en el manejador de la señal "edited" de una forma parecida al siguiente ejemplo que usa un almacén de lista ListStore:

  def edited_cb(cell, path, new_text, user_data):
      liststore, column = user_data
      liststore[path][column] = new_text
      return

14.4.8. Celdas Biestado Activables

Los botones de CellRendererToggle se pueden hacer activables estableciendo la propiedad "activatable" como TRUE. De forma parecida a la celdas editables de CellRendererText la propiedad "activatable" se puede fijar para un conjunto completo de celdas con CellRendererToggle utilizando el método set_property() o individualmente en algunas celdas añadiendo un atributo a la columna TreeViewColumn que contiene el CellRendererToggle.

  cellrenderertoggle.set_property('activatable', True)

  treeviewcolumn.add_attribute(cellrenderertoggle, "activatable", 1)

La creación de botones individuales biestado se puede deducir de los valores de una columna de un TreeModel añadiendo un atributo de manera similar a este ejemplo:

  treeviewcolumn.add_attribute(cellrenderertoggle, "active", 2)

Se debe conectar a la señal "toggled" para disponer de notificación de las pulsaciones del usuario en los botones biestado, de manera que la aplicación pueda modificar los valores del almacén de datos. Por ejemplo:

  cellrenderertoggle.connect("toggled", toggled_cb, (model, column))

La retrollamada tiene la signatura:

  def toggled_cb(cellrenderertoggle, path, user_data)

donde path es el camino de árbol, como cadena, que apunta a la fila que contiene el botón biestado que ha sido pulsado. Es recomendable pasar el TreeModel y, tal vez, el índice de la columna como parte de los datos de usuario user_data para proporcionar el contexto necesario para establecer los valores del almacén de datos. Por ejemplo, la aplicación puede conmutar los valores del almacén de datos, así:

  def toggled_cb(cell, path, user_data):
      model, column = user_data
      model[path][column] = not model[path][column]
      return

Si la aplicación desea mostrar los botones biestado como botones de exclusión y que únicamente uno de ellos esté activo, tendrá que recorrer los datos del almacén para desactivar el botón de exclusión activo y posteriormente activar el botón biestado. Por ejemplo:

  def toggled_cb(cell, path, user_data):
      model, column = user_data
      for row in model:
          row[column] = False
      model[path][column] = True
      return

usa la estrategia "vaga" de poner todos los valores a FALSE antes de fijar el valor TRUE en la fila especificada en path.

14.4.9. Programa de Ejemplo de Celda Editable and Activable

El programa cellrenderer.py ilustra la utilización de celdas CellRendererText editables y de celdas CellRendererToggle activables en un almacén TreeStore.

    1   #!/usr/bin/env python
    2   # vim: ts=4:sw=4:tw=78:nowrap
    3   """ Demonstration using editable and activatable CellRenderers """
    4   import pygtk
    5   pygtk.require("2.0")
    6   import gtk, gobject
    7
    8   tasks =  {
    9       "Buy groceries": "Go to Asda after work",
   10       "Do some programming": "Remember to update your software",
   11       "Power up systems": "Turn on the client but leave the server",
   12       "Watch some tv": "Remember to catch ER"
   13       }
   14
   15   class GUI_Controller:
   16       """ The GUI class is the controller for our application """
   17       def __init__(self):
   18           # establecer la ventana principal
   19           self.root = gtk.Window(type=gtk.WINDOW_TOPLEVEL)
   20           self.root.set_title("CellRenderer Example")
   21           self.root.connect("destroy", self.destroy_cb)
   22           # Obtener el modelo y vincularlo a la vista
   23           self.mdl = Store.get_model()
   24           self.view = Display.make_view( self.mdl )
   25           # Añadir la vista a la ventana principal
   26           self.root.add(self.view)
   27           self.root.show_all()
   28           return
   29       def destroy_cb(self, *kw):
   30           """ Destroy callback to shutdown the app """
   31           gtk.main_quit()
   32           return
   33       def run(self):
   34           """ run is called to set off the GTK mainloop """
   35           gtk.main()
   36           return
   37
   38   class InfoModel:
   39       """ The model class holds the information we want to display """
   40       def __init__(self):
   41           """ Sets up and populates our gtk.TreeStore """
   42           self.tree_store = gtk.TreeStore( gobject.TYPE_STRING,
   43                                            gobject.TYPE_BOOLEAN )
   44           # colocar los datos globales de la gente en la lista
   45           # formamos un árbol simple.
   46           for item in tasks.keys():
   47               parent = self.tree_store.append( None, (item, None) )
   48               self.tree_store.append( parent, (tasks[item],None) )
   49           return
   50       def get_model(self):
   51           """ Returns the model """
   52           if self.tree_store:
   53               return self.tree_store
   54           else:
   55               return None
   56
   57   class DisplayModel:
   58       """ Displays the Info_Model model in a view """
   59       def make_view( self, model ):
   60           """ Form a view for the Tree Model """
   61           self.view = gtk.TreeView( model )
   62           # configuramos el visualizador de celda de texto y permitimos
   63           # la edición de las celdas.
   64           self.renderer = gtk.CellRendererText()
   65           self.renderer.set_property( 'editable', True )
   66           self.renderer.connect( 'edited', self.col0_edited_cb, model )
   67
   68           # Se configura el visualizador de botones biestado y permitimos que se
   69           # pueda cambiar por el usuario.
   70           self.renderer1 = gtk.CellRendererToggle()
   71           self.renderer1.set_property('activatable', True)
   72           self.renderer1.connect( 'toggled', self.col1_toggled_cb, model )   73
   74           # Conectamos la columna 0 de la visualización con la columna o de nuestro modelo     
   75           # El visualizador mostrará lo que haya en la columna 0
   76           # de nuestro modelo.
   77           self.column0 = gtk.TreeViewColumn("Name", self.renderer, text=0)   78
   79           # El estado de activación del modelo se vincula a la segunda columna
   80           # del modelo. Así, cuando el modelo dice True entonces el botón
   81           # se mostrará activo, es decir, encendido.
   82           self.column1 = gtk.TreeViewColumn("Complete", self.renderer1 )
   83           self.column1.add_attribute( self.renderer1, "active", 1)
   84           self.view.append_column( self.column0 )
   85           self.view.append_column( self.column1 )
   86           return self.view
   87       def col0_edited_cb( self, cell, path, new_text, model ):
   88           """
   89           Called when a text cell is edited.  It puts the new text
   90           in the model so that it is displayed properly.
   91           """
   92           print "Change '%s' to '%s'" % (model[path][0], new_text)
   93           model[path][0] = new_text
   94           return
   95       def col1_toggled_cb( self, cell, path, model ):
   96           """
   97           Sets the toggled state on the toggle button to true or false.
   98           """
   99           model[path][1] = not model[path][1]
  100           print "Toggle '%s' to: %s" % (model[path][0], model[path][1],)
  101           return
  102
  103   if __name__ == '__main__':
  104       Store = InfoModel()
  105       Display = DisplayModel()
  106       myGUI = GUI_Controller()
  107       myGUI.run()

El programa proporcional celdas editables en la primera columna y celdas activables en la segunda columna. Las líneas 64-66 crean un CellRendererText editable y conectan la señal "edited" a la retrollamada col0_edited_cb() (líneas 87-94), que cambia el valor en la columna correspondiente de la fila en el almacén de árbol TreeStore. De la misma manera, las líneas 70-72 crean un CellRendererToggle activable y conectan la señal "toggled" a la retrollamada col1_toggled_cb() (líneas 95-101) para cambiar el valor de la fila correspondiente. Cuando se modifica una celada editable o activable se muestra un mensaje para indicar cuál ha sido el cambio.

Figura 14.6, “Celdas Editables y Activables” ilustra el programa cellrenderer.py en ejecución.

Figura 14.6. Celdas Editables y Activables

Celdas Editables y Activables