JavaFX WebView与cefpython显示ECharts图对比
概况
JavaFX WebView和cefpython都提供了在桌面程序中显示网页的功能,但是cefpython显示效果更优。其中cefpython基于Chromium Embedded Framework (CEF)提供了更好的网页显示支持,并能嵌入到Qt / GTK / wxPython构建的窗体中。
作者从Echart实例网站(https://gallery.echartsjs.com/)上找了四个例子,分别用JavaFX WebView和cefpython去加载,得到的效果图见对比。两种库显示效果对比如下表:
示例 | 显示对象 | JavaFX WebView | cefpython | 内存 MB (JavaFX WebView : cefpython : chrome) |
---|---|---|---|---|
案例1 | 极坐标柱状图 | 有些效果显示不了,分辨率低 | 正常(同chrome对比) | 620 : 234 : 390 |
案例2 | 城堡 | 不支持WebGL | 正常 | 909 : 715 : 635 |
案例3 | 立体柱状图 | 显示不全 | 正常 | 704 : 240 : 430 |
案例4 | 出租车立体流动热图 | 不显示 | 正常 | 665 : 1681 : 1510 |
从上表可以看出,JavaFX WebView不支持一些显示效果和WebGL,且在有显示时耗费的内除大约时cefpython的1.2~3倍。在所示案例中cefpython都能正常显示效果图,且加载速度远快于JavaFX WebView。需要指出cefpython后端采用c++实现与chrome内存消耗类似。
示例代码
JavaFX WebView
import javafx.application.Application;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.concurrent.Worker;
import javafx.concurrent.Worker.State;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.ScrollPane;
import javafx.scene.layout.StackPane;
import javafx.scene.web.WebEngine;
import javafx.scene.web.WebView;
import javafx.stage.Stage;
// w w w.j av a2s . c o m
public class Main extends Application {
@Override
public void start(final Stage stage) {
stage.setWidth(400);
stage.setHeight(500);
Scene scene = new Scene(new Group());
final WebView browser = new WebView();
final WebEngine webEngine = browser.getEngine();
ScrollPane scrollPane = new ScrollPane(); //ScrollPane scrollPane = new ScrollPane();
scrollPane.setContent(browser);
browser.prefHeightProperty().bind(stage.heightProperty());
browser.prefWidthProperty().bind(stage.widthProperty());
webEngine.getLoadWorker().stateProperty()
.addListener(new ChangeListener<State>() {
@Override
public void changed(ObservableValue ov, State oldState, State newState) {
if (newState == Worker.State.SUCCEEDED) {
stage.setTitle(webEngine.getLocation());
}
}
});
webEngine.load("https://gallery.echartsjs.com/");
scene.setRoot(scrollPane);
stage.setScene(scene);
stage.show();
}
public static void main(String[] args) {
launch(args);
}
}
cefpython
from cefpython3 import cefpython as cef
import ctypes
import os
import platform
import sys
# GLOBALS
PYQT4 = False
PYQT5 = False
PYSIDE = False
PYSIDE2 = True # False
sys.argv.append("pyside2")
if "pyqt4" in sys.argv:
PYQT4 = True
# noinspection PyUnresolvedReferences
from PyQt4.QtGui import *
# noinspection PyUnresolvedReferences
from PyQt4.QtCore import *
elif "pyqt5" in sys.argv:
PYQT5 = True
# noinspection PyUnresolvedReferences
from PyQt5.QtGui import *
# noinspection PyUnresolvedReferences
from PyQt5.QtCore import *
# noinspection PyUnresolvedReferences
from PyQt5.QtWidgets import *
elif "pyside" in sys.argv:
PYSIDE = True
# noinspection PyUnresolvedReferences
import PySide
# noinspection PyUnresolvedReferences
from PySide import QtCore
# noinspection PyUnresolvedReferences
from PySide.QtGui import *
# noinspection PyUnresolvedReferences
from PySide.QtCore import *
elif "pyside2" in sys.argv:
PYSIDE2 = True
# noinspection PyUnresolvedReferences
import PySide2
# noinspection PyUnresolvedReferences
from PySide2 import QtCore
# noinspection PyUnresolvedReferences
from PySide2.QtGui import *
# noinspection PyUnresolvedReferences
from PySide2.QtCore import *
# noinspection PyUnresolvedReferences
from PySide2.QtWidgets import *
else:
print("USAGE:")
print(" qt.py pyqt4")
print(" qt.py pyqt5")
print(" qt.py pyside")
print(" qt.py pyside2")
sys.exit(1)
# Fix for PyCharm hints warnings when using static methods
WindowUtils = cef.WindowUtils()
# Platforms
WINDOWS = (platform.system() == "Windows")
LINUX = (platform.system() == "Linux")
MAC = (platform.system() == "Darwin")
# Configuration
WIDTH = 800
HEIGHT = 600
# OS differences
CefWidgetParent = QWidget
if LINUX and (PYQT4 or PYSIDE):
# noinspection PyUnresolvedReferences
CefWidgetParent = QX11EmbedContainer
def main():
check_versions()
sys.excepthook = cef.ExceptHook # To shutdown all CEF processes on error
settings = {}
if MAC:
# Issue #442 requires enabling message pump on Mac
# in Qt example. Calling cef.DoMessageLoopWork in a timer
# doesn't work anymore.
settings["external_message_pump"] = True
cef.Initialize(settings)
app = CefApplication(sys.argv)
main_window = MainWindow()
main_window.show()
main_window.activateWindow()
main_window.raise_()
app.exec_()
if not cef.GetAppSetting("external_message_pump"):
app.stopTimer()
del main_window # Just to be safe, similarly to "del app"
del app # Must destroy app object before calling Shutdown
cef.Shutdown()
def check_versions():
print("[qt.py] CEF Python {ver}".format(ver=cef.__version__))
print("[qt.py] Python {ver} {arch}".format(
ver=platform.python_version(), arch=platform.architecture()[0]))
if PYQT4 or PYQT5:
print("[qt.py] PyQt {v1} (qt {v2})".format(
v1=PYQT_VERSION_STR, v2=qVersion()))
elif PYSIDE:
print("[qt.py] PySide {v1} (qt {v2})".format(
v1=PySide.__version__, v2=QtCore.__version__))
elif PYSIDE2:
print("[qt.py] PySide2 {v1} (qt {v2})".format(
v1=PySide2.__version__, v2=QtCore.__version__))
# CEF Python version requirement
assert cef.__version__ >= "55.4", "CEF Python v55.4+ required to run this"
class MainWindow(QMainWindow):
def __init__(self):
# noinspection PyArgumentList
super(MainWindow, self).__init__(None)
# Avoids crash when shutting down CEF (issue #360)
if PYSIDE:
self.setAttribute(Qt.WA_DeleteOnClose, True)
self.cef_widget = None
self.navigation_bar = None
if PYQT4:
self.setWindowTitle("PyQt4 example")
elif PYQT5:
self.setWindowTitle("PyQt5 example")
elif PYSIDE:
self.setWindowTitle("PySide example")
elif PYSIDE2:
self.setWindowTitle("PySide2 example")
self.setFocusPolicy(Qt.StrongFocus)
self.setupLayout()
def setupLayout(self):
self.resize(WIDTH, HEIGHT)
self.cef_widget = CefWidget(self)
self.navigation_bar = NavigationBar(self.cef_widget)
layout = QGridLayout()
# noinspection PyArgumentList
layout.addWidget(self.navigation_bar, 0, 0)
# noinspection PyArgumentList
layout.addWidget(self.cef_widget, 1, 0)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(0)
layout.setRowStretch(0, 0)
layout.setRowStretch(1, 1)
# noinspection PyArgumentList
frame = QFrame()
frame.setLayout(layout)
self.setCentralWidget(frame)
if (PYSIDE2 or PYQT5) and WINDOWS:
# On Windows with PyQt5 main window must be shown first
# before CEF browser is embedded, otherwise window is
# not resized and application hangs during resize.
self.show()
# Browser can be embedded only after layout was set up
self.cef_widget.embedBrowser()
if (PYSIDE2 or PYQT5) and LINUX:
# On Linux with PyQt5 the QX11EmbedContainer widget is
# no more available. An equivalent in Qt5 is to create
# a hidden window, embed CEF browser in it and then
# create a container for that hidden window and replace
# cef widget in the layout with the container.
# noinspection PyUnresolvedReferences, PyArgumentList
self.container = QWidget.createWindowContainer(
self.cef_widget.hidden_window, parent=self)
# noinspection PyArgumentList
layout.addWidget(self.container, 1, 0)
def closeEvent(self, event):
# Close browser (force=True) and free CEF reference
if self.cef_widget.browser:
self.cef_widget.browser.CloseBrowser(True)
self.clear_browser_references()
def clear_browser_references(self):
# Clear browser references that you keep anywhere in your
# code. All references must be cleared for CEF to shutdown cleanly.
self.cef_widget.browser = None
class CefWidget(CefWidgetParent):
def __init__(self, parent=None):
# noinspection PyArgumentList
super(CefWidget, self).__init__(parent)
self.parent = parent
self.browser = None
self.hidden_window = None # Required for PyQt5 on Linux
self.show()
def focusInEvent(self, event):
# This event seems to never get called on Linux, as CEF is
# stealing all focus due to Issue #284.
if cef.GetAppSetting("debug"):
print("[qt.py] CefWidget.focusInEvent")
if self.browser:
if WINDOWS:
WindowUtils.OnSetFocus(self.getHandle(), 0, 0, 0)
self.browser.SetFocus(True)
def focusOutEvent(self, event):
# This event seems to never get called on Linux, as CEF is
# stealing all focus due to Issue #284.
if cef.GetAppSetting("debug"):
print("[qt.py] CefWidget.focusOutEvent")
if self.browser:
self.browser.SetFocus(False)
def embedBrowser(self):
if (PYSIDE2 or PYQT5) and LINUX:
# noinspection PyUnresolvedReferences
self.hidden_window = QWindow()
window_info = cef.WindowInfo()
rect = [0, 0, self.width(), self.height()]
window_info.SetAsChild(self.getHandle(), rect)
self.browser = cef.CreateBrowserSync(window_info,
url="https://gallery.echartsjs.com") #file:///F:/programming/python/TileMapBase/TileMapBase-master/tutorial/html/echart-test.html
# url="https://www.google.com/")
self.browser.SetClientHandler(LoadHandler(self.parent.navigation_bar))
self.browser.SetClientHandler(FocusHandler(self))
def getHandle(self):
if self.hidden_window:
# PyQt5 on Linux
return int(self.hidden_window.winId())
try:
# PyQt4 and PyQt5
return int(self.winId())
except:
# PySide:
# | QWidget.winId() returns <PyCObject object at 0x02FD8788>
# | Converting it to int using ctypes.
if sys.version_info[0] == 2:
# Python 2
ctypes.pythonapi.PyCObject_AsVoidPtr.restype = (
ctypes.c_void_p)
ctypes.pythonapi.PyCObject_AsVoidPtr.argtypes = (
[ctypes.py_object])
return ctypes.pythonapi.PyCObject_AsVoidPtr(self.winId())
else:
# Python 3
ctypes.pythonapi.PyCapsule_GetPointer.restype = (
ctypes.c_void_p)
ctypes.pythonapi.PyCapsule_GetPointer.argtypes = (
[ctypes.py_object])
return ctypes.pythonapi.PyCapsule_GetPointer(
self.winId(), None)
def moveEvent(self, _):
self.x = 0
self.y = 0
if self.browser:
if WINDOWS:
WindowUtils.OnSize(self.getHandle(), 0, 0, 0)
elif LINUX:
self.browser.SetBounds(self.x, self.y,
self.width(), self.height())
self.browser.NotifyMoveOrResizeStarted()
def resizeEvent(self, event):
size = event.size()
if self.browser:
if WINDOWS:
WindowUtils.OnSize(self.getHandle(), 0, 0, 0)
elif LINUX:
self.browser.SetBounds(self.x, self.y,
size.width(), size.height())
self.browser.NotifyMoveOrResizeStarted()
class CefApplication(QApplication):
def __init__(self, args):
super(CefApplication, self).__init__(args)
if not cef.GetAppSetting("external_message_pump"):
self.timer = self.createTimer()
self.setupIcon()
def createTimer(self):
timer = QTimer()
# noinspection PyUnresolvedReferences
timer.timeout.connect(self.onTimer)
timer.start(10)
return timer
def onTimer(self):
cef.MessageLoopWork()
def stopTimer(self):
# Stop the timer after Qt's message loop has ended
self.timer.stop()
def setupIcon(self):
icon_file = os.path.join(os.path.abspath(os.path.dirname(__file__)),
"resources", "{0}.png".format(sys.argv[1]))
if os.path.exists(icon_file):
self.setWindowIcon(QIcon(icon_file))
class LoadHandler(object):
def __init__(self, navigation_bar):
self.initial_app_loading = True
self.navigation_bar = navigation_bar
def OnLoadingStateChange(self, **_):
self.navigation_bar.updateState()
def OnLoadStart(self, browser, **_):
self.navigation_bar.url.setText(browser.GetUrl())
if self.initial_app_loading:
self.navigation_bar.cef_widget.setFocus()
# Temporary fix no. 2 for focus issue on Linux (Issue #284)
if LINUX:
print("[qt.py] LoadHandler.OnLoadStart:"
" keyboard focus fix no. 2 (Issue #284)")
browser.SetFocus(True)
self.initial_app_loading = False
class FocusHandler(object):
def __init__(self, cef_widget):
self.cef_widget = cef_widget
def OnTakeFocus(self, **_):
if cef.GetAppSetting("debug"):
print("[qt.py] FocusHandler.OnTakeFocus")
def OnSetFocus(self, **_):
if cef.GetAppSetting("debug"):
print("[qt.py] FocusHandler.OnSetFocus")
def OnGotFocus(self, browser, **_):
if cef.GetAppSetting("debug"):
print("[qt.py] FocusHandler.OnGotFocus")
self.cef_widget.setFocus()
# Temporary fix no. 1 for focus issues on Linux (Issue #284)
if LINUX:
browser.SetFocus(True)
class NavigationBar(QFrame):
def __init__(self, cef_widget):
# noinspection PyArgumentList
super(NavigationBar, self).__init__()
self.cef_widget = cef_widget
# Init layout
layout = QGridLayout()
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(0)
# Back button
self.back = self.createButton("back")
# noinspection PyUnresolvedReferences
self.back.clicked.connect(self.onBack)
# noinspection PyArgumentList
layout.addWidget(self.back, 0, 0)
# Forward button
self.forward = self.createButton("forward")
# noinspection PyUnresolvedReferences
self.forward.clicked.connect(self.onForward)
# noinspection PyArgumentList
layout.addWidget(self.forward, 0, 1)
# Reload button
self.reload = self.createButton("reload")
# noinspection PyUnresolvedReferences
self.reload.clicked.connect(self.onReload)
# noinspection PyArgumentList
layout.addWidget(self.reload, 0, 2)
# Url input
self.url = QLineEdit("")
# noinspection PyUnresolvedReferences
self.url.returnPressed.connect(self.onGoUrl)
# noinspection PyArgumentList
layout.addWidget(self.url, 0, 3)
# Layout
self.setLayout(layout)
self.updateState()
def onBack(self):
if self.cef_widget.browser:
self.cef_widget.browser.GoBack()
def onForward(self):
if self.cef_widget.browser:
self.cef_widget.browser.GoForward()
def onReload(self):
if self.cef_widget.browser:
self.cef_widget.browser.Reload()
def onGoUrl(self):
if self.cef_widget.browser:
self.cef_widget.browser.LoadUrl(self.url.text())
def updateState(self):
browser = self.cef_widget.browser
if not browser:
self.back.setEnabled(False)
self.forward.setEnabled(False)
self.reload.setEnabled(False)
self.url.setEnabled(False)
return
self.back.setEnabled(browser.CanGoBack())
self.forward.setEnabled(browser.CanGoForward())
self.reload.setEnabled(True)
self.url.setEnabled(True)
self.url.setText(browser.GetUrl())
def createButton(self, name):
resources = os.path.join(os.path.abspath(os.path.dirname(__file__)),
"resources")
pixmap = QPixmap(os.path.join(resources, "{0}.png".format(name)))
icon = QIcon(pixmap)
button = QPushButton()
button.setIcon(icon)
button.setIconSize(pixmap.rect().size())
return button
if __name__ == '__main__':
main()
对比
案例1
JavaFX WebView 显示
显示不完整,且分辨率较差
内存使用量620MB
cefpython 显示
内存使用量约234MB
案例2
JavaFX WebView 显示
cefpython 显示
案例3
JavaFX WebView 显示
cefpython 显示
案例4
JavaFX WebView 显示
cefpython 显示
chrome 显示
此外JavaFX WebView的加载速度远慢于cefpython