Starting with code of a response (set by me) to a of my questions, I have come to improve the panel code finder introducing certain improvements and functions (that as you type the new term and search, replace action, keyboard shortcuts, ...)
The code looks like this:
# encoding: utf-8
'''
####### Python 3 #######
import tkinter as tk
import tkinter.font as tkFont
from tkinter import messagebox as MessageBox
'''
''''''
####### Python 2 #######
import Tkinter as tk
import tkFont
import tkMessageBox as MessageBox
def beep_error(f):
'''
Decorador que permite emitir un beep cuando un método de instancia
decorado de un widget produce una excepción
'''
def applicator(*args, **kwargs):
try:
f(*args, **kwargs)
except:
if args and isinstance(args[0], tk.Widget):
args[0].bell()
return applicator
class MyText(tk.Text):
def __init__(self, parent=None, app=None, *args, **kwargs):
tk.Text.__init__(self, parent, *args, **kwargs)
self.parent = parent
self.app = app
self.bind('<Control-a>', self.seleccionar_todo)
self.bind('<Control-x>', self.cortar)
self.bind('<Control-c>', self.copiar)
self.bind('<Control-v>', self.pegar)
self.bind('<Control-z>', self.deshacer)
self.bind('<Control-Shift-z>', self.rehacer)
self.bind("<Button-3><ButtonRelease-3>", self.mostrar_menu)
# Para Búsqueda Total | Anterior | Siguiente
self.idx_gnral = tk.StringVar()
pos_cursor = self.index(tk.INSERT)
self.idx_gnral.set(pos_cursor)
def mostrar_menu(self, event):
'''
Muestra un menú popup con las opciones copiar, pegar y cortar
al hacer click derecho en el Text
'''
menu = tk.Menu(self, tearoff=0)
menu.add_command(label="Cortar", command=self.cortar)
menu.add_command(label="Copiar", command=self.copiar)
menu.add_command(label="Pegar", command=self.pegar)
menu.tk.call("tk_popup", menu, event.x_root, event.y_root)
def copiar(self, event=None):
self.event_generate("<<Copy>>")
self.see("insert")
return 'break'
def cortar(self, event=None):
self.event_generate("<<Cut>>")
return 'break'
def pegar(self, event=None):
self.event_generate("<<Paste>>")
self.see("insert")
return 'break'
def seleccionar_todo(self, event=None):
self.tag_add('sel', '1.0', 'end')
return 'break'
@beep_error
def deshacer(self, event=None):
self.tk.call(self, 'edit', 'undo')
return 'break'
@beep_error
def rehacer(self, event=None):
self.tk.call(self, 'edit', 'redo')
return 'break'
def buscar_todo(self, txt_buscar=None):
'''Buscar todas las ocurrencias en el Entry de MainApp'''
# eliminar toda marca establecida, si existiera, antes de plasmar nuevos resultados
self.elim_tags(['found', 'found_prev_next'])
# Reiniciar idx_gnral desde la posición del cursor
self.idx_gnral.set(self.index(tk.INSERT))
if txt_buscar:
# empezar desde el principio (y parar al llegar al final [stopindex >> END])
idx = '1.0'
len_ocurr = tk.IntVar()
while True:
# encontrar siguiente ocurrencia, salir del loop si no hay más
idx = self.search(txt_buscar, idx, count=len_ocurr, nocase=1, stopindex=tk.END)
if not idx: break
# index justo después del final de la ocurrencia
lastidx = '%s+%dc' % (idx, len_ocurr.get())
# etiquetando toda la ocurrencia (incluyendo el start, excluyendo el stop)
self.tag_add('found', idx, lastidx)
# preparar para buscar la siguiente ocurrencia
idx = lastidx
# configurando la forma de etiquetar las ocurrencias encontradas
self.tag_config('found', background='dodgerblue')
self.buscar_next(txt_buscar)
# Con el EVENTO establecido en el Entry, se ve menos necesario este aviso.
# Incluso, puede resultar molesto.
####else:
#### MessageBox.showinfo('Info', 'Establecer algún criterio de búsqueda.')
def buscar_prev(self, txt_buscar=None):
'''Buscar previa ocurrencia en el Entry de MainApp'''
# Tratar índice por si viniera buscar_next()
self.idx_gnral.set(self.idx_gnral.get().replace('+', '-'))
# eliminar el tag 'found_prev_next', si existiera, antes de plasmar nuevo resultado
self.elim_tags(['found_prev_next'])
if txt_buscar:
len_ocurr = tk.IntVar()
idx = self.search(txt_buscar, self.idx_gnral.get(), count=len_ocurr, nocase=1, backwards=True)
# Siempre que haya una coincidencia
if(idx != ''):
# Para hacer SCROLL hasta el resultado de la búsqueda
# si ésta no estuviera visible
self.see(idx)
# index justo después del final de la ocurrencia
lastidx = '%s+%dc' % (idx, len_ocurr.get())
# etiquetando toda la ocurrencia (incluyendo el start, excluyendo el stop)
self.tag_add('found_prev_next', idx, lastidx)
# preparar para buscar la anterior ocurrencia
lastidx_prev = '%s-%dc' % (idx, len_ocurr.get())
self.idx_gnral.set(lastidx_prev)
# establecer la marca distintiva para la ocurrencia a etiquetar
self.tag_config('found_prev_next', background='orangered')
# Con el EVENTO establecido en el Entry, se ve menos necesario este aviso.
# Incluso, puede resultar molesto.
####else:
#### MessageBox.showinfo('Info', 'Establecer algún criterio de búsqueda.')
def buscar_next(self, txt_buscar=None):
'''Buscar siguiente ocurrencia en el Entry de MainApp'''
# Tratar índice por si viniera buscar_prev()
self.idx_gnral.set(self.idx_gnral.get().replace('-', '+'))
# eliminar el tag 'found_prev_next', si existiera, antes de plasmar nuevo resultado
self.elim_tags(['found_prev_next'])
if txt_buscar:
len_ocurr = tk.IntVar()
idx = self.search(txt_buscar, self.idx_gnral.get(), count=len_ocurr, nocase=1)
# Siempre que haya una coincidencia
if(idx != ''):
# Para hacer SCROLL hasta el resultado de la búsqueda
# si ésta no estuviera visible
self.see(idx)
# index justo después del final de la ocurrencia
lastidx = '%s+%dc' % (idx, len_ocurr.get())
# etiquetando toda la ocurrencia (incluyendo el start, excluyendo el stop)
self.tag_add('found_prev_next', idx, lastidx)
# preparar para buscar la siguiente ocurrencia
self.idx_gnral.set(lastidx)
# establecer la marca distintiva para la ocurrencia a etiquetar
self.tag_config('found_prev_next', background='orangered')
# Con el EVENTO establecido en el Entry, se ve menos necesario este aviso.
# Incluso, puede resultar molesto.
####else:
#### MessageBox.showinfo('Info', 'Establecer algún criterio de búsqueda.')
def reemplazar(self, txt_buscar=None, txt_reemplazar=None, all=None):
'''Reemplazo de ocurrencia(s) por otro término'''
coords=[]
if(all):
l = list(self.tag_ranges('found'))
print('** antes del REVERSE **:')
for l_i in l:
print l_i
else:
l = list(self.tag_ranges('found_prev_next'))
# dándole la vuelta a la lista de índices marcados
# para poder construir bien las coordenadas
l.reverse()
print('** tras del REVERSE **:')
for l_i in l:
print l_i
while l:
coords.append([l.pop(),l.pop()])
print('** COORDS **:')
for start, end in coords:
print('%s, %s' % (start, end))
print('DENTRO DEL FOR ... COORDS')
if(all):
# para un buen reemplazo múltiple, mejor empezar por el final
coords.reverse()
for start, end in coords:
print('%s, %s' % (start, end))
self.delete(start, end)
self.insert(start, txt_reemplazar)
if(all is None):
self.buscar_next(txt_buscar)
def elim_tags(self, l_tags):
'''Eliminar etiqueta(s) pasada(s)'''
if(len(l_tags) > 0):
for l_tag in l_tags:
self.tag_delete(l_tag)
class MainApp(tk.Tk):
def __init__(self, *args, **kwargs):
tk.Tk.__init__(self, *args, **kwargs)
menubar = tk.Menu(self, bg='black', fg='white')
self.config(menu=menubar)
editmenu = tk.Menu(menubar, tearoff=0, bg='black', fg='white')
menubar.add_cascade(label='Editar', menu=editmenu, underline=0)
findmenu = tk.Menu(menubar, tearoff=0, bg='black', fg='white')
menubar.add_cascade(label='Buscar', menu=findmenu, underline=0)
frame = tk.Frame(self, bg='black')
frame.pack(fill='both', expand=1)
frame.config(padx=10, pady=10)
frame_txt = tk.Frame(frame, background='black')
frame_txt.grid(row=1, column=0, padx=5, pady=5, sticky=tk.W+tk.E)
self.text_font = tkFont.Font(family='Consolas', size=12)
self.text_01 = MyText(frame_txt, app=self, wrap=tk.WORD, bd=0, undo=True)
self.text_01.pack(fill='both', expand=1)
self.text_01.config(bd=0, padx=6, pady=4, font=self.text_font,
selectbackground='lightblue',
width=44, height=16,
bg='#242424', fg='white',
insertbackground='white',
highlightbackground='black',
highlightcolor='white'
)
self.text_01.bind('<Control-f>', lambda x: self.buscar_reemplazar(False))
self.text_01.bind('<Control-h>', lambda x: self.buscar_reemplazar(True))
editmenu.add_command(label='Deshacer',
command=self.text_01.deshacer,
accelerator='Ctrl+Z'
)
editmenu.add_command(label='Rehacer',
command=self.text_01.rehacer,
accelerator='Ctrl+Shift+Z'
)
editmenu.add_separator()
editmenu.add_command(label='Cortar',
command=self.text_01.cortar,
accelerator='Ctrl+X'
)
editmenu.add_command(label='Copiar',
command=self.text_01.copiar,
accelerator='Ctrl+C'
)
editmenu.add_command(label='Pegar',
command=self.text_01.pegar,
accelerator='Ctrl+V'
)
editmenu.add_command(label='Seleccionar todo',
command=self.text_01.seleccionar_todo,
accelerator='Ctrl+A'
)
findmenu.add_command(label='Buscar',
command=self.buscar_reemplazar,
accelerator='Ctrl+F'
)
findmenu.add_command(label='Buscar y Reemplazar',
command=lambda: self.buscar_reemplazar(True),
accelerator='Ctrl+H'
)
# Cargando un texto de prueba
text_01_contenido = '''[INICIO]
01- Esto es un contenido de prueba para buscar cualquier texto.
02- Unas cuántas palabras que forman frases y párrafos en los que buscar concurrencias referidas a los términos buscados.
03- Esto es un contenido de prueba para buscar cualquier texto.
04- Unas cuántas palabras que forman frases y párrafos en los que buscar concurrencias referidas a los términos buscados.
05- Esto es un contenido de prueba para buscar cualquier texto.
06- Unas cuántas palabras que forman frases y párrafos en los que buscar concurrencias referidas a los términos buscados.
[ES el FIN]'''
self.text_01.insert(1.0, text_01_contenido)
# Para evitar que se abran más de un panel de bus_reem_top
self.bus_reem_top_on = False
def buscar_reemplazar(self, con_reemplazo=None, event=None):
'''Panel de búsqueda/reemplazo de términos en el Text'''
if(self.bus_reem_top_on is False):
bus_reem_top_w = 360
bus_reem_top_h = 80
bus_reem_top_tit = 'Buscar'
bus_reem_top_msg_w = 240
if(con_reemplazo):
bus_reem_top_h = 120
bus_reem_top_tit = 'Buscar y Reemplazar'
bus_reem_top_msg_w = 280
bus_reem_top_x = (self.winfo_screenwidth() / 2) - (bus_reem_top_w / 2)
bus_reem_top_y = (self.winfo_screenheight() / 2) - (bus_reem_top_h / 2)
self.bus_reem_top = tk.Toplevel(self)
# Considerando evento de cierre de la ventana
self.bus_reem_top.protocol('WM_DELETE_WINDOW', self.on_closing_bus_reem_top)
self.bus_reem_top.geometry('{}x{}+{}+{}'.format(bus_reem_top_w, bus_reem_top_h, bus_reem_top_x, bus_reem_top_y))
self.bus_reem_top.config(bg='white', padx=5, pady=5)
self.bus_reem_top.resizable(1,1)
self.bus_reem_frm_tit = tk.Frame(self.bus_reem_top, bg='grey', pady=5)
self.bus_reem_frm_tit.pack(fill='x', expand=1)
# ¿¿Cómo centrar este Frame o su contenido??
self.bus_reem_frm_busca = tk.Frame(self.bus_reem_top, bg='grey', padx=5, pady=5)
self.bus_reem_frm_busca.pack(fill='x', expand=1)
self.bus_reem_top.title('{}...'.format(bus_reem_top_tit))
bus_reem_top_head = '~ {} ~'.format(bus_reem_top_tit)
bus_reem_top_msg = tk.Message(self.bus_reem_frm_tit, text=bus_reem_top_head, bg='grey', padx=10, pady=0)
bus_reem_top_msg.pack(fill='both', expand=1)
bus_reem_top_msg.config(width=bus_reem_top_msg_w, justify='center', font=('Consolas', 14, 'bold'))
self.entr_str_busca = tk.Entry(self.bus_reem_frm_busca)
self.entr_str_busca.grid(row=0, column=0, padx=3)
# Si hay un texto seleccionado...
if(self.text_01.tag_ranges('sel')):
TXT_seleccionado = self.text_01.get(tk.SEL_FIRST, tk.SEL_LAST)
# Rellenando la caja si hay algo seleccionado en el Text
self.entr_str_busca.insert(0, TXT_seleccionado)
self.btn_buscar_todo = tk.Button(self.bus_reem_frm_busca, text='Buscar', command=lambda: self.text_01.buscar_todo(self.entr_str_busca.get()))
self.btn_buscar_todo.grid(row=0, column=1, padx=3)
self.btn_buscar_prev = tk.Button(self.bus_reem_frm_busca, text='<|', command=lambda: self.text_01.buscar_prev(self.entr_str_busca.get()))
self.btn_buscar_prev.grid(row=0, column=2, padx=3)
self.btn_buscar_next = tk.Button(self.bus_reem_frm_busca, text='|>', command=lambda: self.text_01.buscar_next(self.entr_str_busca.get()))
self.btn_buscar_next.grid(row=0, column=3, padx=3)
self.entr_str_busca.focus_set()
# Bindings
# Considerando evento tras soltar cualquier tecla pulsada
# dentro del entr_str_busca
self.entr_str_busca.bind('<Any-KeyRelease>', lambda x: self.text_01.buscar_todo(self.entr_str_busca.get()))
####self.bus_reem_top.bind('<KeyRelease-F2>', lambda x: self.text_01.buscar_prev(self.entr_str_busca.get()))
####self.bus_reem_top.bind('<KeyRelease-F3>', lambda x: self.text_01.buscar_next(self.entr_str_busca.get()))
if(con_reemplazo):
self.bus_reem_frm_reempl = tk.Frame(self.bus_reem_top, bg='grey', padx=5, pady=5)
self.bus_reem_frm_reempl.pack(fill='x', expand=1)
self.entr_str_reempl = tk.Entry(self.bus_reem_frm_reempl)
self.entr_str_reempl.grid(row=1, column=0, padx=3)
self.btn_reemplazar_next = tk.Button(self.bus_reem_frm_reempl, text='Reemplazar', command=lambda: self.text_01.reemplazar(self.entr_str_busca.get(), self.entr_str_reempl.get()))
self.btn_reemplazar_next.grid(row=1, column=1, padx=2)
self.btn_reemplazar_todo = tk.Button(self.bus_reem_frm_reempl, text='Reemplazar todo', command=lambda: self.text_01.reemplazar(self.entr_str_busca.get(), self.entr_str_reempl.get(), True))
self.btn_reemplazar_todo.grid(row=1, column=2, padx=2)
# Para evitar que se abran más de un panel de bus_reem_top
self.bus_reem_top_on = True
# Para que el evento no se propague
return 'break'
def on_closing_bus_reem_top(self):
'''En el momento de cerrar el cuadro de búsqueda'''
if MessageBox.askokcancel('Quit', '¿Cerrar el panel de búsqueda?'):
# borrando toda etiqueta establecida en los resultados de búsqueda
self.text_01.elim_tags(['found', 'found_prev_next'])
# cerrando búsqueda
self.bus_reem_top.destroy()
# Para evitar que se abran más de un panel de bus_reem_top
self.bus_reem_top_on = False
if __name__ == '__main__':
MainApp().mainloop()
This works fine:
-
a search panel is opened by the menu option [Search > > Search] and [Search> > Search and Replace], or by the combination of keys [CTRL + F] and [CTRL + H], respectively.
-
if it opens with something selected, it looks for that term.
-
starts a new search as you type something in the Entry.
-
the first match is marked from the point where the cursor is within the Text.
-
and other improvements ...
As I say, everything is fine until I add keyboard shortcuts ("Bindings") to the search panel to activate the actions linked to the buttons of self.btn_buscar_prev
and self.btn_buscar_next
in this way:
(in the previous code block they are commented)
self.bus_reem_top.bind('<KeyRelease-F2>', lambda x: self.text_01.buscar_prev(self.entr_str_busca.get()))
self.bus_reem_top.bind('<KeyRelease-F3>', lambda x: self.text_01.buscar_next(self.entr_str_busca.get()))
Once these two lines are added, in principle, everything continues to work well. But what does not work is that, when pressing (or rather, releasing) [F2] or [F3] do the same as their respective buttons of [< |] and [| >].
For example, the [F2], instead of going to the coincidence to the previous match as it does if the corresponding button is pressed, makes a stranger. The same happens when pressing [F3].
It works badly, but no error is shown in the terminal.
I noticed that there seems to be an incompatibility between applying the two Bindings of the Previous and Next buttons with these two lines within def buscar_todo(self, txt_buscar=None):
:
# ...
# Reiniciar idx_gnral desde la posición del cursor
self.idx_gnral.set(self.index(tk.INSERT))
#...
self.buscar_next(txt_buscar)
So that, if I comment these two lines, the action of the Bindings towards the keys of [F2] and [F3] returns to work well. But, of course, then, I'm left without the functionality of marking the first match automatically.
Then, what would I have to correct or do to make both things compatible? Does anyone see why this happens and how to correct it?
[ Edited ]
On the other hand, I am trying to establish a kind of search results counter, as I explain in another question .
Modifying the def buscar_todo(self, txt_buscar=None):
block so that it counts the search results and stores them in a variable cont_results
:
def buscar_todo(self, txt_buscar=None):
'''Buscar todas las ocurrencias en el Entry de MainApp'''
# eliminar toda marca establecida, si existiera, antes de plasmar nuevos resultados
self.elim_tags(['found', 'found_prev_next'])
# Reiniciar idx_gnral desde la posición del cursor
self.idx_gnral.set(self.index(tk.INSERT))
if txt_buscar:
# Contador total de resultados
cont_results = 0
# empezar desde el principio (y parar al llegar al final [stopindex >> END])
idx = '1.0'
len_ocurr = tk.IntVar()
while True:
# encontrar siguiente ocurrencia, salir del loop si no hay más
idx = self.search(txt_buscar, idx, count=len_ocurr, nocase=1, stopindex=tk.END)
if not idx: break
# index justo después del final de la ocurrencia
lastidx = '%s+%dc' % (idx, len_ocurr.get())
# etiquetando toda la ocurrencia (incluyendo el start, excluyendo el stop)
self.tag_add('found', idx, lastidx)
# preparar para buscar la siguiente ocurrencia
idx = lastidx
cont_results += 1
# configurando la forma de etiquetar las ocurrencias encontradas
self.tag_config('found', background='dodgerblue')
self.buscar_next(txt_buscar)
So, I would like that value to be reflected in the header of the search engine panel. To do this, I modified these lines in the construction of the panel
bus_reem_top_head = '~ {} ~'.format(bus_reem_top_tit)
bus_reem_top_msg = tk.Message(self.bus_reem_frm_tit, text=bus_reem_top_head, bg='grey', padx=10, pady=0)
for these others (in which a StringVar ()) is also added
####bus_reem_top_head = '~ {} ~'.format(bus_reem_top_tit)
# Contador resultados de búsqueda
self.bus_reem_num_results = tk.StringVar()
self.bus_reem_num_results.set('~ {} ~'.format('Sin resultados'))
bus_reem_top_msg = tk.Message(self.bus_reem_frm_tit, text=self.bus_reem_num_results.get(), bg=_cfg__._root_color_quater, padx=10, pady=0)
Therefore, I would like to pass the value of the variable% co_of% that resides in the method cont_results
of the buscar_todo()
to the variable of type class MyText()
that is within the StringVar()
of the def buscar_reemplazar()
.
Taking advantage of the fact that I pass class MainApp()
as class MainApp()
in the construction of the app
, I have tried to access the aforementioned StringVar () but I have not managed, still and all, to take the desired value with this line to end of the class MyText()
block:
#...
self.buscar_next(txt_buscar)
# Reflejando cantidad de resultados
self.app.bus_reem_num_results.set('X de {}'.format(cont_results))
print('self.app.bus_reem_num_results >> [{}]'.format(self.app.bus_reem_num_results.get()))
I only get the value out through the terminal but not in the place of the desired search engine panel. Does anyone know how to get it?
Greetings.