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()