Python+WebKit+HTML开发桌面应用程序

前言

几天前写了一个备份的小工具,用Python写个脚本,也就花个一天的时间搞定,给客户用了一下,对功能很满意,但对界面不满足,想要一个图形界面来管理;
一个备份脚本,有必要整成这样子吗?没办法,谁让是上帝的要求呢,就研究一下;先后找了python的tkinter、pyqt,尝试着画一些界面,虽然功能可以实现,但界面很难看;恰好,在查看pyqt的API文档的时候 ,发现了QWebView组件,据介绍可以实现一个浏览器,并且浏览器中的JS可以与Python代码进行交互。忽然眼前一亮,我不正是我想要的吗!
抽几天时间把工具写完了,目前运行良好;想写篇博客做个记录,也想给需要进行此类开发的朋友做个示例,就把这个工具最核心的内容做了一个DEMO。现在,就把这个小DEMO的开发过程做个说明;

功能简介

这个小DEMO用于显示计算机上的一些信息,显示内容不是主要的,主要为了体现如何用python+HTML开发桌面应用程序。

1.使用HTML开发一个界面;
2.在页面上显示主机信息;
3.实现功能时,体现JS调用Python和Python调用JS;

最终实现的主界面如下:

准备材料

开发工具:Visual Studio Code
开发语言:python 2.7/python 3.7
界面工具包:PyQT4/PyQT5

目录结构


说明:
demoUI.py : python主体程序 ,主要实现窗口创建,加载index.html页面,python与JS的交互;
views/index.html : 主要进行数据展示,及与python交互的js程序

demoUI.py开发

引入的包

from PyQt4 import QtCore
from PyQt4 import QtGui
from PyQt4 import QtWebKit
from PyQt4 import QtNetwork

#处理中文问题
import sys,json
from PyQt4.Qt import QObject
reload(sys) 
sys.setdefaultencoding('utf8')

前四行代码,主要是引入Qt相关的包,这里使用的PyQt4;
后三行代码,主要是为了处理中文问题,可以忽略,但遇到中文乱码的时候,想起来这句话,把它加上就行了

DemoWin类

class DemoWin(QtWebKit.QWebView):

    def __init__(self):
        QtWebKit.QWebView.__init__(self)
        self.resize(800, 600)
        self.setUrl(QtCore.QUrl('views/index.html'))
        self.show()
        mainFrame = self.page().mainFrame()
        winobj = WinObj(mainFrame)
        mainFrame.javaScriptWindowObjectCleared.connect(lambda: mainFrame.addToJavaScriptWindowObject(QtCore.QString('WinObj'), winobj)) 

DemoWin是整个应用的核心类,主要实现窗体创建,关联与JS交互的槽函数等;

class DemoWin(QtWebKit.QWebView):

像标题所说,我们将使用WebKit作为页面展示,所以这里的主页面是以QtWebKit.QWebView作为基类

QtWebKit.QWebView.init(self)
self.resize(800, 600)
self.setUrl(QtCore.QUrl(‘views/index.html’))
self.show()

设置窗口大小为800*600,并且加载index.html进行显示

mainFrame = self.page().mainFrame()

获取网页主框架,按照QT官方文档解释:QWebFrame代表一个网页框架,每一个QWebFrame至少包含一个主框架,使用QWebFrame.mainFrame()获取。
这是进行后续各类操作的基础,类似于JS中只有获取到网页的dom对象,才可以对其中的元素操作一样;

winobj = WinObj(mainFrame)
mainFrame.javaScriptWindowObjectCleared.connect(lambda: mainFrame.addToJavaScriptWindowObject(QtCore.QString(‘WinObj’), winobj)) ##js调用python

这段代码是关键中的关键!
WinObj类:是封装后用于js调用的槽函数类,后续再详细介绍
addToJavaScriptWindowObject类:第一个参数是对象在javascript里的名字, 可以自由命名, 第二个参数是对应的QObject实例指针。 这样在javascript里就可以直接访问WinObj对象拉, 是不是看上去超级简单?但是这个函数的调用时机是有讲究的,按照官方文档的推荐,是需要在javaScriptWindowObjectCleared信号的槽里调用,才有了以上的代码;里面用了lambda表达式,纯粹是为了减少一个槽函数的定义,你如果觉得不好看或不喜欢,完全可以定义一个槽函数;

WinObj类

