背景
web在桌面端的表现不断在演变,从nw 到electron,到现如今有很多现成客户端框架。
大都架构是web内核+服务端语言。
例如:
- electron=web核+nodejs
- pywebview=web核 加 python
- tauri=web核 加 rust
- webview =web核 加 c++/go
- neutralinojs=web核 c扩展js做系统调用 + 无服务端语言,可自选
只要找到适合当前业务的框架即可。
简介
本文主要介绍在window环境下pywebview 3.x 使用的一些注意事项。
pywbview 官方文档
安装
.NET>4.0
- (一个和windows资源相关的调用库)
- 如果没有.NET>4.0的需要安装。一般win10都自带
pythonnet
- (一个python调.NET的库)
- 需要先安装 pythonnet 库
WebView2
- (一个Edge的内核)
- 下载
至于如何检查和让用户安装,下面会说明。
编码与踩坑
配置
pywebview支持很多web内核。如果不指定web内核,pywebview会自动选择,比如啥浏览器都没装的可能会用IE11渲染。所以我们指定内核(WebView2)如下
webview.start(gui='edgechromium',private_mode=True)
其中private_mode=True
则开启浏览器缓存(localStorage等)
自实现拖拽拉伸窗体
拖拽:
//js mousemove handler bridge.move(e.screenX, e.screenY) //实际调用 window.pywebview?.api.move(left,top);
拉伸:
- 不推荐,未解决windows缩放分辨率时窗体右边有一条缝隙。
综上,尽可能用自带的。
用户环境检查webview2与安装
参考了tkwebview2
def have_runtime():#检测是否含有webview2 runtime
from webview.platforms.winforms import _is_chromium
return _is_chromium()
def install_runtime():#安装webview2 runtime
#https://go.microsoft.com/fwlink/p/?LinkId=2124703
from urllib import request
import subprocess
import os
url=r'https://go.microsoft.com/fwlink/p/?LinkId=2124703'
path=os.getcwd()+'\\webview2runtimesetup.exe'
unit=request.urlopen(url).read()
with open(path,mode='wb') as uf:
uf.write(unit)
cmd=path
p=subprocess.Popen(cmd,shell=True)
return_code=p.wait()#等待子进程结束
os.remove(path)
return return_code
管理员与注册表
解决以下兼容问题:
- 解决Renderer Code Integrity造成Chrome浏览器崩溃
- 解决content type编码问题导致html无法被浏览器解析,页面加载不出
解决非管理员权限打开时,以管理员权限重启自身
def check_reg(): try: # https://zhuanlan.zhihu.com/p/400960997 ok=winreg.CreateKeyEx(winreg.HKEY_LOCAL_MACHINE,r'SOFTWARE\Policies\Microsoft\Edge\WebView2',reserved=0,access=winreg.KEY_WRITE) winreg.SetValueEx(ok,'RendererCodeIntegrityEnabled',0,winreg.REG_DWORD,0) winreg.CloseKey(ok) # https://blog.csdn.net/weixin_46099269/article/details/113185882 ok=winreg.CreateKeyEx(winreg.HKEY_CLASSES_ROOT,r'.js',reserved=0,access=winreg.KEY_WRITE) winreg.SetValueEx(ok,'Content Type',0,winreg.REG_SZ,'text/javascript') winreg.CloseKey(ok) except PermissionError: ctypes.windll.shell32.ShellExecuteW(None, "runas", sys.executable, __file__, None, 1) exit()
端口占用
解决上一个flask服务一直占用端口
def kill_process(port): r = os.popen("netstat -ano | findstr "+str(port)) text = r.read() arr=text.split("\n") print("进程个数为:",len(arr)-1) for text0 in arr: arr2=text0.split(" ") if len(arr2)>1: pid=arr2[len(arr2)-1] if pid!="0": os.system("taskkill /PID "+pid+" /T /F") print(pid) r.close()
也可直接用随机端口
服务端
性能
- flask自带的server在请求时是一个个资源返回的,所以要使用 waitress代替flask自带的server。它会起多个线程来监听端口。
阻塞
- 服务端单独开线程
def start(): global PORT,DEBUG PORT=5000 if DEBUG else randint(3333,9999) kill_process(PORT) check_reg() if not have_runtime():#不存在webview2 runtime或版本过低 install_runtime()#下载并安装runtime server_thread = Process(target=start_server, daemon=True,kwargs={'port':PORT,'debug':DEBUG}) server_thread.start() time.sleep(1) start_gui(port=PORT,debug=DEBUG)
其他
- 常见的跨域等安全问题flask都有成熟的解决方案,这里不展开。
dpi 显示设置
windows高分屏的显示设置可能>100%,此时界面可能模糊,或者计算像素有问题
user32 = ctypes.windll.user32
gdi32 = ctypes.windll.gdi32
dc = user32.GetDC(None)
width = gdi32.GetDeviceCaps(dc, 118) # 原始分辨率的宽度
# 最终宽高
dpi_width=int(width*0.6)
dpi_height=int(dpi_width/1202*802)
打包
打包流程
使用nuitka先打包成解压后的文件夹
python -m nuitka --enable-console --standalone --windows-icon-from-ico=public/logo.ico --include-data-dir=backend/www=www --include-data-file=assets/*.dll=assets/ --follow-imports main.py
使用NSIS打包成安装包
{ "build:setup.exe": "\"C:\\Program Files (x86)\\NSIS\\makensis.exe\" setup.nsi" }
注意事项
路径问题
用nuitka将文件夹打包进去使用include-data-dir命令。dll不会被包含,需要用include-data-file命令。
此时 python 获取相对路径如下:dir=os.path.join(os.path.dirname(__file__), 'assets')
杀死当前进程并安装
nsi配置
nsExec::Exec "taskkill /im main.exe /f"
检查安装路径是否有中文
参考了原文
Function PathIsDBCS_A Exch $R0 Push $R1 Push $R2 Push $R3 Push $R4 System::Call "*(&m${NSIS_MAX_STRLEN}R0)p.R1" StrCpy $R0 0 StrCpy $R2 $R1 lbl_loop: # ANSI 版取 1 个字节长度的字符,字符串遇到 0 字符表示结束了。 System::Call "*$R2(&i1.R3)" IntCmp $R3 0 lbl_done # ANSI 字符用 IsDBCSLeadByte 判断是否双字节字符的前导字节。 System::Call "kernel32::IsDBCSLeadByte(iR3)i.R4" IntCmp $R4 0 lbl_skip IntOp $R0 $R0 ! Goto lbl_done lbl_skip: # 用 CharNextA 得到下一个字符的地址 (可正确处理双字节字符)。 System::Call "user32::CharNextA(pR2)p.R2" Goto lbl_loop lbl_done: System::Free $R1 Pop $R4 Pop $R3 Pop $R2 Pop $R1 Exch $R0 FunctionEnd Function .onVerifyInstDir Push $INSTDIR Call PathIsDBCS_A Pop $R0 IntCmp $R0 0 lbl_done Abort lbl_done: FunctionEnd
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。