cefpython3 block UI on windows with tkinter

450 Views Asked by At

I'm making an application with tkinter in python 3.7.8.

I have to show a map so I use Folium to generate it and cefpython3 to show it (as it generates as a html with javascript)

It works wonderful on Linux, but when I try to test it on Windows 10, the map is shown, i can click on buttons, but cant interact with any tkinter.Entry unless i click on another window, and then come back to the window program.

I assumed it was a focus problem, but even trying to set the focus of the cefpython3 browser to False, the problem keep happening.

This is the MapVisualizer Code

class MapVisualizer(tk.Frame):
    """ Generates the map with folium and then attach cefpython3 browser to the frame """
    def __init__(self, root, size, mapPoints, zoom, pointDatas=None, **kargs):
        self.browser_frame = None
        self.navigation_bar = None

        self.replaceFolium()
        m = folium.Map(location=mapPoints, zoom_start=zoom, height=size[0], width=size[1], min_zoom=3, max_zoom=12)
        for data_point in pointDatas:
            folium.Marker(data_point[0], popup=data_point[1]).add_to(m)
        m.save(resource_path("data/map_location.html"))

        # Root
        tk.Grid.rowconfigure(root, 0, weight=1)
        tk.Grid.columnconfigure(root, 0, weight=1)

        # MainFrame
        tk.Frame.__init__(self, root)

        # BrowserFrame
        tk.Grid.rowconfigure(self, 1, weight=1)
        tk.Grid.columnconfigure(self, 0, weight=1)

        # Pack MainFrame
        self.browser_frame = BrowserFrame(self, self.navigation_bar)
        self.browser_frame.grid(row=1, column=0,
                                sticky=(tk.N + tk.S + tk.E + tk.W))

    def on_root_configure(self, _):
        if self.browser_frame:
            self.browser_frame.on_root_configure()

    def on_configure(self, event):
        if self.browser_frame:
            width = event.width
            height = event.height
            if self.navigation_bar:
                height = height - self.navigation_bar.winfo_height()
            self.browser_frame.on_mainframe_configure(width, height)

    def on_focus_in(self, _):
        self.browser.SetFocus(False)

    def on_focus_out(self, _):
        self.browser.SetFocus(False)

    def on_close(self):
        if self.browser_frame:
            self.browser_frame.on_root_close()
        self.master.destroy()

    def get_browser(self):
        if self.browser_frame:
            return self.browser_frame.browser
        return None

    def get_browser_frame(self):
        if self.browser_frame:
            return self.browser_frame
        return None

    def onClose(self):
        cef.Shutdown()

This is the Browser code

class BrowserFrame(tk.Frame):

    def __init__(self, master, navigation_bar=None):
        self.navigation_bar = navigation_bar
        cef.Initialize()
        self.closing = False
        self.browser = None
        tk.Frame.__init__(self, master)
        self.bind("<FocusIn>", self.on_focus_in)
        self.bind("<FocusOut>", self.on_focus_out)
        self.bind("<Configure>", self.on_configure)
        self.focus_set()

    def embed_browser(self):
        window_info = cef.WindowInfo()
        rect = [0, 0, self.winfo_width(), self.winfo_height()]
        window_info.SetAsChild(self.get_window_handle(), rect)
        self.browser = cef.CreateBrowserSync(window_info,
                                             url=f"file:///{os.getcwd()}/data/map_location.html") #todo
        assert self.browser
        self.browser.SetClientHandler(LoadHandler(self))
        self.browser.SetClientHandler(FocusHandler(self))
        self.message_loop_work()

    def get_window_handle(self):
        if self.winfo_id() > 0:
            return self.winfo_id()
        elif MAC:
            from AppKit import NSApp
            import objc
            return objc.pyobjc_id(NSApp.windows()[-1].contentView())
        else:
            raise Exception("Couldn't obtain window handle")

    def message_loop_work(self):
        cef.MessageLoopWork()
        self.after(10, self.message_loop_work)

    def on_configure(self, _):
        if not self.browser:
            self.embed_browser()

    def on_root_configure(self):
        # Root <Configure> event will be called when top window is moved
        if self.browser:
            self.browser.NotifyMoveOrResizeStarted()

    def on_mainframe_configure(self, width, height):
        if self.browser:
            if WINDOWS:
                ctypes.windll.user32.SetWindowPos(
                    self.browser.GetWindowHandle(), 0,
                    0, 0, width, height, 0x0002)
            elif LINUX:
                self.browser.SetBounds(0, 0, width, height)
            self.browser.NotifyMoveOrResizeStarted()

    def on_focus_in(self, _):
        if self.browser:
            self.browser.SetFocus(True)

    def on_focus_out(self, _):
        if self.browser:
            self.browser.SetFocus(False)

    def on_root_close(self):
        if self.browser:
            self.browser.CloseBrowser(True)
            self.clear_browser_references()
        self.destroy()

    def clear_browser_references(self):
        self.browser = None

class LoadHandler(object):

    def __init__(self, browser_frame):
        self.browser_frame = browser_frame

    def OnLoadStart(self, browser, **_):
        if self.browser_frame.master.navigation_bar:
            self.browser_frame.master.navigation_bar.set_url(browser.GetUrl())

class FocusHandler(object):

    def __init__(self, browser_frame):
        self.browser_frame = browser_frame

    def OnTakeFocus(self, next_component, **_):
        self.browser_frame.browser.SetFocus(False)

    def OnSetFocus(self, source, **_):
        self.browser_frame.browser.SetFocus(False)
        return False

    def OnGotFocus(self, **_):
        self.browser_frame.browser.SetFocus(False)

There is no error or debug message, and as I said, this problem only happens on Windows (same env directory and everything).

2

There are 2 best solutions below

0
On

I had to write a small crutch that opens a transparent window for a millisecond, programmatically the cefpython browser focus is reset and it becomes possible to install it on the tk.entry widget.

entrynum = ttk.Entry(self.f_top)
entrynum.pack(side=tk.LEFT)
entrynum.bind('<Button-1>', lambda e, f=entrynum: b1(e, f))
entrynum.focus_set()

def b1(e, f):
    toprs = tk.Toplevel()
    toprs.geometry(f'+{0}+{0}')
    toprs.attributes('-alpha', 0.0)
    tf = ttk.Frame(toprs)
    tf.pack()
    toprs.state('iconic')
    toprs.state('zoomed')
    toprs.attributes("-topmost", True)
    toprs.after(1, lambda: f.focus_set())
    toprs.after(2, lambda: toprs.destroy())
    toprs.mainloop()
0
On

After a few hours, I finally got it to work. With the tk funktion FrameObj.focus_force() you can switch the focus to a given FrameObj. First, I did this with a button but then I found out it is possible to bind to the tk events listed here: https://tcl.tk/man/tcl8.6/TkCmd/bind.htm#M7 In my solution, I bound to the "Enter" event of the FrameObj removing the focus from the browser window. Even better you can bind to the "ButtonPress" of the text element e.g. CtkEntry object

Customtkinter with cefpython3

#  self in this context is the FrameObj

    self.bind("<Enter>", self.enter_frame);

def enter_frame(self, _):
    self.focus_force()

Alternative binding to the text element. As the focus can not be set to the text element directly, the focus must be set on the parent frame element.

    CtkEntryObj.bind("<ButtonPress>", self.click_text_element)

def click_text_element(self, _):
    FrameObject.focus_force()