tkinter使用WebView2网页组件
引言
在对tkinter的一番创新和探索道路上,我自己已经写了两篇关于tkinter使用浏览器网页组件的文章:
- 使用
InternetExplorer.Application
- 使用
miniblink
而且,还总结过其它实现网页组件的方法:
- TkHtml3 | 落后
- cef | 体积庞大
- 嵌套外部exe | 不可控
而正因为前期的技术积累,以及tkinter自身不支持原生网页组件,所以才让各种解决方案相继出现。
但是,微软对IE接口的支持即将“寿终正寝”,Miniblink无法满足通用HTML浏览和显示的需求。所以,基于WebView2嵌套的tkinter网页组件孕育而生!!!
写在前面
赶工
因为根据评论反馈,我另外两篇tkinter网页组件的实现方法已不再是适用于所有tkinter爱好者的需求,而且功能实在是弱爆了。
本来想重写pywebview
,但是因为“赶稿”,就让其成为依赖项了。
而且这篇文章还会继续更新。
我已经将项目上传到PYPI,可通过pip安装tkwebview2
。
依赖库
pythonnet
,这个可能需要安装VS编译器,最终编译体积很小。
pythonnet
,核心库,目前因为时间问题不打算简化。
懒惰
这一篇文章很长,因为比较复杂,如果不想看,可以直接翻到最后查看tkwebview2
的使用介绍。
创建类
class WebView2(Frame):
#说明,若要使用这个组件,请将pywebview的__init__.py改为同目录内新的文件
def __init__(self,parent,width:int,height:int,url:str='',**kw):
'''
parent::父组件
width::宽度
height::高度
url::启动时显示的网页
'''
创建嵌入函数
因为pywebview没有提供句柄获取方法,因此我们需要通过窗口标题获取窗口句柄:
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
重写pywebview的绑定
pywebview的窗口启动在tkinter使用中有两个问题:
- 单线程启动,阻碍tkinter窗口运行。
- 每一次初始化会将所有浏览器窗口激活一遍,造成窗口多余,影响动态创建。
对此,我更改了初始化文件的部分片段,并重新写在了bind.py中。
更改如下:
'''
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()
只启动最新的窗口。
以及:
'''
guilib.create_window(windows[-1])
'''
Thread(target=lambda:guilib.create_window(windows[-1])).start()
以线程启动窗口。
嵌入webview
为了避免标题重复,我们使用Frame的句柄作为窗口标题,具有唯一性。
此外,再根据以往嵌入WinForms组件的经验,动态绑定组件尺寸即可。
class WebView2(Frame):
#说明,若要使用这个组件,请将pywebview的__init__.py改为同目录内新的文件
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):
#嵌入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):
#绑定各个项目
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)
重写方法
class WebView2(Frame):
#...
def get_url(self):
#返回当前url,若果没有则为空
return self.web.get_current_url()
def evaluate_js(self,script):
#执行javascript代码,并返回最终结果
return self.web.evaluate_js(script)
def load_css(self,css):
#加载css
self.web.load_css(css)
def load_html(self,content,base_uri=None):
#加载HTML代码
#content=HTML内容
#base_uri=基本URL,默认为启动程序的目录
if base_uri==None:
self.web.load_html(content)
else:
self.web.load_html(content,base_uri)
def load_url(self,url):
#加载全新的URL
self.web.load_url(url)
def none(self):
pass
大功告成。
使用tkwebview2
使用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()
效果
更新内容
2022-4-10更新:可判断并下载runtime
有些电脑不存在webview2 runtime,也有可能版本过低。对此,tkwebview2提供了两个方法:have_runtime
, install_runtime
。
具体用法如下:
from tkwebview2.tkwebview2 import have_runtime, install_runtime
if not have_runtime():#不存在webview2 runtime或版本过低
install_runtime()#下载并安装runtime
注意,这个操作或暂停主程序运行,直到使用者手动关闭安装指引。
2022-6-11更新:全新创建
本次更新点: 使用pywebview提供的EdgeChrome,而不是直接嵌套窗口
带来的好处:
- tkinter窗口不会失去焦点
- 减少运行消耗
- 不直接使用高gdi渲染
- 可以直接操控WebView2(tkwebview2没有直接提供)
具体见本文章续篇。
结语
这已经是目前我找到最好的纯嵌入式WebView2的方法了,当然,大家还可以探讨其它方法,比如直接使用WebView2.WinForms,而不是借用pywebview
这个中介。
关于更多的拓展用法,请自行参阅相关资料并更改pywebview的WebView2绑定。
☀tkinter创新☀