Un mini editor de texto con wxPython

En esta entrada vamos a explicar cómo desarrollar un editor de texto muy sencillo, que cumpla con algunas funciones muy básicas, tal como un bloc de notas de Windows.

El resultado final será más o menos el siguiente:


Primeramente vamos a importar los módulos a utilizar:

import wx
import os
import os.path

El módulo wx para la librería gráfica (wxPython), y el módulo os para las operaciones con archivos de texto plano (guardar, abrir, etc...).

Una vez importados los módulos necesarios, habremos de definir una estructura base para la aplicación. Para ello extenderemos una clase de wx.Frame, tal cómo se muestra enseguida:

class LABTxt(wx.Frame):
def __init__(self,parent,title):
wx.Frame.__init__(self,parent,title=title,size=(600,400))

def configurarEditor(self):
""" Configura las características iniciales del editor """

def crearMenu(self):
""" Crea la barra de menú """

def abrirArchivo(self, event):
""" Abre un archivo de texto plano"""

def guardarArchivoComo(self, event):
""" Guarda el archivo actual abriendo un cuadro de dialogo """

def guardarArchivo(self,event):
""" Guarda el archivo actual """

def copiar(self,event):
""" Copia el texto seleccionado al portapapeles """

def pegar(self,event):
""" Pega el texto ubicado en el portapapeles """

def configurarTema(self,event):
""" Configura el tema a utilizar """

def ayuda(self,event):
""" Muestra la ayuda de la aplicacion """

def acerca(self, event):
""" Breve descripción del programa """

if __name__=='__main__':
app = wx.App()
fr = LABTxt(None, "LABTxt 0.0.1")
app.MainLoop()

En lo anterior se define una clase LABTxt derivada de wx.Frame, con ciertos métodos definidos que posteriormente desarrollaremos y que, evidentemente, le dan funcionalidad a la aplicación.

El método __init__
En el método __init__ (comúnmente nombrado "constructor" de la clase) se colocarán los elementos básicos de la aplicación, en este caso un wx.TextCtrl y el Sizer correspondiente, tal como se muestra enseguida:

def __init__(self,parent,title):
wx.Frame.__init__(self,parent,title=title,size=(600,400))
if os.path.isfile("icono.png"):
self.SetIcon(wx.Icon('icono.png'))
self.archivo='untitled.txt'
p=wx.Panel(self, -1)

# Sizer
sz=wx.BoxSizer(wx.VERTICAL)

# Editor
self.editor=wx.TextCtrl(p, -1, "", style=wx.TE_MULTILINE)
self.configurarEditor()

# Agregar al sizer
sz.Add(self.editor, 1, wx.EXPAND)
p.SetSizer(sz)

# Crear barra de menu
self.crearMenu()
self.Show()

Colocamos un ícono a la aplicación (en el caso de que este exista), se crea un panel sobre el cual se agregará el control de texto. Enseguida se agrega un wx.TextCtrl con la propiedad style definida como wx.TE_MULTILINE, que permitirá tener un campo de texto multilínea, simulando de esta manera el editor que necesitamos. Se "llama" al método configurarEditor que simplemente configura la fuente y color de fondo del mismo. Finalmente se crea la barra de menús y se muestra la ventana con el método Show.


El método configurarEditor
Este método define las características de la fuente y el color de fondo a utilizar.


El método crearMenu
Aquí se crea la barra de menús con sus respectivos ítems y se agrega la funcionalidad (eventos) a cada uno de ellos, mediante el uso del método Bind de la clase wx.Frame.

def crearMenu(self):  
""" Crea la barra de menú """
marchivo=wx.Menu()
abrir=marchivo.Append(-1, "Abrir\tCtrl-O")
guardar=marchivo.Append(-1, "Guardar\tCtrl-S")
guardarComo=marchivo.Append(-1, "Guardar como")

meditar=wx.Menu()
copiar=meditar.Append(-1, "Copiar\tCtrl-C")
pegar=meditar.Append(-1, "Pegar\tCtrl-V")

self.mtema=wx.Menu()
classic=self.mtema.Append(-1, "Classic")
dark=self.mtema.Append(-1, "Dark")
retro=self.mtema.Append(-1, "Retro")
pink=self.mtema.Append(-1, "Pink")

