Incompatibilities in search engine on a Tkinter Text and show counter

1

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.

    
asked by zacktagnan 16.06.2018 в 21:11
source

1 answer

1

Regarding the first problem, the cause is that there are two events that are generated when the F2 and F3 keys are pressed:

  • self.entr_str_busca.bind('<Any-KeyRelease>'...) causes the event to be generated when any key is released , including of course F2 and F3 when the Focus is on the Entry entr_str_busca .

  • self.bus_reem_top.bind('<KeyRelease-F2>', ...) and self.bus_reem_top.bind('<KeyRelease-F3>', ...) cause the corresponding events to be thrown when you release F2 and F3 respectively in the TopLevel bus_reem_top , parent of entr_str_busca .

When you press and release F2 or F3 the callback associated with '<Any-KeyRelease>' is called first because it is the event associated with the widget itself, as the event continues to spread the callbacks associated with the events '<KeyRelease-F2>' and '<KeyRelease-F3>' of the parent widget are also called. This causes the strange behavior, since before executing self.text_01.buscar_prev and self.text_01.buscar_next it is called self.text_01.buscar_todo .

This would not happen if both events had the same source widget , in which case only the callbacks associated with '<KeyRelease-F2>' and '<KeyRelease-F3>' would be executed.

If you want '<Any-KeyRelease>' to be associated to the Text while the events for F2 and F3 are to your parent, which you can do is block the propagation of the event in the child for those keys that are not these two:

self.entr_str_busca.bind('<Any-KeyRelease>', self.on_entr_str_busca_key_release)
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()))

then we create the following method in class MainApp :

 def on_entr_str_busca_key_release(self, event):
    if event.keysym != "F2" and event.keysym != "F3":  # F2 y F3
        self.text_01.buscar_todo(self.entr_str_busca.get())
        return "break"

Regarding the second problem, there are several ways to approach it, a very simple one is to make buscar_todo return the counter:

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

        cont_results = 0

        if txt_buscar:
            # Contador total de resultados
            # 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)
        return cont_results    

Then in MyApp add a method that acts as a wrapper:

def _buscar(self, event=None):
    cont = self.text_01.buscar_todo(self.entr_str_busca.get())
    if cont:
        self.bus_reem_num_results.set('~ {} ~'.format(cont))
    else:
        self.bus_reem_num_results.set('~ {} ~'.format('Sin resultados'))

and call it instead of self.text_01.buscar_todo in these two points:

  • Callback of btn_buscar_todo button:

    self.btn_buscar_todo = tk.Button(self.bus_reem_frm_busca,
                                     text='Buscar',
                                     command=self._buscar
                                     )
    
  • Method on_entr_str_busca_key_release of the previous point:

    def on_entr_str_busca_key_release(self, event):
        if event.keysym != "F2" and event.keysym != "F3":  # F2 y F3
            self._buscar()
            return "break"
    

On the other hand you should not do this:

bus_reem_top_msg = tk.Message(self.bus_reem_frm_tit,
                              text=self.bus_reem_num_results.get(),
                              padx=10, pady=0
                              )

with it the Message receives the content of the StringVar as the text to be displayed, it is the same as doing text="cadena" . If the StringVar is modified the text of Message will not be altered since it is not linked to the variable itself, you must pass the own instance of the StringVar to the argument textvariable :

bus_reem_top_msg = tk.Message(self.bus_reem_frm_tit,
                              textvariable=self.bus_reem_num_results,
                              padx=10, pady=0)

Edit

Given that in order to implement a search counter you need to search all the matches in one go, buscar_next and buscar_prev lose their utility as they are. To implement the previous two methods and the counter, the methods of Text tag_prevranges and tag_nextranges that given a label and an index or range of indexes look for the first occurrence of this label in the text can be helpful.

One possible implementation could be:

