美文网首页
Python GUI: cefpython3的简单分析和应用

Python GUI: cefpython3的简单分析和应用

作者: stonejianbu | 来源:发表于2019-11-01 23:02 被阅读0次

    内容概要:
    一、cefpython3的浅显理解:cefpython3是什么? 为什么要使用它? 如何使用?
    二、cefpython3的简单应用:安装使用,首先创建浏览器,然后控制浏览器,再将其嵌入到其他GUI框架。
    三、Pyinstaller打包cefpython3应用:首先理解Pyinstaller的是什么,为什么和怎么使用,然后以遇到问题解决问题的方式打包应用。
    四、总结:对文章作简要总结和说明。

    一、cefpython3的浅显理解

    1.1 是什么?

    cefpython3其上游是C++开发的CEF(基于webkit、V8),CEF即(Chromium Embedder Framework),是基于Google Chromium项目的开源Web browser控件。

    cefpython3可应用于HTML5界面软件的开发,或将其嵌入到其他GUI框架,如PyQt、wxWidgets等;或应用于web自动化测试、爬虫等,如requests、selenium等。

    1.2 为什么?

    cefpython3是随着cef而出现,Cpython性能不如C++,但是生成同样的应用,可能有时候我们会更倾向于更简化且易于理解的接口。

    cefpython3并未全部实现CEF(C++)所有接口(50%左右),实现的有些接口也是待优化的状态,cefpython3虽并非一个成熟的项目,但是其也有可取之处:
    1. 了解实现浏览器功能的过程,助于开发安全的web应用
    2. 通过控制浏览器,可以做一些关于web的自动化任务
    3. 将其嵌入到其他框架开发,减少项目开发周期
    4. 制作HTML5界面软件等

    1.3 怎么样?

    如何应用的前提是我们首先要知道它有什么?可查看github文档API categories
    以对项目提供的API有个整体的把握!下面将其划分为五个部分来介绍:
    1. 安装和使用
    2. 创建浏览器
    3. 控制浏览器
    4. 嵌入GUI框架
    5. cefpython3应用编译链接为shell可执行文件

    二、cefpython3的简单应用

    以下内容只在Windows系统中测试通过,并不保证在Linux/Mac正常执行;为了让读者进一步学习和理解,在例子中给出了github文档中相关的链接。

    2.1 安装和使用

    2.1.1 安装
    # 或你应该先创建一个虚拟环境env
    pip install --user cefpython3
    
    2.1.2 使用
    # 查看是否能正常运行
    from cefpython3 import cefpython as cef
    print(cef.GetVersion())  
    

    2.2 创建浏览器

    可参考github文档给出的例子hello_world.py

    2.2.1 示例代码
    from cefpython3 import cefpython as cef
    import sys
    
    # 替换python预定义异常处理逻辑,为保证异常发生时能够结束所有进程
    sys.excepthook = cef.ExceptHook  
    
    # 创建浏览器
    cef.Initialize()
    cef.CreateBrowserSync(url="https://www.baidu.com")
    
    # 消息循环:监听信号和处理事件
    cef.MessageLoop()
    
    # 结束进程
    cef.Shutdown()
    
    2.2.2 创建浏览器的示例代码说明
    • 创建应用和配置

    bool cef.Initialize(settings={...},switches={...})

    from cefpython3 import cefpython as cef
    
    settings = {
        "debug": True,  # 调试模式
        "log_severity": cef.LOGSEVERITY_INFO, # 日志的输出级别
        "log_file": "debug.log",  # 设置日志文件
        "user_agent": "from stonejianbu 0.0.1", 
    }
    switches = {
        "enable-media-stream": "",  # 取消获取媒体流(如音频、视频数据),必须以空字符串代表否!
        "proxy-server": "socks5://127.0.0.1:8888",  # 设置代理
        "disable-gpu": "",  # 设置渲染方式CPU or GPU
    }
    
    cef.Initialize(settings=settings, switches=switches)
    

    关于cef应用的更多设置,可参考github文档cefcef-switchescef-settings

    • 加载浏览器和配置参数

    browser cef.CreateBrowserSync(window_info=cef.WindowInfo(), settings={..}, url="..", window_title="..")

    settings={
        "image_load_disabled": True,  # 设置不加载图片
    }
    cef.CreateBrowserSync(url="https://www.baidu.com/",
                              window_title="百度一下",
                              settings=settings)
    

    关于browser的更多设置,可参考github文档browser-settingscef-WindoInfo

    2.3 控制浏览器

    可参考github文档给出例子tutorial.py,但是其例子稍显复杂化,对于beginner来说是有些费解的!

    2.3.1 示例代码
    """
    创建一个浏览器加载`https://www.baidu.com`,然后通过javascript往查询框输入值并点击查询
    """
    from cefpython3 import cefpython as cef
    import sys
    
    
    # 关于浏览器事件的客户端处理器
    class LoadHandler:
        def OnLoadingStateChange(self, browser, is_loading, **_):
            """当前页面加载状态发生变化的时候被调用"""
            print("页面正在加载....")
            if not is_loading:
                print("页面加载完成....")
                browser.ExecuteJavascript(self._jsCode())
                
        # 注意非client Handlers预定义的方法应该与自定义方法区别而在方法名前添加_
        def _jsCode(self):  
            jsCode = """
            // 通过id获取百度输入框元素
            var input_search = document.getElementById('kw');
            // 设置输入框的值为python
            input_search.value = "python";
            // 点击查询
            document.getElementById('su').click();
            """
            return jsCode
    
    
    # 替换python预定义异常处理逻辑,为保证异常发生时能够结束所有进程
    sys.excepthook = cef.ExceptHook
    
    # 创建application: bool cef.Initialize(settings={...},switches={...})
    cef.Initialize()
    
    # 创建browser: browser cef.CreateBrowserSync(window_info=cef.WindowInfo(), settings={..}, url="..", window_title="..")
    browser = cef.CreateBrowserSync(url="https://www.baidu.com")
    
    # 添加关于浏览器事件的客户端处理器: void browser.SetClientHandler(clientHandler object)
    browser.SetClientHandler(LoadHandler())
    
    # 消息循环:监听信号和处理事件
    cef.MessageLoop()
    
    # 结束进程
    cef.Shutdown()
    
    2.3.2 js控制浏览器的示例代码说明
    • 1.捕捉事件并简单响应

    cefpython3中提供了关于Chromium事件通知的接口类Client handlers
    RequestHandlerLoadHandlerRenderHandlerDisplayHandler等等。我们需要做的是重写这些类及其方法,注意不是继承而是重写,然后将重写的类与browser绑定!对于这些client handler类有哪些方法可以重写,这需要查看相关文档!

    """
    添加LoadHandler以捕捉浏览器加载事件,然后提示加载完成!
    """
    from cefpython3 import cefpython as cef
    import sys
    
    # 关于浏览器事件的客户端处理器
    class LoadHandler:
        def OnLoadingStateChange(self, browser, is_loading, **_):
            """当前页面加载状态发生变化的时候被调用"""
            print("页面正在加载....")
            if not is_loading:
                print("页面加载完成....")
    
    
    # 替换python预定义异常处理逻辑,为保证异常发生时能够结束所有进程
    sys.excepthook = cef.ExceptHook
    
    # 创建application: bool cef.Initialize(settings={...},switches={...})
    cef.Initialize()
    
    # 创建browser: browser cef.CreateBrowserSync(window_info=cef.WindowInfo(), settings={..}, url="..", window_title="..")
    browser = cef.CreateBrowserSync(url="https://www.baidu.com")
    
    # 添加关于浏览器事件的客户端处理器: void browser.SetClientHandler(clientHandler object)
    browser.SetClientHandler(LoadHandler())
    
    # 消息循环:监听信号和处理事件
    cef.MessageLoop()
    
    # 结束进程
    cef.Shutdown()
    

    关于client handlers的更多类及其方法的使用,可参考github文档SetGlobalClientHandlerSetClientHandlerLoadHandler等!

    • 2.绑定javaScript来控制浏览器

    browser.ExecuteJavascript(jsCode_str, scriptUrl="", startLine=1)

    javaScript可由浏览器直接解释执行,可用于web页面的动态交互。绑定javaScript来控制浏览器,换言之,使用python代码来将JavaScript代码交由浏览器来解释执行,可参考github文档browser.ExecuteJavascript

    /*javaScript代码如下,通过往百度一下的输入框中输入`python`,然后点击查询*/
    JavaScript_code = """
    // 通过id获取百度输入框元素
    var input_search = document.getElementById('kw');
    // 设置输入框的值为python
    input_search.value = "python";
    // 点击查询
    document.getElementById('su').click();
    """
    browser.ExecuteJavascript(JavaScript_code)
    

    关于python与JavaScript交互的实现,可参考github文档void browser.executeFunction(funcName,params..)JavascriptBindings_object cef.JavascriptBindingsvoid browser.SetJavascriptBindings(JavascriptBindings object)等!

    2.4 嵌入PyQt框架

    在开始介绍之前,我们不妨想一想?如何将两个互不相干的东西结合在一起呢?纽带?桥梁?API?...

    cefpython3和PyQt5虽互为独立,但是它们都是依托于特定的操作系统,如在windows程序中,有各种各样的资源(窗口、图标、光标等),系统在创建这些资源时会为他们分配内存,并返回标示这些资源的标示号,即句柄;由此,我们可以通过以Qt作为主窗口,且预留一个空位置给cef,cef通过获取Qt句柄来显示到空位置上,如此就把它们拼接在了一起!

    可参考github文档给出的示例qt.py,给出的例子用一份代码包含了PyQt4、PyQt5和PySide在linux、windows、Mac运行的测试,由于cef.MessageLoop()和PyQt上的app.exec_()否是循环等待处理,如果使用单线程处理,似乎这是难以实现的(当然可以尝试考虑使用协程、多进程);但是我接下来介绍的是PyQt5在windows上运行的例子,使用了多线程处理multi_threaded_message_loop,了解在不同系统的不同处理Message Loop。 现在思路有了,那么怎么实现呢?我将上面的思路划分为3个步骤:
    1. 将PyQt作为主窗口并预留一个空位置
    2. cef获取PyQt窗口控件的句柄并显示到空位置上
    3. 多线程处理

    2.4.1 示例代码
    """
    将cefpython3嵌入到PyQt5中,往输入框中输入URL地址,点击查询,创建浏览器并加载HTML内容显示
    """
    from PyQt5 import QtWidgets
    from cefpython3 import cefpython as cef
    import sys
    
    # 浏览器内容窗口
    class CefBrowser(QtWidgets.QWidget):
        def __init__(self, parent=None):
            self.browser = None
            super().__init__(parent)
    
        def create_browser(self, window_info, url):
            self.browser = cef.CreateBrowserSync(window_info, url=url)
    
        def embedBrowser(self, url):
            window_info = cef.WindowInfo()
            # void window_info.SetAsChild(int parentWindowHandle, list windowRect), windowRect~[left,top,right,bottom]
            window_info.SetAsChild(int(self.winId()), [0, 0, self.width(), self.height()])
            cef.PostTask(cef.TID_UI, self.create_browser, window_info, url)
    
    # Qt主窗口
    class BrowserWindow:
        def setUI(self, MainWindow):
            MainWindow.resize(800, 600)
            MainWindow.setWindowTitle("cefpython3-PyQt5")
    
            # URL输入框、查询按钮、浏览器控件
            self.le_search = QtWidgets.QLineEdit()
            self.le_search.setPlaceholderText("输入网址...")
            self.btn_search = QtWidgets.QPushButton()
            self.btn_search.setText("查询")
            self.browser_widget = CefBrowser()
    
            # 设置布局方式:栅栏式
            self.main_layout = QtWidgets.QGridLayout(MainWindow)
            self.main_layout.addWidget(self.le_search, 0, 0, 1, 1)
            self.main_layout.addWidget(self.btn_search, 0, 1, 1, 1)
            self.main_layout.addWidget(self.browser_widget, 1, 0, 8, 2)
    
            # 信号和槽函数
            self.signal_slots()
    
        def signal_slots(self):
            # 绑定`查询`按钮的点击事件
            self.btn_search.clicked.connect(self.slot_load_url)
    
        def slot_load_url(self):
            """获取输入框的URL,判断是否已存在browser对象,如果存在则LoadUrl否则开始创建浏览器"""
            if self.le_search.text():
                if self.browser_widget.browser:
                    self.browser_widget.browser.LoadUrl(self.le_search.text())
                else:
                    self.browser_widget.embedBrowser(self.le_search.text())
    
        def show(self):
            """创建和显示应用窗口,循环监听处理"""
            app = QtWidgets.QApplication([])
            widget = QtWidgets.QWidget()
            main_window = BrowserWindow()
            main_window.setUI(widget)
            widget.show()
            app.exec_()
    
    
    if __name__ == "__main__":
        sys.excepthook = cef.ExceptHook
        # bool cef.Initialize(settings={...},switches={...})
        cef.Initialize(settings={"multi_threaded_message_loop": True})
        BrowserWindow().show()
        cef.Shutdown()
    
    2.4.2 嵌入Qt框架的示例代码说明
    • 1.将PyQt作为主窗口并预留一个空位置
    """
    创建简单浏览器窗口:输入框、查询按钮、预留的内容窗口
    这里不对PyQt5的知识点作过多介绍,以下给出的例子尽可能简单
    """
    from PyQt5 import QtWidgets
    
    
    class BrowserWindow:
        def setUI(self, MainWindow):
            # 设置主窗口大小和标题
            MainWindow.resize(800, 600)
            MainWindow.setWindowTitle("cefpython3-PyQt5")
    
            # URL输入框、查询按钮、浏览器控件
            self.le_search = QtWidgets.QLineEdit()
            self.le_search.setPlaceholderText("输入网址...")
            self.btn_search = QtWidgets.QPushButton()
            self.btn_search.setText("查询")
            # 将CefBrowser类实例化作为PyQt的子控件,用来显示HTML页面
            self.browser_widget = CefBrowser()    
    
            # 设置布局方式:栅栏式
            self.main_layout = QtWidgets.QGridLayout(MainWindow)
            self.main_layout.addWidget(self.le_search, 0, 0, 1, 1)
            self.main_layout.addWidget(self.btn_search, 0, 1, 1, 1)
            self.main_layout.addWidget(self.browser_widget, 1, 0, 8, 2)
    
            # 信号和槽函数
            self.signal_slots()
    
        def signal_slots(self):
            # 绑定`查询`按钮的点击事件
            self.btn_search.clicked.connect(self.slot_load_url)
    
        def slot_load_url(self):
            """获取输入框的URL,判断是否已存在browser对象,如果存在则LoadUrl否则开始创建浏览器"""
            if self.le_search.text():
                if self.browser_widget.browser:
                    self.browser_widget.browser.LoadUrl(self.le_search.text())
                else:
                    self.browser_widget.embedBrowser(self.le_search.text())
    
        def show(self):
            """创建和显示应用窗口,循环监听处理"""
            app = QtWidgets.QApplication([])
            widget = QtWidgets.QWidget()
            main_window = BrowserWindow()
            main_window.setUI(widget)
            # 显示主窗口
            widget.show()
            # 循环监听处理事件
            app.exec_()
    
    • 2.cef获取PyQt窗口控件的句柄并显示到空位置上

    void window_info.SetAsChild(int parentWindowHandle, list windowRect)

    首先,PyQt控件的句柄是通过winId()方法获取,CefBrowser继承于QtWidgets.QWidget,则获取其的句柄通过self.winId();用于设置浏览器的显示方式的类WindowInfo

    class CefBrowser(QtWidgets.QWidget):
        def __init__(self, parent=None):
            self.browser = None
            super().__init__(parent)
    
        def create_browser(self, window_info, url):
            self.browser = cef.CreateBrowserSync(window_info, url=url)
    
        def embedBrowser(self, url):
            window_info = cef.WindowInfo()
            # 设置以浏览器以子窗口显示,SetAsChild有两个参数,一个是父窗口的句柄,另一个是设置窗口位置列表[left,right,width,height]
            window_info.SetAsChild(int(self.winId()), [0, 0, self.width(), self.height()])
            
            # 设置以UI线程来创建浏览器,void cef.PostTask(线程,funcName, [params...]),传入funcName函数的参数不能是关键字
            cef.PostTask(cef.TID_UI, self.create_browser, window_info, url)
    
    • 3.Windows系统中设置多线程处理

    关于多线程处理和设置创建浏览器为UI线程的解释,请参考PostTask

    下面直接给出说明,其中需要修改的地方分别是:
    1. 给cef添加设置:cef.Initialize(settings={"multi_threaded_message_loop": True})
    2. 将UI线程作为创建浏览器的线程:cef.PostTask(cef.TID_UI, self.create_browser, window_info, url),如之前说明
    3. 不再需要手动调用:cef.MessageLoop()
    

    三、 cefpython3应用编译链接为shell可执行文件

    下面使用Pyinstaller作为打包工具,将github文档给出的例子hello_world.py作为打包应用的示例。

    3.1 Pyinstaller的基本概念

    3.1.1 是什么?

    Pyinstaller读取你所写的py脚本,它递归分析脚本主程序代码运行所需的模块、库文件以及python解释器,然后将它们都复制到单个目录中或编译为单个可执行文件。

    3.1.2 为什么?

    python脚本只能由python解释器执行,若想让特定系统shell来执行则需要转换为符合特定系统所规定格式,如windows系统下的*.com、*.exe是直接可执行的格式;而python脚本是需要经过编译链接。Pyinstaller作为一个高级的API,可以地轻松这一任务,而我们无需去关注如何编译?如何链接?*.exe具体的格式什么?

    3.1.3 怎么样?

    如何应用?下面将从3个小步骤来说明,以一个最小化单元执行:
    1. 简单分析说明Pyinstaller提供的接口
    2. 使用Pyinstaller开始打包
    3. 解决异常问题
    4. 执行最小化的完整的打包过程

    3.2 Pyinstaller打包cefpython3应用

    3.2.1 安装和查看API
    1. 安装:pip install --user Pyinstaller
    2. 查看帮助信息:Pyinstaller -h
    3. 说明常用参数:-w              无console  
                    -i title.ico    指定图标
                    -F              打包为单个文件
                    --hidden-import module_name   手动添加Pyinstaller无法获取到的必要模块
                    .....
    
    3.2.2 开始使用
    import cefpython3
    import subprocess
    import os
    
    # 获取cefpython3包提供的hello_world.py文件
    hd_file = os.path.dirname(cefpython3.__file__) + "\\examples\\hello_world.py"
    print("*******打包cefpython3应用:", hd_file)
    
    # 相当于执行打包命令: Pyinstaller hello_world.py
    subprocess.run("Pyinstaller {}".format(hd_file))
    print("********打包成功!*******")
    
    # 打包完成后,尝试启动可执行程序hello_world.exe
    print("********开始执行,成功打包的应用:")
    subprocess.run("./dist/hello_world/hello_world.exe")
    # 如果执行后能出现标题为hell_world的弹窗,那恭喜你,否则继续往下看!
    
    3.2.3 解决异常问题

    我将目前出现的错误归结为依赖错误,划分为两种类型的依赖问题:
    1. 隐藏模块问题
    2. 其他依赖问题

      1. 隐藏模块问题:如ModuleNotFoundError: No module named 'json'错误
    # 对于Pyinstaller无法识别的隐藏模块,我们需要手动告诉它,如添加--hidden-import json
    # 在执行以下脚本之前请先删除之前打包完成的文件和目录build、dist、hello_world.spec
    import cefpython3
    import subprocess
    import os
        
    # 获取cefpython3包提供的hello_world.py文件
    hd_file = os.path.dirname(cefpython3.__file__) + "\\examples\\hello_world.py"
    print("*******打包cefpython3应用:", hd_file)
        
    # 相当于执行打包命令: Pyinstaller hello_world.py
    subprocess.run("Pyinstaller --hidden-import json {}".format(hd_file))
    print("********打包成功!*******")
        
    # 打包完成后,尝试执行可执行程序hello_world.exe
    print("********开始执行,成功打包的应用:")
    subprocess.run("./dist/hello_world/hello_world.exe")
    # 如果执行后能出现标题为hell_world!的弹窗,那恭喜你,否则继续往下看!
    
      1. 其他依赖问题

    由于cefpython3并不是由python直接编写而是由C++转换编译,其包含了dll、pak、bin、exe、dat等文件而目前Pyinstaller是不能够自动地将这些文件复制执行文件目录下,而使得可执行文件不能正常调用依赖文件而无法正常启动。如何解决这种问题呢?三个方法如下:
    1. 告诉Pyinstaller让它在打包过程中帮我们把依赖文件复制过来
    2. 找到cefpython3包,手动复制过来一份放到可执行文件的目录
    3. 编写脚本代码将依赖文件复制到可执行文件的目录

    """   
    下面我使用的是第三种方法,配合使用os和shutil模块完成Pyinstaller打包cefpython3应用
    """ 
    import os
    import shutil
            
    # 将一个文件夹中的指定文件复制到另一个文件夹中
    def copytree(src, dst, ignores_suffix_list=None):
        os.makedirs(dst, exist_ok=True)
        names = [os.path.join(src, name) for name in os.listdir(src)]
        for name in names:
            exclude = False
            for suffix in ignores_suffix_list:
                if name.endswith(suffix):
                    exclude = True
                    continue
            if not exclude:
                if os.path.isdir(name):
                    new_dst = os.path.join(dst, os.path.basename(name))
                    shutil.copytree(name, new_dst, ignore=shutil.ignore_patterns(*ignores_suffix_list))
                else:
                    shutil.copy(name, dst)
    
    
    3.2.4 完整打包示例代码
    import cefpython3
    import subprocess
    import os
    import shutil
    
    
    class PyinstallerCefpython:
        def __init__(self):
            self.no_suffix_script_name = "hello_world"
            # cefpython3的包目录
            self.cef_dir = os.path.dirname(cefpython3.__file__)
            # 获取cefpython3包下examples目录下的hello_world.py
            self.script_file = os.path.join(os.path.join(self.cef_dir, "examples"), "hello_world.py")
    
        def delete_before_generates(self):
            """删除之前打包生成的文件"""
            print("*******正在删除之前打包的生成文件....")
            try:
                shutil.rmtree("./dist")
                shutil.rmtree("./build")
                os.remove("{}.spec".format(self.no_suffix_script_name))
            except Exception as e:
                pass
            print("*******删除成功!")
    
        def script_to_exe(self):
            # 相当于执行打包命令: Pyinstaller hello_world.py
            print("*******开始打包cefpython3应用:", self.script_file)
            subprocess.run("Pyinstaller --hidden-import json {}".format(self.script_file))
    
        def copytree(self, src, dst, ignores_suffix_list=None):
            print("********正在复制将{}目录下的文件复制到{}文件夹下....".format(src, dst))
            os.makedirs(dst, exist_ok=True)
            names = [os.path.join(src, name) for name in os.listdir(src)]
            for name in names:
                exclude = False
                for suffix in ignores_suffix_list:
                    if name.endswith(suffix):
                        exclude = True
                        continue
                if not exclude:
                    if os.path.isdir(name):
                        new_dst = os.path.join(dst, os.path.basename(name))
                        shutil.copytree(name, new_dst, ignore=shutil.ignore_patterns(*ignores_suffix_list))
                    else:
                        shutil.copy(name, dst)
    
        def solve_dependence(self):
            print("*******解决依赖:复制依赖文件到执行文件的目录下....")
            self.copytree(self.cef_dir, "./dist/{}".format(self.no_suffix_script_name), [".txt", ".py", ".log", "examples", ".pyd", "__"])
    
        def exec_application(self):
            print("*******执行成功打包的应用....")
            subprocess.run("./dist/{0}/{0}.exe".format(self.no_suffix_script_name))
    
        def run(self):
            self.delete_before_generates()
            self.script_to_exe()
            self.solve_dependence()
            self.exec_application()
          
        
    if __name__ == "__main__":
      PyinstallerCefpython().run()
    

    四、简单总结

    本文既是个人知识点的总结,同时也为分享更多人来了解和进行cefpython3应用的开发,以上所作分析是基于易于理解了解使用方向的目的,所有的例子都是可扩展和优化的,比如更多的cef和browser功能设置client handler的更多操作、控制浏览器的复杂操作、简单浏览器的自适应大小Focus问题和编写更通用和包含异常处理的Pyinstaller-cefpython接口等等。

    更多的细节扩展还需要读者自行查看官方文档或评论交流学习。

    相关文章

      网友评论

          本文标题:Python GUI: cefpython3的简单分析和应用

          本文链接:https://www.haomeiwen.com/subject/nhoqbctx.html