2

背景

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

例如:

只要找到适合当前业务的框架即可。

简介

本文主要介绍在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

seasonley
607 声望693 粉丝

一切皆数据