下面来重点介绍WinObj类,类型定义如下:

class WinObj(QtCore.QObject):
    def __init__(self,mainFrame):
        super(WinObj,self).__init__()
        self.mainFrame = mainFrame

    @QtCore.pyqtSlot(result="QString")
    def getInfo(self):
        import socket,platform
        hostname = socket.gethostname()
        try:
            ip = socket.gethostbyname(hostname)
        except:
            ip = ''
        list_info = platform.uname()
        sys_name = list_info[0] + list_info[2]
        cpu_name = list_info[5]
        dic_info = {"hostname":hostname,"ip":ip,"sys_name":sys_name, \
                    "cpu_name":cpu_name}
        #调用js函数,实现回调
        self.mainFrame.evaluateJavaScript('%s(%s)' % ('onGetInfo',json.dumps(dic_info)))
        return json.dumps(dic_info)

该类封装了JS可直接调用的方法,有一些区别于普通类的地方要注意;

class WinObj(QtCore.QObject):

该类必须继承自QObject,而不能是object。

def __init__(self,mainFrame):
        super(WinObj,self).__init__()
        self.mainFrame = mainFrame

构造函数将mainFrame传进来,主要用于调用js函数(后面将有示例介绍)

    @QtCore.pyqtSlot(result="QString")
    def getInfo(self):
        import socket,platform
        hostname = socket.gethostname()
        ip = socket.gethostbyname(hostname)
        list_info = platform.uname()
        sys_name = list_info[0] + list_info[2]
        cpu_name = list_info[5]
        dic_info = {"hostname":hostname,"ip":ip,"sys_name":sys_name, \
                    "cpu_name":cpu_name}
        #调用js函数,实现回调
        self.mainFrame.evaluateJavaScript('%s(%s)' % ('onGetInfo',json.dumps(dic_info)))
        return json.dumps(dic_info)

getInfo()实现了一个供JS调用的方法,有几下几点要注意:
(1)@QtCore.pyqtSlot(result=”QString”) 用于将python方法转换为供js用的函数,括号里写明数据类型及返回类型;如果没有声明result,则不能返回数据;
(2)self.mainFrame.evaluateJavaScript(‘%s(%s)’ % (‘onGetInfo’,json.dumps(dic_info)))
调用页面中用JS声明的onGetInfo函数(这里仅作一个示例,将查询到的数据进行回调返回)

demoUI.py完整代码下:

#coding=utf-8
'''
Created on 2017年11月3日

@author: Administrator
'''
from PyQt4 import QtCore
from PyQt4 import QtGui
from PyQt4 import QtWebKit
from PyQt4 import QtNetwork

#处理中文问题
import sys,json
from PyQt4.Qt import QObject
reload(sys)  
sys.setdefaultencoding('utf8')

class DemoWin(QtWebKit.QWebView):

    def __init__(self):
        QtWebKit.QWebView.__init__(self)
        self.resize(800, 600)
        self.setUrl(QtCore.QUrl('views/index.html'))
        self.show()
        mainFrame = self.page().mainFrame()
        winobj = WinObj(mainFrame)
        mainFrame.javaScriptWindowObjectCleared.connect(lambda: mainFrame.addToJavaScriptWindowObject(QtCore.QString('WinObj'), winobj)) ##js调用python

class WinObj(QtCore.QObject):
    def __init__(self,mainFrame):
        super(WinObj,self).__init__()
        self.mainFrame = mainFrame

    @QtCore.pyqtSlot(result="QString")
    def getInfo(self):
        import socket,platform
        hostname = socket.gethostname()
        try:
            ip = socket.gethostbyname(hostname)
        except:
            ip = ''
        list_info = platform.uname()
        sys_name = list_info[0] + list_info[2]
        cpu_name = list_info[5]
        dic_info = {"hostname":hostname,"ip":ip,"sys_name":sys_name, \
                    "cpu_name":cpu_name}
        #调用js函数,实现回调
        self.mainFrame.evaluateJavaScript('%s(%s)' % ('onGetInfo',json.dumps(dic_info)))
        return json.dumps(dic_info)

if __name__ == '__main__':
    app = QtGui.QApplication(sys.argv)
    demoWin = DemoWin()
    sys.exit(app.exec_())

index.html

<!DOCTYPE script PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<script type="text/javascript" src='http://www.mobibrw.com/wp-content/uploads/2020/05/vue.min_.1.0.11.js'></script>
</head>
<body>