mayuda=wx.Menu()
ayuda=mayuda.Append(-1, "Ayuda")
acerca=mayuda.Append(-1, "Acerca de...")

barraMenu=wx.MenuBar()
barraMenu.Append(marchivo, "Archivo")
barraMenu.Append(meditar, "Editar")
barraMenu.Append(self.mtema, "Seleccionar tema")
barraMenu.Append(mayuda, "Ayuda")
self.SetMenuBar(barraMenu)

# Definición de "eventos"
self.Bind(wx.EVT_MENU, self.abrirArchivo, abrir)
self.Bind(wx.EVT_MENU, self.guardarArchivoComo, guardarComo)
self.Bind(wx.EVT_MENU, self.guardarArchivo, guardar)

self.Bind(wx.EVT_MENU, self.copiar, copiar)
self.Bind(wx.EVT_MENU, self.pegar, pegar)

self.Bind(wx.EVT_MENU, self.configurarTema, classic)
self.Bind(wx.EVT_MENU, self.configurarTema, dark)
self.Bind(wx.EVT_MENU, self.configurarTema, retro)
self.Bind(wx.EVT_MENU, self.configurarTema, pink)

self.Bind(wx.EVT_MENU, self.acerca, acerca)
self.Bind(wx.EVT_MENU, self.ayuda, ayuda)

Notará que cada ítem de los menús se "conecta" a un método de la propia clase que define la acción que se ejecutará en cada caso.


El editor...
Finalmente os dejo el código completo del editor. Desde luego existen muchas mejoras que pueden hacerse.

# -*- coding: utf-8 -*-
# ================================
# Por: Jorge De Los Santos
# E-mail: delossantosmfq@gmail.com
# Licencia: BSD License
# ================================

import wx
import os
import os.path

class LABTxt(wx.Frame):
def __init__(self,parent,title):
wx.Frame.__init__(self,parent,title=title,size=(600,400))
if os.path.isfile("icono.png"):
self.SetIcon(wx.Icon('icono.png'))
self.archivo='untitled.txt'
p=wx.Panel(self, -1)

# Sizer
sz=wx.BoxSizer(wx.VERTICAL)

# Editor
self.editor=wx.TextCtrl(p, -1, "", style=wx.TE_MULTILINE)
self.configurarEditor()

# Agregar al sizer
sz.Add(self.editor, 1, wx.EXPAND)
p.SetSizer(sz)

# Crear barra de menu
self.crearMenu()
self.Show()

def configurarEditor(self):
""" Configura las características iniciales del editor """
self.fuente=wx.Font(12, wx.MODERN, wx.NORMAL, wx.NORMAL)
self.fuente.SetFaceName("Courier New")
self.editor.SetFont(self.fuente)
self.editor.SetBackgroundStyle(True)

def crearMenu(self):
""" Crea la barra de menú """
marchivo=wx.Menu()
abrir=marchivo.Append(-1, "Abrir\tCtrl-O")
guardar=marchivo.Append(-1, "Guardar\tCtrl-S")
guardarComo=marchivo.Append(-1, "Guardar como")

meditar=wx.Menu()
copiar=meditar.Append(-1, "Copiar\tCtrl-C")
pegar=meditar.Append(-1, "Pegar\tCtrl-V")

self.mtema=wx.Menu()
classic=self.mtema.Append(-1, "Classic")
dark=self.mtema.Append(-1, "Dark")
retro=self.mtema.Append(-1, "Retro")
pink=self.mtema.Append(-1, "Pink")

mayuda=wx.Menu()
ayuda=mayuda.Append(-1, "Ayuda")
acerca=mayuda.Append(-1, "Acerca de...")

barraMenu=wx.MenuBar()
barraMenu.Append(marchivo, "Archivo")
barraMenu.Append(meditar, "Editar")
barraMenu.Append(self.mtema, "Seleccionar tema")
barraMenu.Append(mayuda, "Ayuda")
self.SetMenuBar(barraMenu)

