Search code examples
pythonhtmltkinter

Is it possible to render HTML in Tkinter?


I have a map and it is saved as html file.

is there any way i can display it in a application using tkinter?

I saw some answers mentioning tkhtml, but I've found little information about it. If anyone could please give me a light, and a insight of how and where to aim my code...

thank you


Solution

  • Yes, you can both embed HTML and open full webpages (with CSS and javascript even) in tkinter. With the cefpython module you can embed a full-blown Chromium browser in a tk window. Below is a working example of displaying a local HTML file (change the HTML file location at the line commented with #todo)

    Update August 2020: this works with Python 2.7 / 3.4 / 3.5 / 3.6 / 3.7

    Update March 2021 Python 3.8 and 3.9 are now also supported https://github.com/cztomczak/cefpython/releases/tag/v66.1

    # Example of embedding CEF Python browser using Tkinter toolkit.
    # This example has two widgets: a navigation bar and a browser.
    #
    # NOTE: This example often crashes on Mac (Python 2.7, Tk 8.5/8.6)
    #       during initial app loading with such message:
    #       "Segmentation fault: 11". Reported as Issue #309.
    #
    # Tested configurations:
    # - Tk 8.5 on Windows/Mac
    # - Tk 8.6 on Linux
    # - CEF Python v55.3+
    #
    # Known issue on Linux: When typing url, mouse must be over url
    # entry widget otherwise keyboard focus is lost (Issue #255
    # and Issue #284).
    
    from cefpython3 import cefpython as cef
    import ctypes
    try:
        import tkinter as tk
    except ImportError:
        import Tkinter as tk
    import sys
    import os
    import platform
    import logging as _logging
    
    # Fix for PyCharm hints warnings
    WindowUtils = cef.WindowUtils()
    
    # Platforms
    WINDOWS = (platform.system() == "Windows")
    LINUX = (platform.system() == "Linux")
    MAC = (platform.system() == "Darwin")
    
    # Globals
    logger = _logging.getLogger("tkinter_.py")
    
    # Constants
    # Tk 8.5 doesn't support png images
    IMAGE_EXT = ".png" if tk.TkVersion > 8.5 else ".gif"
    
    
    class MainFrame(tk.Frame):
    
        def __init__(self, root):
            self.browser_frame = None
            self.navigation_bar = None
    
            # Root
            root.geometry("900x640")
            tk.Grid.rowconfigure(root, 0, weight=1)
            tk.Grid.columnconfigure(root, 0, weight=1)
    
            # MainFrame
            tk.Frame.__init__(self, root)
            self.master.title("Tkinter example")
            self.master.protocol("WM_DELETE_WINDOW", self.on_close)
            self.master.bind("<Configure>", self.on_root_configure)
            self.setup_icon()
            self.bind("<Configure>", self.on_configure)
            self.bind("<FocusIn>", self.on_focus_in)
            self.bind("<FocusOut>", 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"+IMAGE_EXT)
            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 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("<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="file:///J:\q.htm") #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:
                # On Mac window id is an invalid negative value (Issue #308).
                # This is kind of a dirty hack to get window handle using
                # PyObjC package. If you change structure of windows then you
                # need to do modifications here as well.
                # noinspection PyUnresolvedReferences
                from AppKit import NSApp
                # noinspection PyUnresolvedReferences
                import objc
                # Sometimes there is more than one window, when application
                # didn't close cleanly last time Python displays an NSAlert
                # window asking whether to Reopen that window.
                # noinspection PyUnresolvedReferences
                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, _):
            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:
                self.browser.CloseBrowser(True)
                self.clear_browser_references()
            self.destroy()
    
        def clear_browser_references(self):
            # Clear browser references that you keep anywhere in your
            # code. All references must be cleared for CEF to shutdown cleanly.
            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, **_):
            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()
    
    
    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"+IMAGE_EXT)
            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"+IMAGE_EXT)
            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"+IMAGE_EXT)
            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("<FocusIn>", self.on_url_focus_in)
            self.url_entry.bind("<FocusOut>", self.on_url_focus_out)
            self.url_entry.bind("<Return>", self.on_load_url)
            self.url_entry.bind("<Button-1>", 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)
    
    
    if __name__ == '__main__':
        logger.setLevel(_logging.INFO)
        stream_handler = _logging.StreamHandler()
        formatter = _logging.Formatter("[%(filename)s] %(message)s")
        stream_handler.setFormatter(formatter)
        logger.addHandler(stream_handler)
        logger.info("CEF Python {ver}".format(ver=cef.__version__))
        logger.info("Python {ver} {arch}".format(
                ver=platform.python_version(), arch=platform.architecture()[0]))
        logger.info("Tk {ver}".format(ver=tk.Tcl().eval('info patchlevel')))
        assert cef.__version__ >= "55.3", "CEF Python v55.3+ required to run this"
        sys.excepthook = cef.ExceptHook  # To shutdown all CEF processes on error
        root = tk.Tk()
        app = MainFrame(root)
        # Tk must be initialized before CEF otherwise fatal error (Issue #306)
        cef.Initialize()
        
        app.mainloop()
        cef.Shutdown()