<div id='app'>
<input type='button' onclick='getInfo()' value="获取机器信息">
<H3 >主机名:{{config.hostname}} </H3>
<H3 >IP:{{config.ip}} </H3>
<H3 >操作系统:{{config.sys_name}} </H3>
<H3 >CPU:{{config.cpu_name}} </H3>
<H3>以下为python回调信息:</H3>
<label id='info'></label>
</div>
</body>
</html>
<script type="text/javascript">
var vm = new Vue({
    el: '#app',
    data:{
        config:{}
    },
    created:function(){
        this.config = JSON.parse(window.WinObj.getInfo())
    }
});

function getInfo() {
    info = JSON.parse(window.WinObj.getInfo())
    alert(info)
}

//python回调的js函数
function onGetInfo(info) {
    document.getElementById('info').innerText=JSON.stringify(info)
}

</script>

Python 3.7/PyQT5下需要安装 pyQt5

$ python3 -m pip install --upgrade pip

$ pip3 install SIP

$ pip3 install pyQt5

$ pip3 install --upgrade PyQt5

$ pip3 install PyQtWebEngine

Python 3.7/PyQT5下的代码(注意,Python 2.7/PyQT4是的交互是同步调用的,但是Python 3.7/PyQT5通过QtWebChannel交互是异步的)如下:

#coding=utf-8
from PyQt5 import QtCore
from PyQt5 import QtGui
from PyQt5 import QtWidgets
from PyQt5 import QtWebEngineWidgets
from PyQt5 import QtNetwork
from PyQt5 import QtWebChannel

#处理中文问题
import sys,json
from PyQt5.Qt import QObject
#reload(sys)  
#sys.setdefaultencoding('utf8')

# 调试窗口配置
# 如果不想自己创建调试窗口,可以使用Chrome连接这个地址进行调试
import os
DEBUG_PORT = '5588'
DEBUG_URL = 'http://127.0.0.1:%s' % DEBUG_PORT
os.environ['QTWEBENGINE_REMOTE_DEBUGGING'] = DEBUG_PORT

def qt_message_handler(mode, context, message):
    if mode == QtCore.QtInfoMsg:
        mode = 'INFO'
    elif mode == QtCore.QtWarningMsg:
        mode = 'WARNING'
    elif mode == QtCore.QtCriticalMsg:
        mode = 'CRITICAL'
    elif mode == QtCore.QtFatalMsg:
        mode = 'FATAL'
    else:
        mode = 'DEBUG'
    print('qt_message_handler: line: %d, func: %s(), file: %s' % (
          context.line, context.function, context.file))
    print('  %s: %s\n' % (mode, message))

QtCore.qInstallMessageHandler(qt_message_handler)

class DemoWin(QtWebEngineWidgets.QWebEngineView):

    def __init__(self):
        QtWebEngineWidgets.QWebEngineView.__init__(self)
        self.setWindowTitle('Web页面中的JavaScript与 QWebEngineView交互例子')
        self.resize(800, 600)
        
        self.settings().setAttribute(QtWebEngineWidgets.QWebEngineSettings.PluginsEnabled, True)
        self.settings().setAttribute(QtWebEngineWidgets.QWebEngineSettings.JavascriptEnabled, True)
        
        url = QtCore.QUrl('file:///' + QtCore.QFileInfo('views/index.html').absoluteFilePath())
        #url = QtCore.QUrl("https://www.baidu.com")
        self.setUrl(url)
        self.show()
        
        self.winobj = WinObj(self.page())
        self.channel = QtWebChannel.QWebChannel()
        self.channel.registerObject("con", self.winobj)
        self.page().setWebChannel(self.channel)
        
        #网页调试窗口
        self.inspector = QtWebEngineWidgets.QWebEngineView()
        self.inspector.setWindowTitle('Web Inspector')
        self.inspector.load(QtCore.QUrl(DEBUG_URL))
        self.loadFinished.connect(self.handleHtmlLoaded)
        
    def handleHtmlLoaded(self, ok):
        if ok:
            self.page().setDevToolsPage(self.inspector.page())
            self.inspector.show()