# Definición de "eventos"
self.Bind(wx.EVT_MENU, self.abrirArchivo, abrir)
self.Bind(wx.EVT_MENU, self.guardarArchivoComo, guardarComo)
self.Bind(wx.EVT_MENU, self.guardarArchivo, guardar)

self.Bind(wx.EVT_MENU, self.copiar, copiar)
self.Bind(wx.EVT_MENU, self.pegar, pegar)

self.Bind(wx.EVT_MENU, self.configurarTema, classic)
self.Bind(wx.EVT_MENU, self.configurarTema, dark)
self.Bind(wx.EVT_MENU, self.configurarTema, retro)
self.Bind(wx.EVT_MENU, self.configurarTema, pink)

self.Bind(wx.EVT_MENU, self.acerca, acerca)
self.Bind(wx.EVT_MENU, self.ayuda, ayuda)

def abrirArchivo(self, event):
dlg=wx.FileDialog(self, "Abrir archivo", os.getcwd(), style=wx.OPEN)
if dlg.ShowModal() == wx.ID_OK:
try:
fid=open(dlg.GetPath(),'r')
texto=fid.readlines()
self.texto="".join(texto)
self.texto = self.texto.decode("utf8")
fid.close()
self.editor.SetValue(self.texto)
self.archivo=dlg.GetPath()
self.SetTitle("LABTxt "+self.archivo)
except:
wx.MessageBox(u"Archivo no válido","Error")
dlg.Destroy()

def guardarArchivoComo(self, event):
""" Guarda el archivo actual abriendo un cuadro de dialogo """
dlg=wx.FileDialog(self, "Guardar", os.getcwd(), style=wx.SAVE)
if dlg.ShowModal() == wx.ID_OK:
fid=open(dlg.GetPath(),'w')
txt=str(self.editor.GetValue().encode('utf8'))
fid.write(txt)
fid.close()
self.archivo=dlg.GetPath()
self.SetTitle("LABTxt 0.0.1 "+self.archivo)
dlg.Destroy()

def guardarArchivo(self,event):
""" Guarda el archivo actual """
if hasattr(self, 'archivo'):
fid=open(self.archivo,'w')
txt=str(self.editor.GetValue().encode('utf8'))
fid.write(txt)
fid.close()
wx.MessageBox("Hecho","LABTxt")
self.SetTitle("LABTxt 0.0.1 "+self.archivo)
else:
self.guardarArchivoComo(None)

def copiar(self,event):
""" Copia el texto seleccionado al portapapeles """
texto=wx.TextDataObject(self.editor.GetStringSelection())
if wx.TheClipboard.Open():
wx.TheClipboard.SetData(texto)
wx.TheClipboard.Close()

def pegar(self,event):
""" Pega el texto ubicado en el portapapeles """
txt=wx.TextDataObject()
if wx.TheClipboard.Open():
success=wx.TheClipboard.GetData(txt)
wx.TheClipboard.Close()
if success:
self.editor.SetInsertionPoint(self.editor.GetInsertionPoint())
self.editor.write(txt.GetText())

def configurarTema(self,event):
tema_sel=self.mtema.FindItemById(event.GetId()).GetText()
temas={'Classic':((0,0,255),(255,255,255)),
'Dark':((200,200,200),(0,0,0)),
'Retro':((0,255,0),(0,0,0)),
'Pink':((20,50,50),(250,180,180))}
self.editor.SetForegroundColour(temas[tema_sel][0])
self.editor.SetBackgroundColour(temas[tema_sel][1])
self.editor.Refresh()

def ayuda(self,event):
wx.MessageBox("No disponible","LABTxt")

def acerca(self, event):
descripcion=""" Editor de texto sin formato desarrollado en
wxPython """
info=wx.AboutDialogInfo()
info.SetName('LABTxt')
info.SetDescription(descripcion)
info.SetVersion('0.0.1')
info.SetLicense('BSD License')
info.SetDevelopers(['Jorge De Los Santos'])
info.SetWebSite(('labdls.blogspot.mx','LAB DLS'))
info.SetCopyright('(c) 2014')
wx.AboutBox(info)

if __name__=='__main__':
app = wx.App()
fr = LABTxt(None, "LABTxt 0.0.1")
app.MainLoop()



Primer aplicación en wxPython

