背景

web在桌面端的体现一直在演变,从nw 到electron,到现如今有很多现成客户端框架。
大都架构是web内核+服务端语言。

例如:

  • electron=web核+nodejs
  • pywebview=web核 加 python
  • tauri=web核 加 rust
  • webview =web核 加 c++/go

只有找到适宜以后业务的框架即可。

简介

本文次要介绍在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 handlerbridge.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.user32gdi32 = ctypes.windll.gdi32dc = 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"

    查看装置门路是否有中文

    C参考了原文

    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 $R1lbl_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_donelbl_skip:  # 用 CharNextA 失去下一个字符的地址 (可正确处理双字节字符)。  System::Call "user32::CharNextA(pR2)p.R2"  Goto lbl_looplbl_done:  System::Free $R1  Pop  $R4  Pop  $R3  Pop  $R2  Pop  $R1  Exch $R0FunctionEndFunction .onVerifyInstDir  Push $INSTDIR  Call PathIsDBCS_A  Pop $R0   IntCmp $R0 0 lbl_done  Abortlbl_done: FunctionEnd