class WinObj(QtCore.QObject):
    def __init__(self, page):
        super(WinObj,self).__init__()
        self.page = page

    @QtCore.pyqtSlot(result="QString")
    def getInfo(self):
        import socket,platform
        hostname = socket.gethostname()
        try:
            ip = socket.gethostbyname(hostname)
        except:
            ip = ''
        list_info = platform.uname()
        sys_name = list_info[0] + list_info[2]
        cpu_name = list_info[5]
        dic_info = {"hostname":hostname,"ip":ip,"sys_name":sys_name, \
                    "cpu_name":cpu_name}
        #调用js函数,实现回调
        self.page.runJavaScript('%s(%s)' % ('onGetInfo',json.dumps(dic_info)))
        js = json.dumps(dic_info)
        return js

if __name__ == '__main__':
    app = QtWidgets.QApplication(sys.argv)
    demoWin = DemoWin()
    sys.exit(app.exec())

index.html

<!DOCTYPE script PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<script type="text/javascript" src="qwebchannel.js"></script>
<script type="text/javascript" src='http://www.mobibrw.com/wp-content/uploads/2020/05/vue.min_.1.0.11.js'></script>
</head>
<body>
<div id='app'>
<input type='button' onclick='getInfo()' value="获取机器信息">
<H3 >主机名:{{config.hostname}} </H3>
<H3 >IP:{{config.ip}} </H3>
<H3 >操作系统:{{config.sys_name}} </H3>
<H3 >CPU:{{config.cpu_name}} </H3>
<H3>以下为python回调信息:</H3>
<label id='info'></label>
</div>
</body>
</html>

<script type="text/javascript">
    new QWebChannel( qt.webChannelTransport, function(channel) {
            window.con = channel.objects.con;
            window.vm = new Vue({
                el: '#app',
                data: {
                    config:{}
                },
                created:function() {
                    window.con.getInfo(function(info) {
                    window.vm.config = JSON.parse(info);
                });
            }
        });
    });

    function getInfo() {
        window.con.getInfo();
    }

    //python回调的js函数
    function onGetInfo(info) {
        document.getElementById('info').innerText=JSON.stringify(info)
    }
</script>

qwebchannel.js

/****************************************************************************
**
** Copyright (C) 2016 The Qt Company Ltd.
** Copyright (C) 2014 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author Milian Wolff <milian.wolff@kdab.com>
** Contact: https://www.qt.io/licensing/
**
** This file is part of the QtWebChannel module of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:BSD$
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** BSD License Usage
** Alternatively, you may use this file under the terms of the BSD license
** as follows:
**
** "Redistribution and use in source and binary forms, with or without
** modification, are permitted provided that the following conditions are
** met:
**   * Redistributions of source code must retain the above copyright
**     notice, this list of conditions and the following disclaimer.
**   * Redistributions in binary form must reproduce the above copyright
**     notice, this list of conditions and the following disclaimer in
**     the documentation and/or other materials provided with the
**     distribution.
**   * Neither the name of The Qt Company Ltd nor the names of its
**     contributors may be used to endorse or promote products derived
**     from this software without specific prior written permission.
**
**
** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
**
** $QT_END_LICENSE$
**
****************************************************************************/

"use strict";

var QWebChannelMessageTypes = {
    signal: 1,
    propertyUpdate: 2,
    init: 3,
    idle: 4,
    debug: 5,
    invokeMethod: 6,
    connectToSignal: 7,
    disconnectFromSignal: 8,
    setProperty: 9,
    response: 10,
};