wxPython es un binding de la biblioteca gráfica wxWidgets para el lenguaje de programación Python. La biblioteca wxWidgets se caracteriza por ser multiplataforma, por lo que su uso junto a Python permite el desarrollo rápido de aplicaciones gráficas multiplataforma.

Para desarrollar una aplicación en wxPython, normalmente primero debe crearse una clase heredada de wx.Frame:

import wx

class MiAplicacion(wx.Frame):
def __init__(self,parent,title):
wx.Frame.__init__(self,parent,title=title,size=(600,400))

En el código anterior primero se importa el módulo wx, enseguida se define una clase llamada MiAplicacion, la cual hereda de wx.Frame. El método __init__ de la clase creada debe contener al menos dos argumentos de entrada, self y parent, donde self es una cadena utilizada por convención para referenciar a un objeto de la propia clase y parent es el objeto gráfico padre del Frame que se creará cuando instanciemos un objeto de esta clase. El otro argumento definido, title, será una cadena que se mostrará en la parte superior de la ventana. El método __init__ podría considerarse como el "constructor" de la clase, cuando instanciemos un objeto de esa clase, se pasarán como argumentos de entrada los parámetros definidos en __init__, exceptuando self.

Una vez definida la clase, ahora vamos a instanciar un objeto de esa clase como sigue:

if __name__=='__main__':
app = wx.App()
frame = MiAplicacion(None, u"Mi aplicación")
frame.Show()
app.MainLoop()

Primero se crea un objeto de la clase wx.App, el cuál se encargará de "lanzar" la aplicación y ejecutar las órdenes necesarias para poder interactuar con la interfaz gráfica. Luego, se define un objeto frame de la clase MiAplicación, teniendo como primer argumento None, indicando que no tendrá un objeto gráfico padre, como segundo argumento se pasa una cadena de texto con el título que queremos colocar en la parte superior de la ventana. Finalmente, el método MainLoop de la clase wx.App inicia la aplicación wxPython. En la siguiente figura se muestra la ventana resultante.



Es recomendable que el método Show sea implementado dentro del método __init__ de la clase derivada de wx.Frame, además podemos centrar la interfaz gráfica en la pantalla para obtener una mejor visualización, quedando nuestro código como sigue:

# -*- coding: utf8 -*-
import wx

class MiAplicacion(wx.Frame):
def __init__(self,parent,title):
wx.Frame.__init__(self,parent,title=title,size=(600,400))
self.Centre(True)
self.Show()

if __name__=='__main__':
app = wx.App()
frame = MiAplicacion(None, u"Mi aplicación")
app.MainLoop()

¿Y... cómo añadir controles?

Hasta ahora tenemos simplemente la ventana de la aplicación, sin ningún tipo de control gráfico que nos permita interactuar con el programa. Para añadir controles, en principio, la cuestión no es muy complicada, sólo habrá que especificar el tipo de control y algunos parámetros requeridos, véase el ejemplo a continuación que muestra como agregar un campo de texto editable (wx.TextCtrl) que permite emular un editor de texto plano:

# -*- coding: utf8 -*-
import wx

class MiAplicacion(wx.Frame):
def __init__(self,parent,title):
wx.Frame.__init__(self,parent,title=title,size=(400,300))
boton = wx.TextCtrl(self, style=wx.TE_MULTILINE)
self.Centre(True)
self.Show()

if __name__=='__main__':
app = wx.App()
frame = MiAplicacion(None, u"Mi aplicación")
app.MainLoop()



Como puede observarse, en el código sólo se añade una línea, en la cual se instancia un objeto de la clase wx.TextCtrl, pasándole como parent el Frame principal, y el argumento style especificando que se permita el uso de líneas múltiples dentro de ese control.

¿Y si quiero añadir más controles?, bueno aquí la cuestión se complica un poco, pero vamos, nada que no se pueda resolver. Aunque para ello ha de introducirse otro concepto básico en el desarrollo de aplicaciones en wxPython: los Sizers, que son clases que permiten alinear y organizar los objetos dentro de una ventana o contenedores, mediante algoritmos de posicionamiento. Evidentemente esto lo estaremos tratando en otro post, para no alargarnos demasiado.

