quarta-feira, 18 de setembro de 2013

Um editor de textos rapidinho

Veremos como é simples criar uma aplicação gráfica rapidamente usando Tkinter.

Vamos começar a a partir de um boilerplate:

#!/usr/bin/env python
# coding: UTF-8
from __future__ import absolute_import, division, print_function, unicode_literals

#-------------------------------------------------------------
# Cabeçalhos
#

# Aqui vão entrar os imports

__all__ = ['Editor']


#-------------------------------------------------------------

class Editor(object):

    def mainloop(self):
        pass


#-------------------------------------------------------------
# Rodapé

if __name__ == '__main__':
    app = Editor()
    app.mainloop()

A primeira coisa que precisamos fazer é criar nossa janela raiz. Para tanto, adicione aos cabeçalhos o import:

from Tkinter import *

Método construtor

Em seguida, no começo da classe Editor, vamos começar a escrever nosso construtor:

class Editor(object):

    def __init__(self):
        root = self.root = Tk()
        root.title('EditPy')

Precisamos então de um frame para colocar os botões de abrir, salvar e salvar-como:

        fbuttons = Frame(root)
        Button(fbuttons, text='Open', command=self.open) \
              .pack(side=LEFT)
        Button(fbuttons, text='Save', command=self.save) \
              .pack(side=LEFT)
        Button(fbuttons, text='Save As', command=self.save_as) \
              .pack(side=LEFT)

O pai (master) do frame fbuttons é nossa janela raiz e o pai dos botões é o frame.

Agora precisamos de uma caixa de texto para editar e exibir o conteúdo dos arquivos. Primeiro vá aos cabeçalhos novamente e acrescente o import:

from ScrolledText import ScrolledText

Então, continuando o construtor, crie a caixa de texto com rolagem:

        steditor = self.steditor = ScrolledText(root)

Para fechar o construtor só falta exibir o frame e a caixa de texto, e colocar o foco na caixa:

        fbuttons.pack(expand=True, fill=X)
        steditor.pack(expand=True, fill=BOTH)
        steditor.focus()

Loop principal

Se você tentar rodar o código agora, nada acontece. Isso porque o método mainloop() faz isso: absolutamente nada.

Precisamos então, neste método, chamar o loop principal do Tkinter. Para isso, altere o método:

    def mainloop(self):
        self.root.mainloop()

Chamamos então o loop principal através da janela raiz (root).

Abrir um arquivo

Porém coisas estranhas e erros esdrúxulos podem acontecer, pois ainda não implementamos nenhum dos métodos chamados pelos botões – self.open, self.save e self.save_as.

Parece-me justo que comecemos pelo método de abertura de arquivo. Criemos a assinatura do método:

    def open(self):

Para abrir um arquivo, precisamos de uma ferramenta chamada askopenfilename que precisamos importar, então nos cabeçalhos acrescente:

from tkFileDialog import askopenfilename

Então, de volta ao método open(), podemos pegar o nome do arquivo e carregá-lo:

        filename = askopenfilename(title='EditPy | Open')
        if filename:
            self.filename = filename
            self.load_file()

O método load_file() é quem vai de fato carregar o arquivo, mas precisamos antes apagar o conteúdo da caixa de texto:

    def load_file(self):
        filename = self.filename
        steditor = self.steditor

        steditor.delete('@0,0', 'end')
        with open(filename) as fd:
            steditor.insert('@0,0', fd.read())

O método delete() da caixa de texto apaga parte do conteúdo da caixa e os parâmetros são os índices de começo e fim:
  • @0,0 significa linha 0, coluna 0 (começo do texto);
  • end significa o final do texto.

O método insert() inserte a string (lida do descritor de arquivo) na posição indicada (@0,0).

Salvando o texto

Para salvar o texto, precisamos implementar o método save(). Ele precisa:
  1. Verificar se temos um nome de arquivo – se não tivermos, é um save_as.
  2. Abrir o arquivo para escrita.
  3. Obter o texto da caixa de texto.
  4. Gravar o texto no arquivo.

Simples, indolor e o código é intuitivo:

    def save(self):
        filename = self.filename
        if not filename:
            return self.save_as()

        with open(filename, 'w') as fd:
            fd.write(
                self.steditor.get('@0,0', 'end')
            )

Repare nos índices usados no método get().

Em seguida é preciso saber o que fazer quando não temos o nome do arquivo ou quando escolhemos Save As.

Primeiro a assinatura do método:

    def save_as(self):

Caso haja um nome de arquivo, é honesto usá-lo como ponto de partida para caixa de diálogo de salvamento. Para isso precisamos separar o nome de diretório do nome base do arquivo e uma forma de fazer isso é com o método de string rfind.

Para sabermos qual o separador de diretórios usado, adicione aos imports:

from os import path

Então continue no método:

        filename, directory = self.filename, None
        if filename and path.sep in filename:
            index = filename.rfind(path.sep) + 1
            directory = filename[:index]
            filename = filename[index:]

[update 2014-04-12]
Optei por usar rfind, mas poderia ter usado path.basename e path.dirname. É uma questão de escolha e estilo.
[/update]
Feito o slice, já podemos exibir a caixa de diálogo de salvamento. Para isso precisamos da ferramenta asksaveasfilename.

Modifique o import do askopenfilename para:

from tkFileDialog import askopenfilename, asksaveasfilename

Continuando agora no método save_as(), podemos exibir a caixa de diálogo e, se algum arquivo for informado, chamar o método save():

        filename = asksaveasfilename(
            title = 'EditPy | Save As',
            initialfile = filename,
            initialdir = directory,
        )

        if filename:
            self.filename = filename
            self.save()

E pronto! Já pode testar seu editor de texto!

Lapidando

Uma forma de melhorar um pouco o visual da aplicação é usando a extensão ajulejada do tk, chamada ttk.

Para tanto, basta adicionar ao cabeçalho, no final dos imports:

from ttk import *

Você também pode usar os métodos wm_protocol() e bind_all() da janela raiz para adicionar funcionalidades interessantes, mas isso é história pra outro dia. ;-)

[]’s
Cacilhας, La Batalema
blog comments powered by Disqus