var QWebChannel = function(transport, initCallback)
{
    if (typeof transport !== "object" || typeof transport.send !== "function") {
        console.error("The QWebChannel expects a transport object with a send function and onmessage callback property." +
                      " Given is: transport: " + typeof(transport) + ", transport.send: " + typeof(transport.send));
        return;
    }

    var channel = this;
    this.transport = transport;

    this.send = function(data)
    {
        if (typeof(data) !== "string") {
            data = JSON.stringify(data);
        }
        channel.transport.send(data);
    }

    this.transport.onmessage = function(message)
    {
        var data = message.data;
        if (typeof data === "string") {
            data = JSON.parse(data);
        }
        switch (data.type) {
            case QWebChannelMessageTypes.signal:
                channel.handleSignal(data);
                break;
            case QWebChannelMessageTypes.response:
                channel.handleResponse(data);
                break;
            case QWebChannelMessageTypes.propertyUpdate:
                channel.handlePropertyUpdate(data);
                break;
            default:
                console.error("invalid message received:", message.data);
                break;
        }
    }

    this.execCallbacks = {};
    this.execId = 0;
    this.exec = function(data, callback)
    {
        if (!callback) {
            // if no callback is given, send directly
            channel.send(data);
            return;
        }
        if (channel.execId === Number.MAX_VALUE) {
            // wrap
            channel.execId = Number.MIN_VALUE;
        }
        if (data.hasOwnProperty("id")) {
            console.error("Cannot exec message with property id: " + JSON.stringify(data));
            return;
        }
        data.id = channel.execId++;
        channel.execCallbacks[data.id] = callback;
        channel.send(data);
    };

    this.objects = {};

    this.handleSignal = function(message)
    {
        var object = channel.objects[message.object];
        if (object) {
            object.signalEmitted(message.signal, message.args);
        } else {
            console.warn("Unhandled signal: " + message.object + "::" + message.signal);
        }
    }

    this.handleResponse = function(message)
    {
        if (!message.hasOwnProperty("id")) {
            console.error("Invalid response message received: ", JSON.stringify(message));
            return;
        }
        channel.execCallbacks[message.id](message.data);
        delete channel.execCallbacks[message.id];
    }

    this.handlePropertyUpdate = function(message)
    {
        for (var i in message.data) {
            var data = message.data[i];
            var object = channel.objects[data.object];
            if (object) {
                object.propertyUpdate(data.signals, data.properties);
            } else {
                console.warn("Unhandled property update: " + data.object + "::" + data.signal);
            }
        }
        channel.exec({type: QWebChannelMessageTypes.idle});
    }

    this.debug = function(message)
    {
        channel.send({type: QWebChannelMessageTypes.debug, data: message});
    };

    channel.exec({type: QWebChannelMessageTypes.init}, function(data) {
        for (var objectName in data) {
            var object = new QObject(objectName, data[objectName], channel);
        }
        // now unwrap properties, which might reference other registered objects
        for (var objectName in channel.objects) {
            channel.objects[objectName].unwrapProperties();
        }
        if (initCallback) {
            initCallback(channel);
        }
        channel.exec({type: QWebChannelMessageTypes.idle});
    });
};

