Problem with Combobox in Frame "scrollable", the scroll event passes to the father

1

I'm doing a desktop application with Tkinter and Python. I have inserted a drop-down control ttk.Combobox for several options but I have a problem. The combobox is inside a frame with scroll bar and when under the mouse wheel the combobox it goes down together with all the frame , it's very rare. Does anyone know how to solve this?

This is my code, the class VerticalScrolledFrame is downloaded from GitHub:

import tkinter as tk
from tkinter import ttk


class VerticalScrolledFrame(tk.Frame):
    def __init__(self, parent, *args, **kw):
        tk.Frame.__init__(self, parent, *args, **kw)

        # create a canvas object and a vertical scrollbar for scrolling it
        vscrollbar = tk.Scrollbar(self, orient=tk.VERTICAL)
        vscrollbar.pack(fill=tk.Y, side=tk.RIGHT, expand=tk.FALSE)
        canvas = tk.Canvas(self, bd=0, highlightthickness=0, bg='green',
                        yscrollcommand=vscrollbar.set)
        canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=tk.TRUE)
        vscrollbar.config(command=canvas.yview)

        # reset the view
        canvas.xview_moveto(0)
        canvas.yview_moveto(0)

        # create a frame inside the canvas which will be scrolled with it
        self.interior = interior = tk.Frame(canvas)
        interior_id = canvas.create_window(0, 0, window=interior,
                                           anchor=tk.NW)
        def _on_mousewheel(event):
            canvas.yview_scroll(int(-1 * (event.delta / 120)), "units")
        self.interior.bind_all("<MouseWheel>", _on_mousewheel)

        # track changes to the canvas and frame width and sync them,
        # also updating the scrollbar
        def _configure_interior(event):
            # update the scrollbars to match the size of the inner frame
            size = (interior.winfo_reqwidth(), interior.winfo_reqheight())
            canvas.config(scrollregion="0 0 %s %s" % size)
            if interior.winfo_reqwidth() != canvas.winfo_width():
                # update the canvas's width to fit the inner frame
                canvas.config(width=interior.winfo_reqwidth())
        interior.bind('<Configure>', _configure_interior)

        def _configure_canvas(event):
            if interior.winfo_reqwidth() != canvas.winfo_width():
                # update the inner frame's width to fill the canvas
                canvas.itemconfigure(interior_id, width=canvas.winfo_width())
        canvas.bind('<Configure>', _configure_canvas)

class Application(ttk.Frame):
    def __init__(self, main_window):


        super().__init__(main_window)


        main_window.geometry("400x400")

        frame = VerticalScrolledFrame(main_window)
        frame.pack(expand=True, fill='both')
        optionList = ['option1','option2','option3','option4','option5','option6','option7','option8',]
        combobox = ttk.Combobox(frame.interior, state="readonly", height=4, values=optionList)
        combobox.pack(side='top')

        for i in range(20):
            tk.Button(frame.interior, text= 'Button'+str(i)).pack()


main_window = tk.Tk()
app = Application(main_window)
app.mainloop()
    
asked by Alfredo Lopez Rodes 29.08.2018 в 11:14
source

1 answer

2

The problem is that bind_all is being used to link the scroll events with the callback . bind_all performs the link at the application level, so all events generated by the mouse wheel in the application end up calling the callback _on_mousewheel of the last instance of the class VerticalScrolledFrame . This causes that when carrying out a scroll in the Combobox , not only its own callback is executed, but also the associate at the app level by bind_all .

This is solved using bind that performs the link of the event with the callback at the widget level (instance) and not at the level of the whole app as bind_all :

import tkinter as tk
from tkinter import ttk


class VerticalScrolledFrame(tk.Frame):
    def __init__(self, parent, *args, **kw):
        super().__init__(parent, *args, **kw)   

        self._vscrollbar = tk.Scrollbar(self, orient=tk.VERTICAL)
        self._vscrollbar.pack(fill=tk.Y, side=tk.RIGHT, expand=tk.FALSE)
        self._canvas = tk.Canvas(self, bd=0, highlightthickness=0, bg='green',
                                 yscrollcommand=self._vscrollbar.set)
        self._canvas.pack(side=tk.TOP, fill=tk.BOTH, expand=tk.TRUE)
        self._vscrollbar.config(command=self._canvas.yview)

        self._canvas.xview_moveto(0)
        self._canvas.yview_moveto(0)

        self._inner_frame = tk.Frame(self._canvas)
        self._inner_id = self._canvas.create_window(0, 0,
                                                    window=self._inner_frame,
                                                    anchor=tk.NW
                                                   )
        self._inner_frame.bind("<MouseWheel>", self.on_mousewheel) 
        self._inner_frame.bind('<Configure>', self._configure_inner)                
        self._canvas.bind('<Configure>', self._configure_canvas)

    @property
    def container(self):
        return self._inner_frame

    def _configure_canvas(self, event):
        width = max(self._inner_frame.winfo_reqwidth(), self._canvas.winfo_width())
        height = max(self._inner_frame.winfo_reqheight(), self._canvas.winfo_height())
        self._canvas.itemconfigure(self._inner_id, width=width, height=height)

    def _configure_inner(self, event):
        self._canvas.configure(scrollregion=self._canvas.bbox("all"))

    def on_mousewheel(self, event):
        self._canvas.yview_scroll(int(-1 * (event.delta / 120)), "units")
        return "break"


class Application(ttk.Frame):
    def __init__(self, main_window):
        super().__init__(main_window)
        main_window.geometry("400x400")
        frame = VerticalScrolledFrame(main_window)
        frame.pack(expand=True, fill='both')
        optionList = ['option1','option2','option3','option4','option5','option6','option7','option8',]
        combobox = ttk.Combobox(frame.container, name="combo",  state="readonly",
                                height=4, values=optionList
                                )
        combobox.pack(side='top')

        for i in range(20):
            btn = tk.Button(frame.container, text= 'Button' + str(i))
            btn.pack()
            # Habilitamos el scroll en los botones
            btn.bind("<MouseWheel>", frame.on_mousewheel)


if __name__ == "__main__":
    main_window = tk.Tk()
    app = Application(main_window)
    app.mainloop()

The result is:

  

Note: in Linux with X11 the event "<MouseWheel>" is not appropriate to capture the scroll of the mouse wheel, instead you must capture the events "<Button-4>" (upload) and "<Button-5>" (download):

self._inner_frame.bind("<Button-4>", self.on_mousewheel) 
self._inner_frame.bind("<Button-5>", self.on_mousewheel) 

def on_mousewheel(self, event):
    if event.num == 4:
        print(4)
        self._canvas.yview_scroll(-1, "units")
    elif event.num == 5:
        print(5)
        self._canvas.yview_scroll(1, "units")
    
answered by 29.08.2018 / 22:14
source