Para tener una referencia más sólida respecto al desarrollo de aplicaciones en wxPython es recomendable que revisen el siguiente libro:

wxPython in Action [Noel Rappin and Robin Dunn]

Imágenes en menús de wxPython

Los menús en wxPython se definen de manera muy sencilla creando primeramente la barra de menú principal, derivada de la clase wx.MenuBar, enseguida se definen los menús principales que compondrán la barra de menú, derivando estos de la clase wx.Menu, finalmente se agregan los sub-menús que tienen cómo base la clase wx.MenuItem. En el siguiente esquema se muestra la jerarquía de menús en wxPython.


Un ejemplo muy básico:

self.mb = wx.MenuBar() # Creamos barra de menú
# Creamos el menú archivo
self.archivo = wx.Menu()
# Creamos los MenuItem (Guardar, Abrir)
self.guardar = wx.MenuItem(self.archivo,-1,"Guardar")
self.archivo.AppendItem(self.guardar)
self.abrir = wx.MenuItem(self.archivo,-1,"Abrir","")
self.archivo.AppendItem(self.abrir)
# Agregando el menú "Archivo" a la barra
self.mb.Append(self.archivo, "Archivo")
# Configurando a "mb" como la barra de menú del Frame
self.SetMenuBar(self.mb)

En lo anterior se supone que todo ese código está inmerso dentro de una clase heredada de wx.Frame. Así, se crea una barra de menú similar a lo mostrado en la siguiente imagen:




Para agregar una imagen al menú, se debe utilizar el método SetBitmap de la clase wx.MenuItem, pasando como parámetro un objeto de la clase wx.Bitmap, el cual deberá contener la información necesaria de la imagen o icono a utilizar, debe tomarse en cuenta que el método SetBitmap deberá "llamarse" antes de agregar el sub-menú al menú padre, de lo contrario no se verá reflejado dicho método.

Enseguida se adjunta el código completo de una aplicación wxPython que incluye imágenes en menús.

# -*- coding: utf8 -*-
import wx

class MiAplicacion(wx.Frame):
def __init__(self,parent,title):
wx.Frame.__init__(self,parent,title=title,size=(300,200))
self.createMenu() # Llamamos al método que inicializa el menú
self.Centre(True)
self.Show()

def createMenu(self):
"""
Crea el menú de la aplicación
"""
# Menú archivo
self.archivo = wx.Menu()
# Agregamos el sub-menú Guardar
self.guardar = wx.MenuItem(self.archivo,-1,"Guardar")
self.guardar.SetBitmap(wx.Bitmap( u"img/ic_save.png", wx.BITMAP_TYPE_ANY ))
self.archivo.AppendItem(self.guardar)
# Agregamos el sub-menú Abrir
self.abrir = wx.MenuItem(self.archivo,-1,"Abrir")
self.abrir.SetBitmap(wx.Bitmap( u"img/ic_open.png", wx.BITMAP_TYPE_ANY ))
self.archivo.AppendItem(self.abrir)
# Agregamos el sub-menú Imprimir
self.imprimir = wx.MenuItem(self.archivo,-1,"Imprimir")
self.imprimir.SetBitmap(wx.Bitmap( u"img/ic_print.png", wx.BITMAP_TYPE_ANY ))
self.archivo.AppendItem(self.imprimir)
# Agregamos el sub-menú Salir
self.salir = wx.MenuItem(self.archivo,-1,"Salir")
self.salir.SetBitmap(wx.Bitmap( u"img/ic_exit.png", wx.BITMAP_TYPE_ANY ))
self.archivo.AppendItem(self.salir)
# Creamos la barra de menú principal y la configuramos
self.mb = wx.MenuBar()
self.mb.Append(self.archivo, "Archivo")
self.SetMenuBar(self.mb)

if __name__=='__main__':
app = wx.App()
frame = MiAplicacion(None, u"Imágenes Menú")
app.MainLoop()



Note que la clase wx.Bitmap necesita como parámetro de entrada la ruta donde se encuentra la imagen y una constante wx.BITMAP_TYPE_ANY, que simplemente especifica el tipo de imagen leída. Puede consultar más en la documentación de wx.Bitmap.