function QObject(name, data, webChannel)
{
    this.__id__ = name;
    webChannel.objects[name] = this;

    // List of callbacks that get invoked upon signal emission
    this.__objectSignals__ = {};

    // Cache of all properties, updated when a notify signal is emitted
    this.__propertyCache__ = {};

    var object = this;

    // ----------------------------------------------------------------------

    this.unwrapQObject = function(response)
    {
        if (response instanceof Array) {
            // support list of objects
            var ret = new Array(response.length);
            for (var i = 0; i < response.length; ++i) {
                ret[i] = object.unwrapQObject(response[i]);
            }
            return ret;
        }
        if (!response
            || !response["__QObject*__"]
            || response.id === undefined) {
            return response;
        }

        var objectId = response.id;
        if (webChannel.objects[objectId])
            return webChannel.objects[objectId];

        if (!response.data) {
            console.error("Cannot unwrap unknown QObject " + objectId + " without data.");
            return;
        }

        var qObject = new QObject( objectId, response.data, webChannel );
        qObject.destroyed.connect(function() {
            if (webChannel.objects[objectId] === qObject) {
                delete webChannel.objects[objectId];
                // reset the now deleted QObject to an empty {} object
                // just assigning {} though would not have the desired effect, but the
                // below also ensures all external references will see the empty map
                // NOTE: this detour is necessary to workaround QTBUG-40021
                var propertyNames = [];
                for (var propertyName in qObject) {
                    propertyNames.push(propertyName);
                }
                for (var idx in propertyNames) {
                    delete qObject[propertyNames[idx]];
                }
            }
        });
        // here we are already initialized, and thus must directly unwrap the properties
        qObject.unwrapProperties();
        return qObject;
    }

    this.unwrapProperties = function()
    {
        for (var propertyIdx in object.__propertyCache__) {
            object.__propertyCache__[propertyIdx] = object.unwrapQObject(object.__propertyCache__[propertyIdx]);
        }
    }

    function addSignal(signalData, isPropertyNotifySignal)
    {
        var signalName = signalData[0];
        var signalIndex = signalData[1];
        object[signalName] = {
            connect: function(callback) {
                if (typeof(callback) !== "function") {
                    console.error("Bad callback given to connect to signal " + signalName);
                    return;
                }

                object.__objectSignals__[signalIndex] = object.__objectSignals__[signalIndex] || [];
                object.__objectSignals__[signalIndex].push(callback);

                if (!isPropertyNotifySignal && signalName !== "destroyed") {
                    // only required for "pure" signals, handled separately for properties in propertyUpdate
                    // also note that we always get notified about the destroyed signal
                    webChannel.exec({
                        type: QWebChannelMessageTypes.connectToSignal,
                        object: object.__id__,
                        signal: signalIndex
                    });
                }
            },
            disconnect: function(callback) {
                if (typeof(callback) !== "function") {
                    console.error("Bad callback given to disconnect from signal " + signalName);
                    return;
                }
                object.__objectSignals__[signalIndex] = object.__objectSignals__[signalIndex] || [];
                var idx = object.__objectSignals__[signalIndex].indexOf(callback);
                if (idx === -1) {
                    console.error("Cannot find connection of signal " + signalName + " to " + callback.name);
                    return;
                }
                object.__objectSignals__[signalIndex].splice(idx, 1);
                if (!isPropertyNotifySignal && object.__objectSignals__[signalIndex].length === 0) {
                    // only required for "pure" signals, handled separately for properties in propertyUpdate
                    webChannel.exec({
                        type: QWebChannelMessageTypes.disconnectFromSignal,
                        object: object.__id__,
                        signal: signalIndex
                    });
                }
            }
        };
    }

    /**
     * Invokes all callbacks for the given signalname. Also works for property notify callbacks.
     */
    function invokeSignalCallbacks(signalName, signalArgs)
    {
        var connections = object.__objectSignals__[signalName];
        if (connections) {
            connections.forEach(function(callback) {
                callback.apply(callback, signalArgs);
            });
        }
    }

    this.propertyUpdate = function(signals, propertyMap)
    {
        // update property cache
        for (var propertyIndex in propertyMap) {
            var propertyValue = propertyMap[propertyIndex];
            object.__propertyCache__[propertyIndex] = propertyValue;
        }

        for (var signalName in signals) {
            // Invoke all callbacks, as signalEmitted() does not. This ensures the
            // property cache is updated before the callbacks are invoked.
            invokeSignalCallbacks(signalName, signals[signalName]);
        }
    }

    this.signalEmitted = function(signalName, signalArgs)
    {
        invokeSignalCallbacks(signalName, signalArgs);
    }

    function addMethod(methodData)
    {
        var methodName = methodData[0];
        var methodIdx = methodData[1];
        object[methodName] = function() {
            var args = [];
            var callback;
            for (var i = 0; i < arguments.length; ++i) {
                if (typeof arguments[i] === "function")
                    callback = arguments[i];
                else
                    args.push(arguments[i]);
            }

            webChannel.exec({
                "type": QWebChannelMessageTypes.invokeMethod,
                "object": object.__id__,
                "method": methodIdx,
                "args": args
            }, function(response) {
                if (response !== undefined) {
                    var result = object.unwrapQObject(response);
                    if (callback) {
                        (callback)(result);
                    }
                }
            });
        };
    }

    function bindGetterSetter(propertyInfo)
    {
        var propertyIndex = propertyInfo[0];
        var propertyName = propertyInfo[1];
        var notifySignalData = propertyInfo[2];
        // initialize property cache with current value
        // NOTE: if this is an object, it is not directly unwrapped as it might
        // reference other QObject that we do not know yet
        object.__propertyCache__[propertyIndex] = propertyInfo[3];

        if (notifySignalData) {
            if (notifySignalData[0] === 1) {
                // signal name is optimized away, reconstruct the actual name
                notifySignalData[0] = propertyName + "Changed";
            }
            addSignal(notifySignalData, true);
        }

        Object.defineProperty(object, propertyName, {
            configurable: true,
            get: function () {
                var propertyValue = object.__propertyCache__[propertyIndex];
                if (propertyValue === undefined) {
                    // This shouldn't happen
                    console.warn("Undefined value in property cache for property \"" + propertyName + "\" in object " + object.__id__);
                }

                return propertyValue;
            },
            set: function(value) {
                if (value === undefined) {
                    console.warn("Property setter for " + propertyName + " called with undefined value!");
                    return;
                }
                object.__propertyCache__[propertyIndex] = value;
                webChannel.exec({
                    "type": QWebChannelMessageTypes.setProperty,
                    "object": object.__id__,
                    "property": propertyIndex,
                    "value": value
                });
            }
        });

    }

    // ----------------------------------------------------------------------

    data.methods.forEach(addMethod);

    data.properties.forEach(bindGetterSetter);

    data.signals.forEach(function(signal) { addSignal(signal, false); });

    for (var name in data.enums) {
        object[name] = data.enums[name];
    }
}

