# Example of embedding CEF Python browser using Tkinter toolkit. # This example has two widgets: a navigation bar and a browser. # Tested with Tk 8.6 and CEF Python v53+. # TODO: url entry loses keyboard focus when mouse hovers over browser (#255) from cefpython3 import cefpython as cef try: import tkinter as tk except ImportError: import Tkinter as tk import sys import os import logging # Globals logger = logging.getLogger() def main(): logger.setLevel(logging.INFO) logger.addHandler(logging.StreamHandler()) logger.info("CEF Python {ver}".format(ver=cef.__version__)) logger.info("Python {ver}".format(ver=sys.version[:6])) logger.info("Tk {ver}".format(ver=tk.TkVersion)) assert cef.__version__ >= "53.1", "CEF Python v53.1+ required to run this" sys.excepthook = cef.ExceptHook # To shutdown all CEF processes on error cef.Initialize() app = MainFrame(tk.Tk()) app.mainloop() cef.Shutdown() class MainFrame(tk.Frame): def __init__(self, master): self.browser_frame = None self.navigation_bar = None # Root master.geometry("800x600") tk.Grid.rowconfigure(master, 0, weight=1) tk.Grid.columnconfigure(master, 0, weight=1) # MainFrame tk.Frame.__init__(self, master) self.master.title("Tkinter example") self.master.protocol("WM_DELETE_WINDOW", self.on_close) self.master.bind("", self.on_root_configure) self.setup_icon() self.bind("", self.on_configure) self.bind("", self.on_focus_in) self.bind("", self.on_focus_out) # NavigationBar self.navigation_bar = NavigationBar(self) self.navigation_bar.grid(row=0, column=0, sticky=(tk.N + tk.S + tk.E + tk.W)) tk.Grid.rowconfigure(self, 0, weight=0) tk.Grid.columnconfigure(self, 0, weight=0) # BrowserFrame 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)) tk.Grid.rowconfigure(self, 1, weight=1) tk.Grid.columnconfigure(self, 0, weight=1) # Pack MainFrame self.pack(fill=tk.BOTH, expand=tk.YES) def on_root_configure(self, _): logger.debug("MainFrame.on_root_configure") if self.browser_frame: self.browser_frame.on_root_configure() def on_configure(self, event): logger.debug("MainFrame.on_configure") 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, _): logger.debug("MainFrame.on_focus_in") def on_focus_out(self, _): logger.debug("MainFrame.on_focus_out") 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 setup_icon(self): resources = os.path.join(os.path.dirname(__file__), "resources") icon_path = os.path.join(resources, "tkinter.png") if os.path.exists(icon_path): self.icon = tk.PhotoImage(file=icon_path) # noinspection PyProtectedMember self.master.call("wm", "iconphoto", self.master._w, self.icon) class NavigationBar(tk.Frame): def __init__(self, master): self.back_state = tk.NONE self.forward_state = tk.NONE self.back_image = None self.forward_image = None self.reload_image = None tk.Frame.__init__(self, master) resources = os.path.join(os.path.dirname(__file__), "resources") # Back button back_png = os.path.join(resources, "back.png") if os.path.exists(back_png): self.back_image = tk.PhotoImage(file=back_png) self.back_button = tk.Button(self, image=self.back_image, command=self.go_back) self.back_button.grid(row=0, column=0) # Forward button forward_png = os.path.join(resources, "forward.png") if os.path.exists(forward_png): self.forward_image = tk.PhotoImage(file=forward_png) self.forward_button = tk.Button(self, image=self.forward_image, command=self.go_forward) self.forward_button.grid(row=0, column=1) # Reload button reload_png = os.path.join(resources, "reload.png") if os.path.exists(reload_png): self.reload_image = tk.PhotoImage(file=reload_png) self.reload_button = tk.Button(self, image=self.reload_image, command=self.reload) self.reload_button.grid(row=0, column=2) # Url entry self.url_entry = tk.Entry(self) self.url_entry.bind("", self.on_url_focus_in) self.url_entry.bind("", self.on_url_focus_out) self.url_entry.bind("", self.on_load_url) self.url_entry.bind("", self.on_button1) self.url_entry.grid(row=0, column=3, sticky=(tk.N + tk.S + tk.E + tk.W)) tk.Grid.rowconfigure(self, 0, weight=100) tk.Grid.columnconfigure(self, 3, weight=100) # Update state of buttons self.update_state() def go_back(self): if self.master.get_browser(): self.master.get_browser().GoBack() def go_forward(self): if self.master.get_browser(): self.master.get_browser().GoForward() def reload(self): if self.master.get_browser(): self.master.get_browser().Reload() def set_url(self, url): self.url_entry.delete(0, tk.END) self.url_entry.insert(0, url) def on_url_focus_in(self, _): logger.debug("NavigationBar.on_url_focus_in") def on_url_focus_out(self, _): logger.debug("NavigationBar.on_url_focus_out") def on_load_url(self, _): if self.master.get_browser(): self.master.get_browser().StopLoad() self.master.get_browser().LoadUrl(self.url_entry.get()) def on_button1(self, _): """Fix CEF focus issues (#255). See also FocusHandler.OnGotFocus.""" logger.debug("NavigationBar.on_button1") self.master.master.focus_force() def update_state(self): browser = self.master.get_browser() if not browser: if self.back_state != tk.DISABLED: self.back_button.config(state=tk.DISABLED) self.back_state = tk.DISABLED if self.forward_state != tk.DISABLED: self.forward_button.config(state=tk.DISABLED) self.forward_state = tk.DISABLED self.after(100, self.update_state) return if browser.CanGoBack(): if self.back_state != tk.NORMAL: self.back_button.config(state=tk.NORMAL) self.back_state = tk.NORMAL else: if self.back_state != tk.DISABLED: self.back_button.config(state=tk.DISABLED) self.back_state = tk.DISABLED if browser.CanGoForward(): if self.forward_state != tk.NORMAL: self.forward_button.config(state=tk.NORMAL) self.forward_state = tk.NORMAL else: if self.forward_state != tk.DISABLED: self.forward_button.config(state=tk.DISABLED) self.forward_state = tk.DISABLED self.after(100, self.update_state) class Tabs(tk.Frame): def __init__(self): tk.Frame.__init__(self) # TODO: implement tabs class BrowserFrame(tk.Frame): def __init__(self, master, navigation_bar=None): self.navigation_bar = navigation_bar self.closing = False self.browser = None tk.Frame.__init__(self, master) self.bind("", self.on_focus_in) self.bind("", self.on_focus_out) self.bind("", self.on_configure) self.focus_set() def embed_browser(self): window_info = cef.WindowInfo() window_info.SetAsChild(self.winfo_id()) self.browser = cef.CreateBrowserSync(window_info, url="https://www.google.com/") self.browser.SetClientHandler(LoadHandler(self)) # FocusHandler requires cefpython 53.2+ if cef.__version__ >= "53.2": self.browser.SetClientHandler(FocusHandler(self)) self.message_loop_work() 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 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: self.browser.SetBounds(0, 0, width, height) self.browser.NotifyMoveOrResizeStarted() def on_focus_in(self, _): logger.debug("BrowserFrame.on_focus_in") if self.browser: self.browser.SetFocus(True) def on_focus_out(self, _): logger.debug("BrowserFrame.on_focus_out") if self.browser: self.browser.SetFocus(False) def on_root_close(self): if self.browser: # Close browser and free reference by setting to None self.browser.CloseBrowser(True) self.browser = None self.destroy() 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): """FocusHandler is available in CEF Python 53.2 and higher.""" def __init__(self, browser_frame): self.browser_frame = browser_frame def OnTakeFocus(self, _, next_component): logger.debug("FocusHandler.OnTakeFocus, next={next}" .format(next=next_component)) def OnSetFocus(self, _, source): logger.debug("FocusHandler.OnSetFocus, source={source}" .format(source=source)) return False def OnGotFocus(self, _): """Fix CEF focus issues (#255). Call browser frame's focus_set to get rid of type cursor in url entry widget.""" logger.debug("FocusHandler.OnGotFocus") self.browser_frame.focus_set() if __name__ == '__main__': main()