tkinter uses WebView2 web components

Posted by meltingpotclub on Sat, 25 Dec 2021 21:59:58 +0100

introduction

On the road of innovation and exploration of tkinter, I have written two articles on tkinter's use of browser web components:

  • Use Internet Explorer Application
  • Using minilink

In addition, other methods of realizing web page components have been summarized:

  • TkHtml3 | backward
  • cef | bulky
  • Nested external exe | uncontrollable

It is precisely because of the early technology accumulation and tkinter itself does not support native web page components that various solutions appear one after another.

However, Microsoft's support for IE interface is coming to an end, and Miniblink can't meet the needs of general HTML browsing and display. Therefore, tkinter web page components nested based on WebView2 were born!!!

Write in front

Catch up

According to the comments and feedback, the implementation methods of my other two tkinter web components are no longer applicable to the needs of all tkinter enthusiasts, and the functions are really weak.

Originally, I wanted to rewrite pywebview, but it became a dependency because it was "in a hurry".

And this article will continue to be updated.

I have uploaded the project to PYPI and can install tkwebview2 through pip.

Dependency Library

Python net, which may need to install the VS compiler. The final compilation volume is very small.

Python net, the core library, is not going to be simplified due to time problems.

lazy

This article is very long because it is complex. If you don't want to read it, you can turn to the end to see the introduction to tkwebview2.

Create class

class WebView2(Frame):
    #Note: to use this component, set the__ init__.py to a new file in the same directory
    
    def __init__(self,parent,width:int,height:int,url:str='',**kw):
        '''
        parent::Parent component
        width::width
        height::height
        url::Web page displayed at startup
        '''

Create embedded function

Because pywebview does not provide a handle acquisition method, we need to obtain the window handle through the window title:

enumWindows = ctypes.windll.user32.EnumWindows
enumWindowsProc = ctypes.WINFUNCTYPE(ctypes.c_bool, ctypes.c_int, ctypes.POINTER(ctypes.c_int))
getWindowText = ctypes.windll.user32.GetWindowTextW
getWindowTextLength = ctypes.windll.user32.GetWindowTextLengthW
isWindowVisible = ctypes.windll.user32.IsWindowVisible
SetParent=ctypes.windll.user32.SetParent
MoveWindow=ctypes.windll.user32.MoveWindow
GetWindowLong=ctypes.windll.user32.GetWindowLongA
SetWindowLong=ctypes.windll.user32.SetWindowLongA
def _getAllTitles():
    titles=[]
    def foreach_window(hWnd, lParam):
        if isWindowVisible(hWnd):
            length = getWindowTextLength(hWnd)
            buff = ctypes.create_unicode_buffer(length + 1)
            getWindowText(hWnd, buff, length + 1)
            titles.append((hWnd, buff.value))
        return True
    enumWindows(enumWindowsProc(foreach_window),0)
    return titles
def getWindowsWithTitle(title):
    hWndsAndTitles = _getAllTitles()
    windowObjs = []
    for hWnd, winTitle in hWndsAndTitles:
        if title.upper() in winTitle.upper():
            windowObjs.append(hWnd)
    return windowObjs

Override the binding of pywebview

There are two problems with the window startup of pywebview in tkinter:

  1. Single thread startup prevents tkinter window from running.
  2. Each initialization will activate all browser windows, resulting in redundant windows and affecting dynamic creation.

In this regard, I changed some fragments of the initialization file and rewritten them in bind Py.

Change as follows:

'''
for window in windows:
	windows[-1]._initialize(guilib, multiprocessing, http_server)

if len(windows) > 1:
    t = Thread(target=_create_children, args=(windows[1:],))
    t.start()
'''
#for window in windows:
windows[-1]._initialize(guilib, multiprocessing, http_server)

#if len(windows) > 1:
#    t = Thread(target=_create_children, args=(windows[1:],))
#    t.start()

Start only the latest window.

And:

'''
guilib.create_window(windows[-1])
'''
Thread(target=lambda:guilib.create_window(windows[-1])).start()

Start the window as a thread.

Embed webview

In order to avoid the repetition of the title, we use the handle of the Frame as the window title, which is unique.

In addition, according to the previous experience of embedding WinForms components, the component size can be dynamically bound.

class WebView2(Frame):
    #Note: to use this component, set the__ init__.py to a new file in the same directory
    
    def __init__(self,parent,width:int,height:int,url:str='',**kw):
        Frame.__init__(self,parent,width=width,height=height,**kw)
        self.fid=self.winfo_id()
        self.width=width
        self.height=height
        self.title=str(self.fid)
        self.parent=parent
        if url=='':
            self.web=webview.create_window(self.title,width=width,height=height,frameless=True,text_select=True)
        else:
            self.web=webview.create_window(self.title,url,width=width,height=height,frameless=True,text_select=True)
        webview.start(self.__in_frame)

    def __in_frame(self):
        #Embed WebView2
        wid=getWindowsWithTitle(self.title)
        while wid==[]:
            wid=getWindowsWithTitle(self.title)
        wid=wid[0]
        SetParent(wid,self.fid)
        MoveWindow(wid,0,0,self.width,self.height,True)
        self.wid=wid
        self.__go_bind()

    def __go_bind(self):
        #Bind each item
        self.bind('<Destroy>',lambda event:self.web.destroy())
        self.bind('<Configure>',self.__resize_webview)
    def __resize_webview(self,event):
        MoveWindow(self.wid,0,0,self.winfo_width(),self.winfo_height(),True)

override method

class WebView2(Frame):
    #...
    def get_url(self):
        #Returns the current url. If not, it is null
        return self.web.get_current_url()

    def evaluate_js(self,script):
        #Execute the javascript code and return the final result
        return self.web.evaluate_js(script)

    def load_css(self,css):
        #Load css
        self.web.load_css(css)

    def load_html(self,content,base_uri=None):
        #Load HTML code
        #content=HTML content
        #base_uri = Basic URL, the default is the directory where the program starts
        if base_uri==None:
            self.web.load_html(content)
        else:
            self.web.load_html(content,base_uri)

    def load_url(self,url):
        #Load new URL
        self.web.load_url(url)

    def none(self):
        pass

be accomplished.

Using tkwebview2

Use pip install tkwebview2.

from tkinter import Frame,Tk
from tkwebview2.tkwebview2 import WebView2

if __name__=='__main__':
    root=Tk()
    root.title('pywebview for tkinter test')
    root.geometry('1200x600+5+5')

    frame=WebView2(root,500,500)
    frame.load_html('<h1>hi hi hi</h1>')
    frame.pack(side='left')
    
    frame2=WebView2(root,500,500)
    frame2.load_url('https://smart-space.com.cn/')
    frame2.pack(side='right',fill='x',expand=True)

    root.mainloop()


effect

epilogue

This is the best method I have found for pure embedded WebView2. Of course, you can also explore other methods, such as using WebView2 directly WinForms instead of borrowing the mediation pywebview.

For more extended usage, please refer to the relevant materials and change the WebView2 binding of pywebview.

☀ tkinter innovation ☀

Topics: Python Tkinter