//required for use with nodejs
if (typeof module === 'object') {
    module.exports = {
        QWebChannel: QWebChannel
    };
}

参考链接


PyQt5出现ImportError cannot import name 'QtWebEngineWidgets' from 'PyQt5' 问题解决

今天想在macOS Catalina(10.15.4)系统上,测试一下Python下的QT界面操作,结果在执行

from PyQt5 import QtWebEngineWidgets

的时候,报告如下错误:

Exception has occurred: ImportError
cannot import name 'QtWebEngineWidgets' from 'PyQt5' (/usr/local/lib/python3.7/site-packages/PyQt5/__init__.py)
  File "demoUI.py", line 10, in <module>
    from PyQt5 import QtWebEngineWidgets

也有可能报告:

No module named PyQt5.QtWebEngineWidgets

查了好久才在Stack Overflow上找到一个回答,说是这个模块被PyQt5移除了,需要单独安装。

安装命令为:

$ pip install PyQtWebEngine

参考链接


Python-PyQt5-PyQtWebEngine采坑记录-- No module named PyQt5.QtWebEngineWidgets

PyQt5出现No module named 'PyQt5.sip'问题解决

今天想在macOS Catalina(10.15.4)系统上,测试一下Python下的QT界面操作,结果在执行

from PyQt5 import QtCore

的时候报告错误:

No module named 'PyQt5.sip'

解决方法如下:

$ python3 -m pip install --upgrade pip

$ pip3 install SIP

$ pip3 install pyQt5

$ pip3 install --upgrade PyQt5

$ pip3 install PyQtWebEngine

参考链接


No module named 'pip._internal.cli.main'

今天,在macOS Catalina (10.15.4)系统上执行升级pip的命令

$ pip3 install --upgrade pip

之后,执行更新命令,报告错误。如下:

$ pip3 install --upgrade PyQt5
Traceback (most recent call last):
  File "/usr/local/bin/pip3", line 5, in <module>
    from pip._internal.cli.main import main
ModuleNotFoundError: No module named 'pip._internal.cli.main'

解决方法为重新升级安装一次 pip,如下:

$ python3 -m pip install --upgrade pip

参考链接


解决 ModuleNotFoundError: No module named 'pip._internal'

pip批量更新过期的python库

今天看了下系统环境,不少python库都有了更新,再用旧版本库可能已经不适合了,就想把所有的库都更新到最新版本。

查看系统里过期的python库,可以用pip命令

$ pip list #列出所有安装的库
$ pip list --outdated #列出所有过期的库

对于列出的过期库,pip也提供了更新的命令

$ pip install --upgrade 库名

但此命令不支持全局全部库升级。

在stackoverflow上有人提供了批量更新的办法,一个循环就搞定(注意--upgrade后面的空格)

import pip
from subprocess import call

for dist in pip.get_installed_distributions():
    call("pip install --upgrade " + dist.project_name, shell=True)

另外的也有人提到用 pip-review ,不想安装就没用

$ pip install pip-review

$ pip-review --local --interactive --auto

参考链接


pip --upgrade批量更新过期的python库

openpyxl - A Python library to read/write Excel 2010 xlsx/xlsm files

$ pip install openpyxl
from openpyxl import Workbook
wb = Workbook()

# grab the active worksheet
ws = wb.active

# Data can be assigned directly to cells
ws['A1'] = 42

# Rows can also be appended
ws.append([1, 2, 3])

# Python types will automatically be converted
import datetime
ws['A2'] = datetime.datetime.now()

# Save the file
wb.save("sample.xlsx")

openpyxl特点

  openpyxl(可读写excel表)专门处理Excel2007及以上版本产生的xlsx文件,xls和xlsx之间转换容易 注意:如果文字编码是“gb2312” 读取后就会显示乱码,请先转成Unicode

继续阅读openpyxl - A Python library to read/write Excel 2010 xlsx/xlsm files