# -*- coding: 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)


        self._ocurrencias_encontradas = []
        self._numero_ocurrencia_actual = None


    @property
    def numero_ocurrencias(self):
        return len(self._ocurrencias_encontradas)

    @property
    def numero_ocurrencia_actual(self):
        return self._numero_ocurrencia_actual

    @property
    def indice_ocurrencia_actual(self):
        tags = self.tag_ranges('found_prev_next')
        return tags[:2] if tags else None

    @indice_ocurrencia_actual.setter
    def indice_ocurrencia_actual(self, idx):
        # establecer la marca distintiva para la ocurrencia a etiquetar
        self.elim_tags(['found_prev_next'])
        self.tag_config('found_prev_next', background='orangered')

        if idx is not None:
            self.tag_add('found_prev_next', *idx)
            self.see(idx[0])
            self._numero_ocurrencia_actual = self._ocurrencias_encontradas.index(self.indice_ocurrencia_actual) + 1
        else:
            self._numero_ocurrencia_actual = None

    @property
    def ocurrencias_encontradas(self):
        return self._ocurrencias_encontradas


    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 get_index(self, index):
        '''Dado un índice en cualquier formato retorna una tupla (fila -> int, columna -> int)'''
        return tuple(int(idx) for idx in self.index(index).split("."))


    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'])

        if txt_buscar:
            # Contador total de resultados
            # 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')

        tags = self.tag_ranges('found')
        self._ocurrencias_encontradas = list(zip(*[iter(tags)] * 2))

        self.buscar_next()


    def buscar_prev(self):
        '''Buscar previa ocurrencia en el Entry de MainApp'''
        idx = self.indice_ocurrencia_actual[0] if self.indice_ocurrencia_actual else self.index(tk.INSERT)    
        self.indice_ocurrencia_actual = self.tag_prevrange('found', idx) or self.tag_prevrange('found', self.index(tk.END)) or None


    def buscar_next(self):
        '''Buscar siguiente ocurrencia en el Entry de MainApp'''
        idx = self.indice_ocurrencia_actual[1] if self.indice_ocurrencia_actual else self.index(tk.INSERT)    
        self.indice_ocurrencia_actual = self.tag_nextrange('found', idx) or self.tag_nextrange('found', "0.0") or None


    def reemplazar(self, txt_reemplazar=None, all=False):
        '''Reemplazo de ocurrencia(s) por otro término'''
        if not all and self.indice_ocurrencia_actual is not None:
            start, end = self.indice_ocurrencia_actual
            self._ocurrencias_encontradas.remove(self.indice_ocurrencia_actual)
            self.delete(start, end)
            self.insert(start, txt_reemplazar)
            tags = self.tag_ranges('found')
            self.buscar_next()


        elif all:
            for start, end in reversed(self.ocurrencias_encontradas):
                self.delete(start, end)
                self.insert(start, txt_reemplazar)
                self._ocurrencias_encontradas  = []
                self.indice_ocurrencia_actual = None


    def elim_tags(self, l_tags):
        '''Eliminar etiqueta(s) pasada(s)'''
        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 not self.bus_reem_top_on:

            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))
            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, textvariable=self.bus_reem_num_results, bg=_cfg__._root_color_quater, padx=10, pady=0)  # <<<<<<<<<<<<<< comentado para no depender de _cgfg__
            bus_reem_top_msg = tk.Message(self.bus_reem_frm_tit, textvariable=self.bus_reem_num_results, 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._buscar()
                self._buscar_anterior()

            self.btn_buscar_todo = tk.Button(self.bus_reem_frm_busca, text='Buscar', command=self._buscar)
            self.btn_buscar_todo.grid(row=0, column=1, padx=3)
            self.btn_buscar_prev = tk.Button(self.bus_reem_frm_busca, text='<|', command=self._buscar_anterior)
            self.btn_buscar_prev.grid(row=0, column=2, padx=3)
            self.btn_buscar_next = tk.Button(self.bus_reem_frm_busca, text='|>', command=self._buscar_siguiente)
            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>', self.on_entr_str_busca_key_release)
            self.bus_reem_top.bind('<KeyRelease-F2>', self._buscar_anterior)
            self.bus_reem_top.bind('<KeyRelease-F3>', self._buscar_siguiente)

            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=self._reemplazar)
                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=self._reemplazar_todo)
                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

    def on_entr_str_busca_key_release(self, event):
        if event.keysym != "F2" and event.keysym != "F3":  # F2 y F3
            self._buscar()
            return "break"

    def _buscar(self, event=None):
        self.text_01.buscar_todo(self.entr_str_busca.get())
        if self.text_01.ocurrencias_encontradas:
            self.bus_reem_num_results.set('~ {} de {} ~'.format(self.text_01.numero_ocurrencia_actual, self.text_01.numero_ocurrencias))
        else:
            self.bus_reem_num_results.set('~ {} ~'.format('Sin resultados'))

    def _buscar_siguiente(self, event=None):
        self.text_01.buscar_next()
        if self.text_01.ocurrencias_encontradas:
            self.bus_reem_num_results.set('~ {} de {} ~'.format(self.text_01.numero_ocurrencia_actual, self.text_01.numero_ocurrencias))
        else:
            self.bus_reem_num_results.set('~ {} ~'.format('Sin resultados'))

    def _buscar_anterior(self, event=None):
        self.text_01.buscar_prev()
        if self.text_01.ocurrencias_encontradas:
            self.bus_reem_num_results.set('~ {} de {} ~'.format(self.text_01.numero_ocurrencia_actual, self.text_01.numero_ocurrencias))
        else:
            self.bus_reem_num_results.set('~ {} ~'.format('Sin resultados'))

    def _reemplazar(self, event=None):
        self.text_01.reemplazar(self.entr_str_reempl.get())
        if self.text_01.ocurrencias_encontradas:
            self.bus_reem_num_results.set('~ {} de {} ~'.format(self.text_01.numero_ocurrencia_actual, self.text_01.numero_ocurrencias))
        else:
            self.bus_reem_num_results.set('~ {} ~'.format('Sin resultados'))

    def _reemplazar_todo(self, event=None):
        self.text_01.reemplazar(self.entr_str_reempl.get(), True)
        if self.text_01.ocurrencias_encontradas:
            self.bus_reem_num_results.set('~ {} de {} ~'.format(self.text_01.numero_ocurrencia_actual, self.text_01.numero_ocurrencias))
        else:
            self.bus_reem_num_results.set('~ {} ~'.format('Sin resultados'))




if __name__ == '__main__':
    MainApp().mainloop()

the code surely can be improved. There are some things to be determined, for example, currently the Text can be edited while a search is performed, which may conflict with the handling of indices in the search and replacements. The common solution for the latter is to block the editing of the text while the search / replace form is open.

    
answered by 17.06.2018 / 01:42
source