def __init__(self): QWidget.__init__(self, flags=Qt.Widget) # 레이아웃 선언 및 Form Widget에 설정 self.layout_1 = QBoxLayout(QBoxLayout.LeftToRight, self) self.layout_2 = QBoxLayout(QBoxLayout.LeftToRight) self.layout_3 = QBoxLayout(QBoxLayout.TopToBottom) # 부모 레이아웃에 자식 레이아웃을 추가 self.layout_1.addLayout(self.layout_2) self.layout_1.addLayout(self.layout_3) self.web = QWebEngineView() self.pb_1 = QPushButton("요청하기") self.pb_2 = QPushButton("밝기지도") self.pb_3 = QPushButton("길찾기") # self.te_1 = QTextEdit() # Web과 통신할 수 있게 채널링 channel = QWebChannel(self.web.page()) self.web.page().setWebChannel(channel) self.handler = CallHandler() # 반드시 인스턴스 변수로 사용해야 한다. channel.registerObject('handler', self.handler) # js에서 불리울 이름 self.setLayout(self.layout_1) self.init_widget()
def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # 设置窗口标题 # self.setWindowTitle('My Browser') # 设置窗口图标 # self.setWindowIcon(QIcon('icons/penguin.png')) # 设置窗口大小900*600 # self.resize(900, 600) # self.show() # 2 self.view = QWebEngineView() channel = QWebChannel() handler = CallHandler() channel.registerObject('handler', handler) self.view.page().setWebChannel(channel) self.view.loadFinished.connect(self._loadFinish) # 添加浏览器到窗口中 # self.setCentralWidget(self.view) # htmlUrl = 'file:////Users/play/github/Python_Master_Courses/GUI图形界面/pyqt5/pyqt5-javascript-互相传值/js1.html' self.view.load(QUrl(htmlUrl)) self.view.show()
def initUI(self): '''----测试数据----''' # --函数测试通过删除 self.ledit_StartLat.setText(str(46.49)) self.ledit_StartLon.setText(str(6.6)) self.ledit_StopLat.setText(str(46.53)) self.ledit_StopLon.setText(str(6.7)) self.spinBox_zoom.setValue(15) #testWeb=myClass() #self.browser=QWebEngineView() #self.browser=QWebEngineView(self.centralwidget) #self.browser.setObjectName("browser_test") #self.gridLayout.addWidget(self.browser,0,1,2,1) #myChannel=QWebChannel() #self.browser.page().setWebChannel(myChannel) #myChannel.registerObject('testObject',testWeb) #self.browser.load(QUrl('E:///PySpace/tilesMap/index.html')) #self.browser.show() self.webBrowser.load(QUrl.fromLocalFile(os.path.abspath('./map/map.html'))) self.interact_obj=TInteractObj(self) self.interact_obj.receive_str_from_js_callback = self.receive_data channel=QWebChannel(self.webBrowser.page()) channel.registerObject("interact_obj",self.interact_obj) self.webBrowser.page().setWebChannel(channel)
def __init__(self, url=None): QWebEnginePage.__init__(self) # put the most recent buffer at the beginning of the BUFFERS list BUFFERS.insert(0, self) hooks.webbuffer_created(self) self.fullScreenRequested.connect(self._on_full_screen_requested) self.featurePermissionRequested.connect(self._on_feature_requested) self._content_handler = WebContentHandler(self) channel = QWebChannel(self) channel.registerObject("contentHandler", self._content_handler) self.setWebChannel(channel, QWebEngineScript.ApplicationWorld) self.loadFinished.connect(self.finished) self.authenticationRequired.connect(self.handle_authentication) self.linkHovered.connect(self.on_url_hovered) self.titleChanged.connect(self.update_title) self.__authentication_data = None self.__delay_loading_url = None self.__keymap_mode = Mode.KEYMAP_NORMAL self.__mode = get_mode("standard-mode") self.__text_edit_mark = False if url: if isinstance(url, DelayedLoadingUrl): self.__delay_loading_url = url else: self.load(url)
class SearchResultsPage(BaseSearchPage): def __init__(self, profile, tmplMain, ipfsConnParams, webchannel=None, parent=None): super(SearchResultsPage, self).__init__(parent, profile=profile) self.channel = QWebChannel(self) self.handler = IPFSSearchHandler(self) self.channel.registerObject('ipfssearch', self.handler) self.setWebChannel(self.channel) self.app = QApplication.instance() self.template = tmplMain self.ipfsConnParams = ipfsConnParams ensure(self.render()) def javaScriptConsoleMessage(self, level, message, lineNumber, sourceId): log.debug( 'JS: level: {0}, source: {1}, line: {2}, message: {3}'.format( level, sourceId, lineNumber, message)) async def render(self): html = await renderTemplate(self.template, ipfsConnParams=self.ipfsConnParams) self.setHtml(html)
class WebPlayer(QWebEngineView): def __init__(self, parent=None): super().__init__(parent) # 클래스 기본 설정 path = os.getcwd() + '/support/webplayer.html' file = open(path, 'r', encoding="UTF8") self.html = file.read() file.close() NotificationCenter.subscribe(NotificationName.play, self.play) QWebEngineSettings.globalSettings().setAttribute( QWebEngineSettings.PluginsEnabled, True) QWebEngineSettings.globalSettings().setAttribute( QWebEngineSettings.JavascriptEnabled, True) # UI 설정 self.setHtml(self.html) self.web_channel = QWebChannel(self) self.web_channel.registerObject('handler', self) self.page().setWebChannel(self.web_channel) # 재생 def play(self, token): html = self.html.replace('{0}', token) self.setHtml(html) @pyqtSlot() def endVideo(self): NotificationCenter.notification(NotificationName.end_video)
def __init__(self, parent: QWidget = None): super().__init__(parent) self.setupUi(self) self.m_content = Document() self.m_filePath = "" self.editor.setFont(QFontDatabase.systemFont(QFontDatabase.FixedFont)) self.preview.setContextMenuPolicy(Qt.NoContextMenu) page = PreviewPage(self) self.preview.setPage(page) self.editor.textChanged.connect( lambda: self.m_content.setText(self.editor.toPlainText())) channel = QWebChannel(self) channel.registerObject("content", self.m_content) page.setWebChannel(channel) self.preview.setUrl(QUrl("qrc:/index.html")) self.actionNew.triggered.connect(self.onFileNew) self.actionOpen.triggered.connect(self.onFileOpen) self.actionSave.triggered.connect(self.onFileSave) self.actionSaveAs.triggered.connect(self.onFileSaveAs) self.actionExit.triggered.connect(self.onExit) self.editor.document().modificationChanged.connect( self.actionSave.setEnabled) defaultTextFile = QFile(":/default.md") defaultTextFile.open(QIODevice.ReadOnly) self.editor.setPlainText(defaultTextFile.readAll().data().decode())
class WebEngineView(QWebEngineView): def __init__(self, _record,*args, **kwargs): super(WebEngineView, self).__init__(*args, **kwargs) self.channel = QWebChannel(self) self.record=_record self.imgViewer = Img_Dialog() # 把自身对象传递进去 self.channel.registerObject('Bridge', self) # 设置交互接口 self.page().setWebChannel(self.channel) def view_img(self,result): if result: imgpath=result[8:] imgpath=imgpath.replace(imgpath.split("//")[1],"下载") self.imgViewer.setImg(imgpath) self.imgViewer.show() # 注意pyqtSlot用于把该函数暴露给js可以调用 @pyqtSlot(str,str) def callFromJs(self,method, result): if method=="img": self.view_img(result) elif method=="tip": datas=result.split("|") lists=self.record.findNote(datas[2]) response={} response["clientX"]=int(datas[0]) response["clientY"] = int(datas[1]) response["sectionId"] = datas[2] notes=[] for item in lists: notes.append(item["text"]) response["notes"] = notes self.page().runJavaScript("\n showToolTip({});".format(response))
class SC(QWidget): def __init__(self): super().__init__() self.initUI() def initUI(self): self.view = QWebEngineView(self) self.channel = QWebChannel() self.handler = CallHandler() self.channel.registerObject('pyjs', self.handler) self.view.page().setWebChannel(self.channel) self.handler._signal1.connect(self.handler._slot1) #self.handler._signal2.connect(self.handler._slot2) b1 = QPushButton("b1", self) b1.clicked.connect(self.s1s) #b2 = QPushButton("b2", self) #b2.clicked.connect(self.s2s) url_string = "file:///test.html" self.view.load(QUrl(url_string)) self.setGeometry(300, 300, 300, 220) self.show() def s1s(self): self.handler._signal1.emit("b1") def s2s(self): self.handler._signal2.emit("b2")
class WebEngineView(QtWebEngineWidgets.QWebEngineView): customSignal = pyqtSignal(str) def __init__(self, *args, **kwargs): super(WebEngineView, self).__init__(*args, **kwargs) self.channel = QWebChannel(self) # 把自身对象传递进去 self.channel.registerObject('Bridge', self) # 设置交互接口 self.page().setWebChannel(self.channel) # self.page().loadStarted.connect(self.onLoadStart) # self._script = open('./qwebchannel.js', 'rb').read().decode() # def onLoadStart(self): # print("here") # self.page().runJavaScript(self._script) # 注意pyqtSlot用于把该函数暴露给js可以调用 @pyqtSlot(str) def callFromJs(self, text): print(text) QMessageBox.information(self, "提示", "来自js调用:{}".format(text)) def sendCustomSignal(self): # 发送自定义信号 self.customSignal.emit('当前时间: ' + str(time())) @pyqtSlot(str) @pyqtSlot(QUrl) def load(self, url): ''' eg: load("https://pyqt5.com") :param url: 网址 ''' return super(WebEngineView, self).load(QUrl(url))
class Dialog(QDialog): def __init__(self, parent=None): super().__init__(parent) self.ui = Ui_Dialog() self.ui.setupUi(self) self.m_view = QWebEngineView(self) self.channel = QWebChannel(self) self.b = bridge(self) self.channel.registerObject('b', self.b) self.m_view.page().setWebChannel(self.channel) self.m_view.page().load( QUrl("file:///" + QFileInfo("index.html").absoluteFilePath())) self.ui.viewLayout.addWidget(self.m_view) # js_str = '''window.bridge.showMsgBox()''' js_str = '''alert('from runJavaScript')''' self.ui.pbAlert.clicked.connect( lambda: self.m_view.page().runJavaScript(js_str, self.callback)) # self.ui.pbAlert.clicked.connect(lambda: self.b.sendText.emit()) # self.ui.pbGetWebWidth.clicked.connect(lambda: self.b.sendText.emit()) # self.b.sendText.connect(lambda: print('b.sendText.emit():')) # self.b.signal_1.connect(lambda: print('b.signal_1.emit():')) def __del__(self): del self.ui def callback(self, v): print('callback:', v)
def __init__(self, *args, **kw): url = kw.pop('url', None) first = False if not url: url = get_authenticated_url() first = True self.url = url self.closing = False super(QWebView, self).__init__(*args, **kw) self.setWindowTitle('Bitmask') self.bitmask_browser = NewPageConnector(self) if first else None self.loadPage(self.url) self.bridge = AppBridge(self) if first else None if self.bridge is not None and HAS_WEBENGINE: print "[+] registering python<->js bridge" channel = QWebChannel(self) channel.registerObject("bitmaskApp", self.bridge) self.page().setWebChannel(channel) icon = QtGui.QIcon() icon.addPixmap( QtGui.QPixmap(":/mask-icon.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off) self.setWindowIcon(icon)
class FancyBox(QWebEngineView): def __init__(self, interface, title="", html=""): super().__init__() self.setWindowTitle(title) self.setWindowModality(Qt.ApplicationModal) self.resize(800, 600) # setup page self.web_page = CustomPage() self.web_page.setUrl( QUrl.fromLocalFile(html_to_file_path(html or f"<h1>{title}</h1>")) ) # self.web_page.setHtml(html or f"<h1>{title}</h1>") self.setPage(self.web_page) # setup channel self.channel = QWebChannel() self.channel.registerObject("backend", interface) self.page().setWebChannel(self.channel) self.set_view(QApplication.instance()) def set_view(self, app: QApplication): # Set geometry. screen_rect = app.desktop().screen().rect() # Set zoom. if System.is_windows(): # view.setZoomFactor(1.8) screen_height = screen_rect.height() base_height = 1080 if screen_height > base_height: scale = (screen_height / base_height) * 0.9 print("[Qt/window] scale", scale) self.setZoomFactor(scale)
class MotionGraphVisualizerWidget(QWebEngineView): def __init__(self, parent=None): QWebEngineView.__init__(self, parent) self.setAttribute(Qt.WA_TranslucentBackground) self.setWindowFlags(Qt.FramelessWindowHint) self._page = GraphVisualizePage() self.setPage(self._page) self._channel = QWebChannel(self._page) self._channel.registerObject('page', self._page) self._page.setWebChannel(self._channel) self.loadFinished.connect(self._on_load_finished) self.setUrl(QUrl('qrc:/graph_visualizer/test.html')) def _on_load_finished(self): self._page.init() def load_motion_graph(self, motion_graph): self._page.load_motion_graph(motion_graph) @property def visualizer(self): return self._page
class WebChannelServer: def __init__(self): self._server = None self._clientWrapper = None self._channel = None self._bridge = None def setupWebChannel(self, bridge=None, bridgename="bridge", port=12345): # setup the QWebSocketServer self._server = QWebSocketServer("Grumble webchannel", QWebSocketServer.NonSecureMode) if not self._server.listen(QHostAddress.LocalHost, port): qFatal("Failed to open web socket server.") return False # wrap WebSocket clients in QWebChannelAbstractTransport objects self._clientWrapper = WebSocketClientWrapper(self._server, self) # setup the channel self._channel = QWebChannel() self._clientWrapper.clientConnected.connect( self._channel.connectTo) # Publish the bridge object to the QWebChannel self._bridge = bridge if bridge is not None: self._channel.registerObject(bridgename, bridge)
class MainWindow(QWidget): def __init__(self): super(MainWindow, self).__init__() self.setWindowTitle('js <-> py') self.resize(330, 220) self.layout = QVBoxLayout() self.btn_test = QPushButton('test') # self.btn_test.clicked.connect(self.test) # 创建一个 QWebEngineView 对象 self.web = QWebEngineView() file_path = QFileInfo("./test.html").absoluteFilePath() self.web.load(QUrl(file_path)) # 把QWebView和button加载到layout布局中 self.layout.addWidget(self.web) self.layout.addWidget(self.btn_test) self.setLayout(self.layout) self.channel = QWebChannel() self.shared = Myshared() self.set_channel() def set_channel(self): self.channel.registerObject("conn_shared", self.shared) self.web.page().setWebChannel(self.channel) def __del__(self): ''' 删除相关对象 ''' self.web.deleteLater()
def set_action(self, action, d): from PyQt5.QtWebChannel import QWebChannel self.page().setWebChannel(None) channel = QWebChannel(self.page()) channel.registerObject(action.action(), action) self.page().setWebChannel(channel) action.callback = d
class MapWindow(QDialog): rectSignal = pyqtSignal(str) def __init__(self, parent=None): super(MapWindow, self).__init__(parent) self.label = QLabel(self) self.setWindowTitle(APP_NAME) self.resize(QSize(1400, 900)) layout = QVBoxLayout(self) self.webview = QWebEngineView(self) self.channel = QWebChannel(self) # 把自身对象传递进去 self.channel.registerObject('Bridge', self) # 设置交互接口 self.webview.page().setWebChannel(self.channel) layout.addWidget(self.webview) self.webview.load( QUrl.fromLocalFile(APP_HtmlPath + os.sep + "choose_rect.html")) self.show() @pyqtSlot(str) @pyqtSlot(QUrl) def getRect(self, rect): leftbottom = rect.split(' ')[0] righttop = rect.split(' ')[1] self.rectSignal.emit(rect) # 发射信号 self.close()
def __init__(self, is_static, parent): super().__init__(parent) self.language = Settings()['locale'] self.is_static = is_static self.lat = None self.lng = None self.points = [] self.activated = False self.initialized = False self.loadFinished.connect(self.onLoadFinished) if importedQtWebEngine: self.setPage(WebEnginePage(self)) channel = QWebChannel(self.page()) channel.registerObject("qtWidget", self) self.page().setWebChannel(channel) else: self.page().setLinkDelegationPolicy(QWebPage.DelegateAllLinks) self.page().linkClicked.connect(self.linkClicked) self.page().mainFrame().addToJavaScriptWindowObject( "qtWidget", self) self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
class View(QWebEngineView): QT_SCRIPT_URL = QUrl.fromLocalFile(os.path.join(os.path.dirname(__file__), "qt.js")) def __init__(self, data): super().__init__() self.renderer = theria.Renderer( data, header_scripts=["qrc:///qtwebchannel/qwebchannel.js"], body_scripts=[self.QT_SCRIPT_URL.toString()], ) # setup page self.web_page = CustomPage() self.web_page.setUrl(QUrl.fromLocalFile(html_to_file_path(self.renderer.html))) self.setPage(self.web_page) # setup channel self.interface = Interface(self.renderer) self.channel = QWebChannel() self.channel.registerObject("backend", self.interface) self.page().setWebChannel(self.channel) self.interface.updated.connect(self.reload_page) self.interface.dialog.connect(self.load_fancy_box) # setup fancy box self.fancy_box = None def set_view(self, app: QApplication): # Set geometry. screen_rect = app.desktop().screen().rect() screen_center = screen_rect.center() width = (7 * screen_rect.width()) // 10 height = (2 * screen_rect.height()) // 3 x = screen_center.x() - width // 2 y = screen_center.y() - height // 2 print( f"[Qt/window] size {width} x {height}, position ({x}; {y})", file=sys.stderr ) self.setGeometry(x, y, width, height) # Set zoom. if System.is_windows(): # view.setZoomFactor(1.8) screen_height = screen_rect.height() base_height = 1080 if screen_height > base_height: scale = (screen_height / base_height) * 0.9 print("[Qt/window] scale", scale) self.setZoomFactor(scale) def reload_page(self): self.web_page.setUrl(QUrl.fromLocalFile(html_to_file_path(self.renderer.html))) def load_fancy_box(self, html): print("[fancybox]") print(html) if self.fancy_box: del self.fancy_box self.fancy_box = FancyBox(self.interface, title="Form", html=html) self.fancy_box.show()
def clone(self): nc = QWebChannel(self) objects = self.registeredObjects() for objName, obj in objects: log.debug(f'{self}: cloning object with ID: {objName}') nc.registerObject(objName, obj) return nc
def register_delivery_channel(self): """主窗口与交割服务界面的js交互通道""" web_channel = DeliveryChannel() # 交互信号对象 web_channel.hasReceivedUserToken.connect(self.send_token_timer.stop) web_channel.moreCommunicationSig.connect(self.more_communication) # 更多交流讨论 web_channel.linkUsPageSig.connect(self.to_link_us_page) channel_qt_obj = QWebChannel(self.web_show.page()) self.web_show.page().setWebChannel(channel_qt_obj) channel_qt_obj.registerObject("GUIMsgChannel", web_channel) # 注册信号对象
def init_channel(self): """ 为webview绑定交互对象 """ self.interact_obj = TInteractObj(self) self.interact_obj.receive_str_from_js_callback = self.receive_webdata channel = QWebChannel(self.view.page()) # interact_obj 为交互对象的名字,js中使用 channel.registerObject("interact_obj", self.interact_obj) self.view.page().setWebChannel(channel)
def __init__(self, web_channel, file_url, *args, **kwargs): super(ChartContainWidget, self).__init__(*args, **kwargs) self.setAttribute(Qt.WA_DeleteOnClose, True) # 加载图形容器 self.page().load(QUrl(file_url)) # 加载页面 # 设置与页面信息交互的通道 channel_qt_obj = QWebChannel(self.page()) # 实例化qt信道对象,必须传入页面参数 self.contact_channel = web_channel # 页面信息交互通道 self.page().setWebChannel(channel_qt_obj) channel_qt_obj.registerObject("pageContactChannel", self.contact_channel) # 信道对象注册信道,只能注册一个
def setup_markdown_editor(self): self.renderers = {} self.fallback_renderer = PlainRenderer() self.add_renderer(self.fallback_renderer) self.add_renderer(MarkdownRenderer()) self.add_renderer(ReSTRenderer()) # Set up Markdown editor self.setup_lexer() self.setup_scintilla(self.ui.markdownEditor) # Set up our custom page to open external links in a browser page = CustomWebPage(self) self.ui.markdownPreview.setPage(page) QWebEngineSettings.globalSettings().setAttribute( QWebEngineSettings.FocusOnNavigationEnabled, False) # self.ui.markdownPreview. # Set up the web channel to intercept clicks on links channel = QWebChannel(self) self.channel_proxy = WebChannelProxy(self) channel.registerObject("proxy", self.channel_proxy) page.setWebChannel(channel) self.channel_proxy.link_clicked.connect(self.link_clicked) # Set up HTML preview self.ui.htmlPreview.setLexer(QsciLexerHTML()) # Connect signals self.ui.actionUndo.triggered.connect(self.undo) self.ui.actionRedo.triggered.connect(self.redo) self.ui.actionSave.triggered.connect(self.save_article) self.ui.actionCommit.triggered.connect(self.commit_article) self.ui.actionEdit.toggled.connect(self.edit_toggled) self.ui.actionFullscreen.triggered.connect(self.show_fullscreen_editor) self.ui.actionAutoLink.triggered.connect(self.auto_link_word) self.ui.actionAutoLinkAll.triggered.connect(self.auto_link_all) self.ui.markdownEditor.installEventFilter(self) # Load Github style style_file = QFile(':/styles/github.css') style_file.open(QIODevice.ReadOnly) self.style = QTextStream(style_file).readAll() style_file.close() self.current_article = None self.ui.markdownEditor.hide() self.ui.actionUndo.setEnabled(False) self.ui.actionRedo.setEnabled(False) self.update_toolbar()
def __init__(self, parent=None, bridge=None, *, debug=False, **kwargs): debug = debug or _ORANGE_DEBUG if debug: port = os.environ.setdefault('QTWEBENGINE_REMOTE_DEBUGGING', '12088') warnings.warn( 'To debug QWebEngineView, set environment variable ' 'QTWEBENGINE_REMOTE_DEBUGGING={port} and then visit ' 'http://127.0.0.1:{port}/ in a Chromium-based browser. ' 'See https://doc.qt.io/qt-5/qtwebengine-debugging.html ' 'This has also been done for you.'.format(port=port)) super().__init__( parent, sizeHint=QSize(500, 400), sizePolicy=QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding), **kwargs) self.bridge = bridge self.debug = debug with open(_WEBVIEW_HELPERS, encoding="utf-8") as f: self._onloadJS( f.read(), name='webview_helpers', injection_point=QWebEngineScript.DocumentCreation) qtwebchannel_js = QFile("://qtwebchannel/qwebchannel.js") if qtwebchannel_js.open(QFile.ReadOnly): source = bytes(qtwebchannel_js.readAll()).decode("utf-8") with open(_WEBENGINE_INIT_WEBCHANNEL, encoding="utf-8") as f: init_webchannel_src = f.read() self._onloadJS( source + init_webchannel_src % dict( exposeObject_prefix=self._EXPOSED_OBJ_PREFIX), name='webchannel_init', injection_point=QWebEngineScript.DocumentCreation) else: warnings.warn("://qtwebchannel/qwebchannel.js is not readable.", RuntimeWarning) self._onloadJS( ';window.__load_finished = true;', name='load_finished', injection_point=QWebEngineScript.DocumentReady) channel = QWebChannel(self) if bridge is not None: if isinstance(bridge, QWidget): print( "Don't expose QWidgets in WebView. Construct minimal QObjects instead." ) channel.registerObject("pybridge", bridge) channel.registerObject('__bridge', _QWidgetJavaScriptWrapper(self)) self.page().setWebChannel(channel)
def register_webchannel(self, page): # Register one bridge object per profile so it can use the # seperate cookieJar and User-Agent for xhrs. if page.profile() not in self.channels: new_wc = QWebChannel() self.channels[page.profile()] = new_wc new_bridge = GreasemonkeyBridge(new_wc, page.profile()) self.bridges[page.profile()] = new_bridge new_wc.registerObject('qute', new_bridge) wc = self.channels[page.profile()] page.setWebChannel(wc) #worldId is second param
def __init__(self, parent): super(View, self).__init__(parent) self._highlightFormat = QTextCharFormat() self.jslink = JSLink(self) channel = QWebChannel(self) channel.registerObject("pyLinks", self.jslink) self.page().setWebChannel(channel) self.loadFinished.connect(self.svgLoaded) self.mainwindow().aboutToClose.connect(self.cleanupForClose) app.settingsChanged.connect(self.readSettings) self.readSettings() self.load(self.defaulturl)
def execute_in_view(self, view: QWebEngineView): view.load(Core.QUrl(self.url)) page = view.page() channel = QWebChannel(view) page.setWebChannel(channel) channel.registerObject('app_receiver', self.receiver) script = QWebEngineScript() script.setSourceCode(_get_jsinject_dep_code() + self.jscode) script.setWorldId(QWebEngineScript.MainWorld) page.profile().scripts().insert(script)
def initBrowser(self): view = QWebEngineView() channel = QWebChannel() handler = CallHandler(self) channel.registerObject('pyjs', handler) view.page().setWebChannel(channel) url_string = "https://bing.com" view.load(QUrl(url_string)) view.show() self.view = view self.channel = channel self.handler = handler
def __init__(self, qMainWindow=None, qtDesigner=None): if self.__class__.logger is None: self.__class__.logger = logging.getLogger('root') if self.__class__.loader is None: self.__class__.loader = genshi.template.TemplateLoader( os.path.abspath(os.path.join(os.path.dirname(sys.argv[0]), 'www')), auto_reload=True ) if self.__class__.qMainWindow is None and qMainWindow is not None: self.__class__.qMainWindow = qMainWindow if self.__class__.qtDesigner is None and qtDesigner is not None: self.__class__.qtDesigner = qtDesigner class WebEnginePage(QWebEnginePage): updated = pyqtSignal(str, float, str) def javaScriptConsoleMessage(self, level, msg, line, source): harkfm.Interface.logger.debug('%s:%s %s', source, line, msg) def acceptNavigationRequest(self, url, type, is_main_frame): if type == QWebEnginePage.NavigationTypeLinkClicked: webbrowser.open(url.toString()) return False return True # Turn a jQuery.serialize() query into a dict def _deserialize(self, query): form = {} query = query.split('&') for val in query: val = val.split('=') form[urllib.parse.unquote(val[0])] = urllib.parse.unquote(val[1]) return form @pyqtSlot() def update(self): if engine.current is not None: self.updated.emit(engine.current.elapsed, engine.current.percent, engine.current.remaining) @pyqtSlot() def queued(self): queued = 0 if engine.current is not None: queued = engine.current.queued self.updated.emit(sys._getframe().f_code.co_name, queued) @pyqtSlot() def love(self): engine.current.love() interface = harkfm.Interface() interface.index() @pyqtSlot() def unlove(self): engine.current.unlove() interface = harkfm.Interface() interface.index() @pyqtSlot(str) def login(self, query): form = self._deserialize(query) storage = harkfm.Storage() api_key = harkfm.Engine.config['apis']['last.fm']['key'] api_secret = harkfm.Engine.config['apis']['last.fm']['secret'] try: network = pylast.get_lastfm_network(api_key, api_secret) session_key = pylast.SessionKeyGenerator(network).get_session_key(form['username'], pylast.md5(form['password'])) storage.config_set('apis/last.fm/session_key', session_key) interface = harkfm.Interface() interface.index() except Exception as e: harkfm.Interface.logger.error('%s %s', type(e), e) @pyqtSlot() def logout(self): engine = harkfm.Engine() engine.lfm_logout() @pyqtSlot(str) def save_settings(self, form): form = json.loads(form) storage = harkfm.Storage() for key in form: if type(form[key]) is str and form[key].isdigit(): form[key] = int(form[key]) storage.config_set('settings/' + key, form[key]) interface = harkfm.Interface() interface.index() # Set custom page page = WebEnginePage(self.QWebEngineView) page.profile().setHttpCacheType(QWebEngineProfile.NoCache) self.QWebEngineView.setPage(page) # Set custom channel channel = QWebChannel(page) channel.registerObject('py', page) page.setWebChannel(channel) engine = harkfm.Engine() # init harkfm.Engine lfm_network = engine.lfm_login() if lfm_network is None: self.login() else: self.index()
class BrowserView(QMainWindow): instances = {} inspector_port = None # The localhost port at which the Remote debugger listens create_window_trigger = QtCore.pyqtSignal(object) set_title_trigger = QtCore.pyqtSignal(str) load_url_trigger = QtCore.pyqtSignal(str) html_trigger = QtCore.pyqtSignal(str, str) dialog_trigger = QtCore.pyqtSignal(int, str, bool, str, str) destroy_trigger = QtCore.pyqtSignal() fullscreen_trigger = QtCore.pyqtSignal() window_size_trigger = QtCore.pyqtSignal(int, int) current_url_trigger = QtCore.pyqtSignal() evaluate_js_trigger = QtCore.pyqtSignal(str, str) class JSBridge(QtCore.QObject): api = None parent_uid = None qtype = QtCore.QJsonValue if is_webengine else str def __init__(self): super(BrowserView.JSBridge, self).__init__() @QtCore.pyqtSlot(str, qtype, result=str) def call(self, func_name, param): func_name = BrowserView._convert_string(func_name) param = BrowserView._convert_string(param) return _js_bridge_call(self.parent_uid, self.api, func_name, param) class WebView(QWebView): def __init__(self, parent=None): super(BrowserView.WebView, self).__init__(parent) if parent.frameless: QApplication.instance().installEventFilter(self) self.setMouseTracking(True) def contextMenuEvent(self, event): menu = self.page().createStandardContextMenu() # If 'Inspect Element' is present in the default context menu, it # means the inspector is already up and running. for i in menu.actions(): if i.text() == 'Inspect Element': break else: # Inspector is not up yet, so create a pseudo 'Inspect Element' # menu that will fire it up. inspect_element = QAction('Inspect Element', menu) inspect_element.triggered.connect(self.show_inspector) menu.addAction(inspect_element) menu.exec_(event.globalPos()) # Create a new webview window pointing at the Remote debugger server def show_inspector(self): uid = self.parent().uid + '-inspector' try: # If inspector already exists, bring it to the front BrowserView.instances[uid].raise_() BrowserView.instances[uid].activateWindow() except KeyError: title = 'Web Inspector - {}'.format(self.parent().title) url = 'http://localhost:{}'.format(BrowserView.inspector_port) inspector = BrowserView(uid, title, url, 700, 500, True, False, (300, 200), False, '#fff', False, None, True, self.parent().webview_ready) inspector.show() def mousePressEvent(self, event): if event.button() == QtCore.Qt.LeftButton: self.drag_pos = event.globalPos() - self.parent().frameGeometry().topLeft() event.accept() def mouseMoveEvent(self, event): if self.parent().frameless and int(event.buttons()) == 1: # left button is pressed self.parent().move(event.globalPos() - self.drag_pos) def eventFilter(self, object, event): if object.parent() == self: if event.type() == QtCore.QEvent.MouseMove: self.mouseMoveEvent(event) elif event.type() == QtCore.QEvent.MouseButtonPress: self.mousePressEvent(event) return False # New-window-requests handler for Qt 5.5+ only class NavigationHandler(QWebPage): def __init__(self, parent=None): super(BrowserView.NavigationHandler, self).__init__(parent) def acceptNavigationRequest(self, url, type, is_main_frame): webbrowser.open(url.toString(), 2, True) return False class WebPage(QWebPage): def __init__(self, parent=None): super(BrowserView.WebPage, self).__init__(parent) self.nav_handler = BrowserView.NavigationHandler(self) if is_webengine else None if not is_webengine: def acceptNavigationRequest(self, frame, request, type): if frame is None: webbrowser.open(request.url().toString(), 2, True) return False return True def createWindow(self, type): return self.nav_handler def __init__(self, uid, title, url, width, height, resizable, fullscreen, min_size, confirm_quit, background_color, debug, js_api, text_select, frameless, webview_ready): super(BrowserView, self).__init__() BrowserView.instances[uid] = self self.uid = uid self.js_bridge = BrowserView.JSBridge() self.js_bridge.api = js_api self.js_bridge.parent_uid = self.uid self.is_fullscreen = False self.confirm_quit = confirm_quit self.text_select = text_select self._file_name_semaphore = Semaphore(0) self._current_url_semaphore = Semaphore(0) self.load_event = Event() self.webview_ready = webview_ready self._js_results = {} self._current_url = None self._file_name = None self.resize(width, height) self.title = title self.setWindowTitle(title) # Set window background color self.background_color = QColor() self.background_color.setNamedColor(background_color) palette = self.palette() palette.setColor(self.backgroundRole(), self.background_color) self.setPalette(palette) if not resizable: self.setFixedSize(width, height) self.setMinimumSize(min_size[0], min_size[1]) self.frameless = frameless if frameless: self.setWindowFlags(QtCore.Qt.Window | QtCore.Qt.FramelessWindowHint) self.view = BrowserView.WebView(self) if debug and is_webengine: # Initialise Remote debugging (need to be done only once) if not BrowserView.inspector_port: BrowserView.inspector_port = BrowserView._get_free_port() os.environ['QTWEBENGINE_REMOTE_DEBUGGING'] = BrowserView.inspector_port else: self.view.setContextMenuPolicy(QtCore.Qt.NoContextMenu) # disable right click context menu self.view.setPage(BrowserView.WebPage(self.view)) if url is not None: self.view.setUrl(QtCore.QUrl(url)) else: self.load_event.set() self.setCentralWidget(self.view) self.create_window_trigger.connect(BrowserView.on_create_window) self.load_url_trigger.connect(self.on_load_url) self.html_trigger.connect(self.on_load_html) self.dialog_trigger.connect(self.on_file_dialog) self.destroy_trigger.connect(self.on_destroy_window) self.fullscreen_trigger.connect(self.on_fullscreen) self.window_size_trigger.connect(self.on_window_size) self.current_url_trigger.connect(self.on_current_url) self.evaluate_js_trigger.connect(self.on_evaluate_js) self.set_title_trigger.connect(self.on_set_title) if is_webengine and platform.system() != 'OpenBSD': self.channel = QWebChannel(self.view.page()) self.view.page().setWebChannel(self.channel) self.view.page().loadFinished.connect(self.on_load_finished) if fullscreen: self.toggle_fullscreen() self.move(QApplication.desktop().availableGeometry().center() - self.rect().center()) self.activateWindow() self.raise_() webview_ready.set() def on_set_title(self, title): self.setWindowTitle(title) def on_file_dialog(self, dialog_type, directory, allow_multiple, save_filename, file_filter): if dialog_type == FOLDER_DIALOG: self._file_name = QFileDialog.getExistingDirectory(self, localization['linux.openFolder'], options=QFileDialog.ShowDirsOnly) elif dialog_type == OPEN_DIALOG: if allow_multiple: self._file_name = QFileDialog.getOpenFileNames(self, localization['linux.openFiles'], directory, file_filter) else: self._file_name = QFileDialog.getOpenFileName(self, localization['linux.openFile'], directory, file_filter) elif dialog_type == SAVE_DIALOG: if directory: save_filename = os.path.join(str(directory), str(save_filename)) self._file_name = QFileDialog.getSaveFileName(self, localization['global.saveFile'], save_filename) self._file_name_semaphore.release() def on_current_url(self): url = BrowserView._convert_string(self.view.url().toString()) self._current_url = None if url == '' else url self._current_url_semaphore.release() def on_load_url(self, url): self.view.setUrl(QtCore.QUrl(url)) def on_load_html(self, content, base_uri): self.view.setHtml(content, QtCore.QUrl(base_uri)) def closeEvent(self, event): if self.confirm_quit: reply = QMessageBox.question(self, self.title, localization['global.quitConfirmation'], QMessageBox.Yes, QMessageBox.No) if reply == QMessageBox.No: event.ignore() return event.accept() del BrowserView.instances[self.uid] try: # Close inspector if open BrowserView.instances[self.uid + '-inspector'].close() del BrowserView.instances[self.uid + '-inspector'] except KeyError: pass if len(BrowserView.instances) == 0: _app.exit() def on_destroy_window(self): self.close() def on_fullscreen(self): if self.is_fullscreen: self.showNormal() else: self.showFullScreen() self.is_fullscreen = not self.is_fullscreen def on_window_size(self, width, height): self.setFixedSize(width, height) def on_evaluate_js(self, script, uuid): def return_result(result): result = BrowserView._convert_string(result) uuid_ = BrowserView._convert_string(uuid) js_result = self._js_results[uuid_] js_result['result'] = None if result is None or result == 'null' else result if result == '' else json.loads(result) js_result['semaphore'].release() try: # < Qt5.6 result = self.view.page().mainFrame().evaluateJavaScript(script) return_result(result) except AttributeError: self.view.page().runJavaScript(script, return_result) def on_load_finished(self): if self.js_bridge.api: self._set_js_api() else: self.load_event.set() if not self.text_select: script = disable_text_select.replace('\n', '') try: # QT < 5.6 self.view.page().mainFrame().evaluateJavaScript(script) except AttributeError: self.view.page().runJavaScript(script) def set_title(self, title): self.set_title_trigger.emit(title) def get_current_url(self): self.load_event.wait() self.current_url_trigger.emit() self._current_url_semaphore.acquire() return self._current_url def load_url(self, url): self.load_event.clear() self.load_url_trigger.emit(url) def load_html(self, content, base_uri): self.load_event.clear() self.html_trigger.emit(content, base_uri) def create_file_dialog(self, dialog_type, directory, allow_multiple, save_filename, file_filter): self.dialog_trigger.emit(dialog_type, directory, allow_multiple, save_filename, file_filter) self._file_name_semaphore.acquire() if dialog_type == FOLDER_DIALOG: file_names = (self._file_name,) elif dialog_type == SAVE_DIALOG or not allow_multiple: file_names = (self._file_name[0],) else: file_names = tuple(self._file_name[0]) # Check if we got an empty tuple, or a tuple with empty string if len(file_names) == 0 or len(file_names[0]) == 0: return None else: return file_names def destroy_(self): self.destroy_trigger.emit() def toggle_fullscreen(self): self.fullscreen_trigger.emit() def set_window_size(self, width, height): self.window_size_trigger.emit(width, height) def evaluate_js(self, script): self.load_event.wait() result_semaphore = Semaphore(0) unique_id = uuid1().hex self._js_results[unique_id] = {'semaphore': result_semaphore, 'result': ''} self.evaluate_js_trigger.emit(script, unique_id) result_semaphore.acquire() result = deepcopy(self._js_results[unique_id]['result']) del self._js_results[unique_id] return result def _set_js_api(self): def _register_window_object(): frame.addToJavaScriptWindowObject('external', self.js_bridge) script = parse_api_js(self.js_bridge.api) if is_webengine: qwebchannel_js = QtCore.QFile('://qtwebchannel/qwebchannel.js') if qwebchannel_js.open(QtCore.QFile.ReadOnly): source = bytes(qwebchannel_js.readAll()).decode('utf-8') self.view.page().runJavaScript(source) self.channel.registerObject('external', self.js_bridge) qwebchannel_js.close() else: frame = self.view.page().mainFrame() _register_window_object() try: # < QT 5.6 self.view.page().mainFrame().evaluateJavaScript(script) except AttributeError: self.view.page().runJavaScript(script) self.load_event.set() @staticmethod def _convert_string(result): try: if result is None or result.isNull(): return None result = result.toString() # QJsonValue conversion except AttributeError: pass return convert_string(result) @staticmethod # A simple function to obtain an unused localhost port from the os return it def _get_free_port(): s = socket() s.bind(('localhost', 0)) port = str(s.getsockname()[1]) s.close() return port @staticmethod # Receive func from subthread and execute it on the main thread def on_create_window(func): func()
def set_action(self, action, d): self.page().setWebChannel(None) channel = QWebChannel(self.page()) channel.registerObject(action.action(), action) self.page().setWebChannel(channel) action.callback = d
def _OnUrlChanged(self, url): webchannel = QWebChannel(self.page()) self.page().setWebChannel(webchannel) webchannel.registerObject("kiwoom", self._kiwoom)
# jsFile = constants.QTWEBCHANNELJS_FILE # with open(jsFile, encoding="UTF-8") as file: # js = file.read() # script = QWebEngineScript(); # script.setSourceCode(js) # script.setName('qwebchannel.js') # script.setInjectionPoint(QWebEngineScript.DocumentCreation) # script.setWorldId(QWebEngineScript.MainWorld) # script.setRunsOnSubFrames(False) # # mainwindow.view.page.profile().scripts().insert(script) channel = QWebChannel() myobject=myObject() channel.registerObject("xdpy",myobject) mainwindow.view.page.setWebChannel(channel) # # jsFile = constants.QTWEBCHANNELJS_FILE # with open(jsFile, encoding="UTF-8") as file: # js = file.read() # mainwindow.injectJS(js, 'qwebchannel.js') js='\nnew QWebChannel(qt.webChannelTransport, function(channel){alert("正在调用回调函数");window.xdpy=channel.objects.xdpy;xdpy.test();});\n' #mainwindow.view.page.runJavaScript(js,mainwindow.js_callback) # mainwindow.view.page.runJavaScript('window.channel="AAAA"', mainwindow.js_callback) mainwindow.view.page.runJavaScript(js,mainwindow.js_callback) #mainwindow.view.page.runJavaScript("window.xdpy.test()",mainwindow.js_callback) app.exec();
class PreviewSync(QObject): """This class synchronizes the contents of the web and text views and aligns them vertically. """ textToPreviewSynced = pyqtSignal() # Setup / cleanup # =============== def __init__(self, # The preview dock involved in synchronization. previewDock): QObject.__init__(self) # Only set up sync if fuzzy matching is available. if not findApproxTextInTarget: return # Gather into one variable all the JavaScript needed for PreviewSync. self._jsPreviewSync = self._jsOnClick + self._jsWebCursorCoords self._dock = previewDock self._callbackManager = CallbackManager() self._initPreviewToTextSync() self._initTextToPreviewSync() self._unitTest = False def terminate(self): # Uninstall the text-to-web sync only if it was installed in the first # place (it depends on TRE). if findApproxTextInTarget: self._cursorMovementTimer.stop() # Shut down the background sync. If a sync was already in progress, # then discard its output. self._runLatest.future.cancel(True) self._runLatest.terminate() # End all callbacks. self._callbackManager.skipAllCallbacks() self._callbackManager.waitForAllCallbacks() # Bug: DON'T de-register the QWebChannel. This casues the error message ``onmessage is not a callable property of qt.webChannelTransport. Some things might not work as expected.`` to be displayed. I see this in https://code.woboq.org/qt5/qtwebengine/src/core/renderer/web_channel_ipc_transport.cpp.html#221; I assume it's a result of attempting to send a signal to the web page, where the web page doesn't have qwebchannel.js running. #self.channel.deregisterObject(self) # Delete it. Note that the channel is `NOT owned by the page <http://doc.qt.io/qt-5/qwebenginepage.html#setWebChannel>`. sip.delete(self.channel) # Disconnect all signals. sip.delete(self) # # Vertical synchronization ##======================== # These routines perform vertical synchronization. # # This function computes the distance, in pixels, measured from the target # cursor location to the source cursor location, as shown in part (a) of the # figure below: delta = source - target, so that source = target + delta. # This distance is limited by a constraint: the resulting target cursor # location must be kept a padding pixels amount away from the boundaries of # the target widget. Part (b) of the figure shows show this distance is # limited when the source lies above the target widget; the same constraint # applies when the source lies below the target widget. # # .. image:: sync_delta.png # # Ideally, this would instead operate on the baseline of the text, rather # than the bottom, but getting this is harder. def _alignScrollAmount(self, # The top (y) coordinate of the source widget in a global coordinate frame, # such as screen coordinates. In pixels. sourceGlobalTop, # The bottom coordinate of the cursor in the source widget, measured from the # top of the widget, NOT the top of the viewport. In pixels. sourceCursorBottom, # The top (y) coordinate of the target widget in a global coordinate frame, # such as screen coordinates. In pixels. targetGlobalTop, # The bottom coordinate of the cursor in the target widget, measured from the # top of the widget, NOT the top of the viewport. In pixels. targetCursorBottom, # The height of the target widget. In pixels. targetHeight, # The height of the cursor in the target widget. In pixels. targetCursorHeight, # The minimum allowable distance between target + delta and the top or # bottom of the target widget. padding): # Compute the raw delta between the source and target widgets. # # .. image:: dtop_initial_diagram.png delta = ( # Global coords of the source cursor top. (sourceGlobalTop + sourceCursorBottom) - # Global coords of the target cursor top. The difference # gives the number of pixels separating them. (targetGlobalTop + targetCursorBottom) ); # Constrain the resulting delta so that the stays padding pixels from # the top of the target widget. delta = max(-targetCursorBottom + targetCursorHeight + padding, delta) # Likewise, constrain the bottom. delta = min(targetHeight - targetCursorBottom - padding, delta) return delta # This string contains JavaScript code to determine the coordinates and height of the # anchor of the selection in the web view. _jsWebCursorCoords = ( # This function returns the [top, left] position in pixels of ``obj`` # relative to the screen, not to the viewport. This introduces one # potential problem: if obj is not visible when this is called, it # returns coordinates outside the screen (such that top or left is # negative or greater than the screen's height or width. # # It was slightly modified from http://www.quirksmode.org/js/findpos.html, # which reproduces jQuery's offset method (https://api.jquery.com/offset/). 'function findPos(obj) {' 'var curLeft = 0;' 'var curTop = 0;' # element.offsetLeft and element.offsetTop measure relative to # the object's parent. Walk the tree of parents, summing each # offset to determine the offset from the origin of the web page. 'do {' 'curLeft += obj.offsetLeft;' 'curTop += obj.offsetTop;' '} while (obj = obj.offsetParent);' # See `element.getBoundingClientRect # <https://developer.mozilla.org/en-US/docs/Web/API/element.getBoundingClientRect>`_ # for converting viewport coords to screen coords. 'return [curLeft - window.scrollX, curTop - window.scrollY];' '}' + # This function returns [top, left, width], of the current # selection, where: # # top, left - coordinates of the anchor of the # selection relative to the screen, in pixels. # # height - height at the beginning of the selection, in pixels. # # Adapted from http://stackoverflow.com/questions/2031518/javascript-selection-range-coordinates. # Changes: # # - jQuery usage eliminated for all but debug prints. # - The original code used ``range.endOffset`` instead of # ``selection.focusOffset``. This caused occasional errors when # dragging selections. 'function selectionAnchorCoords() {' # Using ``window.getSelection()`` # Make sure a `selection <https://developer.mozilla.org/en-US/docs/Web/API/Selection>`_ exists. 'var selection = window.getSelection();' 'if (selection.rangeCount == 0) return 0;' # The selection can contain not just a point (from a # single mouse click) but a range (from a mouse drag or # shift+arrow keys). # We're looking for the coordinates of the focus node # (the place where the mouse ends up after making the selection). # However, the range returned by ``selection.getRangeAt(0)`` # begins earlier in the document and ends later, regardless # how the mouse was dragged. So, create a new range containing # just the point at the focus node, so we actually get # a range pointing to where the mouse is. # Ref: `focus <https://developer.mozilla.org/en-US/docs/Web/API/Selection.focusNode>`_ of the selection. # `Range <https://developer.mozilla.org/en-US/docs/Web/API/range>`_ 'var rangeAtFocus = document.createRange();' 'rangeAtFocus.setStart(selection.focusNode, selection.focusOffset);' # Insert a measurable element (a span) at the selection's # focus. 'var span = document.createElement("span");' 'rangeAtFocus.insertNode(span);' # Measure coordinates at this span, then remove it. 'var [left, top] = findPos(span);' 'var height = span.offsetHeight;' 'span.remove();' 'return [left, top, height];' '}' # Clear the current selection, if it exists. 'function clearSelection() {' 'if (window.getSelection()) {' 'window.getSelection().empty();' '}' '}' # Given text to find, place a highlight on the last line containing the # text. 'function highlightFind(' # The text to find, typically consisting of all text in the web page # from its beginning to the point to be found. 'txt) {' # Clear the current selection, so that a find will start at the # beginning of the page. 'clearSelection();' # Find or create a ``div`` used as a highlighter. 'var highlighter = getHighlight();' 'if (!highlighter) {' 'highlighter = document.createElement("div");' 'document.body.appendChild(highlighter);' 'highlighter.style.zIndex = 100;' 'highlighter.style.width = "100%";' 'highlighter.style.position = "absolute";' # Pass any click on the highlight on to the webpage underneath. # See https://developer.mozilla.org/en-US/docs/Web/CSS/pointer-events. 'highlighter.style.pointerEvents = "none";' 'highlighter.style.backgroundColor = "rgba(255, 255, 0, 0.4)";' 'highlighter.id = "highlighter";' '}' # See https://developer.mozilla.org/en-US/docs/Web/API/Window/find. ## aString, aCaseSensitive, aBackwards, aWrapAround, aWholeWord, aSearchInFrames, aShowDialog) 'var found = window.find(txt, true, false, false, false, true, false);' # If the text was found, or the search string was empty, highlight a line. 'if (found || txt === "") {' # Determine the coordiantes of the end of the selection. 'var res = selectionAnchorCoords();' 'if (res) {' # Unpack the coordinates obtained. 'var [left, top, height] = res;' # Position it based on the coordinates. 'highlighter.style.height = height + "px";' 'highlighter.style.top = (window.scrollY + top) + "px";' '}' 'return true;' '}' # Remove the highlight if we can't find the text. 'clearHighlight();' # Clear the selection, since we won't use it later. 'clearSelection();' 'return false;' '}' # Return the ``div`` used to produce a highlight, or None if it doesn't exist. 'function getHighlight() {' 'return document.getElementById("highlighter");' '}' # Delete the element used to produce a highlight. 'function clearHighlight() {' 'var highlighter = getHighlight();' 'if (highlighter) {' 'highlighter.remove();' '}' '}') # Scroll the web view to align its cursor with the qutepart cursor or vice # versa. def _scrollSync(self, # None to scroll the text view to the y coordinate of the web view's # cursor. True or False to do the opposite: scroll the web view so that # its cursor aligns vertically with the y coordinate of the text view. In # this case, True will use the tolerance to scroll only if the amount to # scroll exceeds that tolerance; False will scroll irregardless of the # tolerance. alreadyScrolling=None, # Ignored if ``alreadyScrolling == None``. Used as both a padding value and a # scroll tolerance, as described in alreadyScrolling. tolerance=50): # Per the `window geometry # <http://qt-project.org/doc/qt-4.8/application-windows.html#window-geometry>`_, # `geometry() <http://qt-project.org/doc/qt-4.8/qwidget.html#geometry-prop>`_ # is relative to the parent frame. Then, use `mapToGlobal # <http://qt-project.org/doc/qt-4.8/qwidget.html#mapToGlobal>`_ to # put this in global coordinates. This works for `QWebEngineView # <http://doc.qt.io/qt-5/qwebengineview.html>`_, since it # inherits from QWidget. wv = self._dock._widget.webEngineView qp = core.workspace().currentDocument().qutepart qpGlobalTop = qp.mapToGlobal(qp.geometry().topLeft()).y() wvGlobalTop = wv.mapToGlobal(wv.geometry().topLeft()).y() # `qutepart.cursorRect() # <http://qt-project.org/doc/qt-4.8/qplaintextedit.html#cursorRect-2>`_ # gives a value in viewport == widget coordinates. Use that directly. cr = qp.cursorRect() qpCursorHeight = cr.height() qpCursorBottom = cr.top() + qpCursorHeight # Widget height includes the scrollbars. Subtract that off to get a # viewable height for qutepart. qpHeight = qp.geometry().height() hsb = qp.horizontalScrollBar() # The scrollbar height is a constant, even if it's hidden. So, only # include it in calculations if it's visible. if hsb.isVisible(): qpHeight -= qp.horizontalScrollBar().height() page = wv.page() wvHeight = wv.geometry().height() # JavaScript callback to determine the coordinates and height of the # anchor of the selection in the web view. It expects a 3-element tuple # of (left, top, height), or None if there was no selection, where: # top is the coordinate (in pixels) of the top of the selection, measured from the web page's origin; # left is the coordinate (in pixels) of the left of the selection, measured from the web page's origin. def callback(res): # See if a 3-element tuple is returned. Exit if the selection was empty. if not res: return _, wvCursorTop, wvCursorHeight = res wvCursorBottom = wvCursorTop + wvCursorHeight if alreadyScrolling is not None: deltaY = self._alignScrollAmount(qpGlobalTop, qpCursorBottom, wvGlobalTop, wvCursorBottom, wvHeight, wvCursorHeight, tolerance) # Uncomment for helpful debug info. ##print(("qpGlobalTop = %d, qpCursorBottom = %d, qpHeight = %d, deltaY = %d, tol = %d\n" + ## " wvGlobalTop = %d, wvCursorBottom = %d, wvHeight = %d, wvCursorHeight = %d") % ## (qpGlobalTop, qpCursorBottom, qpHeight, deltaY, tolerance, ## wvGlobalTop, wvCursorBottom, wvHeight, wvCursorHeight)) # Only scroll if we've outside the tolerance. if alreadyScrolling or (abs(deltaY) > tolerance): # Note that scroll bars are backwards: to make the text go up, you must # move the bars down (a positive delta) and vice versa. Hence, the # subtration, rather than addition, below. page.runJavaScript('window.scrollTo(0, window.scrollY - {});'.format(deltaY)) # Clear the selection, whether we scrolled or not. self.clearSelection() else: deltaY = self._alignScrollAmount(wvGlobalTop, wvCursorBottom, qpGlobalTop, qpCursorBottom, qpHeight, qpCursorHeight, 0) vsb = qp.verticalScrollBar() # The units for the vertical scroll bar is pixels, not lines. So, do # a kludgy conversion by assuming that all line heights are the # same. vsb.setValue(vsb.value() - round(deltaY/qpCursorHeight)) self._dock._afterLoaded.afterLoaded(lambda: page.runJavaScript('selectionAnchorCoords();', QWebEngineScript.ApplicationWorld, self._callbackManager.callback(callback))) # Clear the current selection in the web view. def clearSelection(self): if not self._unitTest: self._dock._afterLoaded.afterLoaded(self._dock._widget.webEngineView.page().runJavaScript, 'clearSelection();', QWebEngineScript.ApplicationWorld) # # # Synchronizing between the text pane and the preview pane ##======================================================== # A single click in the preview pane should move the text pane's cursor to the # corresponding location. Likewise, movement of the text pane's cursor should # select the corresponding text in the preview pane. To do so, an approximate # search for text surrounding the current cursor or click location perfomed on # text in the other pane provides the corresponding location in the other pane # to highlight. # # Bugs / to-do items ##------------------ # #. I call ``toPlainText()`` several times. In the past, this was quite slow # in a ``QTextEdit``. Check performance and possibly cache this value; it # should be easy to update by adding a few lines to _setHtml(). # # Preview-to-text sync ##-------------------- # This functionaliy relies heavily on the Web to Qt bridge. Some helpful # references: # # * `The QtWebKit Bridge <http://qt-project.org/doc/qt-4.8/qtwebkit-bridge.html>`_ # gives a helpful overview. # * `QWebEngineView`_ is the top-level widget used to embed a Web page in a Qt # application. # # For this sync, the first step is to find the single click's location in a # plain text rendering of the preview's web content. This is implemented in # JavaScript, which emits a Qt signal with the location on a click. A slot # connected to this signal then performs the approximate match and updates the # text pane's cursor. To do this: # # #. ``jsClick``, a PyQt signal with a single numeric argument (the index into # a string containing the plain text rendering of the web page) is defined. # This signal is `connected <onJavaScriptCleared.connect>`_ to the # ``onWebviewClick`` slot. # #. The ``onJavaScriptCleared`` method inserts the JavaScript to listen for a # click and then emit a signal giving the click's location. # #. The ``onWebviewClick`` method then performs the approximate match and # updates the text pane's cursor location. # #. When a new web page is loaded, all JavaScript is lost and must be reinserted. # The ``onJavaScriptCleared`` slot, connected to the # ``javaScriptWindowObjectCleared`` signal, does this. # # The job of this JavaScript handler is to # translate a mouse click into an index into the text rendering of the # webpage. To do this, we must: # # #. Get the current selection made by the mouse click, which is typically # an empty range. (I assume a click and drag will produce a non-empty # range; however this code still works). # #. Extend a copy of this range so that it begins at the start of the # webpage and, of course, ends at the character nearest the mouse # click. # #. Get a string rendering of this range. # #. Emit a signal with the length of this string. # # Note: A JavaScript development environment with this code is available # at http://jsfiddle.net/hgDwx/110/. _jsOnClick = ( # The `window.onclick # <https://developer.mozilla.org/en-US/docs/Web/API/Window.onclick>`_ # event is "called when the user clicks the mouse button while the # cursor is in the window." Although the docs claim that "this event # is fired for any mouse button pressed", I found experimentally # that it on fires on a left-click release; middle and right clicks # had no effect. 'function window_onclick() {' # Clear the current highlight -- it doesn't make sense to have other # text highlighted after a click. 'clearHighlight();' # This performs step 1 above. In particular: # # - `window.getSelection <https://developer.mozilla.org/en-US/docs/Web/API/Window.getSelection>`_ # "returns a `Selection # <https://developer.mozilla.org/en-US/docs/Web/API/Selection>`_ # object representing the range of text selected by the # user." Since this is only called after a click, I assume # the Selection object is non-null. # - The Selection.\ `getRangeAt <https://developer.mozilla.org/en-US/docs/Web/API/Selection.getRangeAt>`_ # method "returns a range object representing one of the # ranges currently selected." Per the Selection `glossary # <https://developer.mozilla.org/en-US/docs/Web/API/Selection#Glossary>`_, # "A user will normally only select a single range at a # time..." The index for retrieving a single-selection range # is of course 0. # - "The `Range <https://developer.mozilla.org/en-US/docs/Web/API/range>`_ # interface represents a fragment of a document that can # contain nodes and parts of text nodes in a given document." # We clone it to avoid modifying the user's existing # selection using `cloneRange # <https://developer.mozilla.org/en-US/docs/Web/API/Range.cloneRange>`_. 'var r = window.getSelection().getRangeAt(0).cloneRange();' # This performs step 2 above: the cloned range is now changed # to contain the web page from its beginning to the point where # the user clicked by calling `setStartBefore # <https://developer.mozilla.org/en-US/docs/Web/API/Range.setStartBefore>`_ # on `document.body # <https://developer.mozilla.org/en-US/docs/Web/API/document.body>`_. 'r.setStartBefore(document.body);' # Step 3: # # - `cloneContents <https://developer.mozilla.org/en-US/docs/Web/API/Range.cloneContents>`_ # "Returns a `DocumentFragment # <https://developer.mozilla.org/en-US/docs/Web/API/DocumentFragment>`_ # copying the nodes of a Range." # - DocumentFragment's parent `Node <https://developer.mozilla.org/en-US/docs/Web/API/Node>`_ # provides a `textContent # <https://developer.mozilla.org/en-US/docs/Web/API/Node.textContent>`_ # property which gives "a DOMString representing the textual # content of an element and all its descendants." This therefore # contains a text rendering of the webpage from the beginning of the # page to the point where the user clicked. 'var rStr = r.cloneContents().textContent.toString();' # Step 4: the length of the string gives the index of the click # into a string containing a text rendering of the webpage. # Call Python with the document's text and that index. 'window.previewSync._onWebviewClick(document.body.textContent.toString(), rStr.length);' '}') _qtJsInit = ( # _`Bug 1`: I can't seem to avoid the error message ``js: Uncaught TypeError: channel.execCallbacks[message.id] is not a function``. It seems like this occurs when a new page is loaded, but the QWebChannel on the Python side sends a message intended for a previously-loaded page. Adding delays / waiting until all JS init finishes / wiating until the event queue is empty helps, but doesn't fix it. Even with these, enough busyness (constant CPU use), this still happends -- perhaps the Python/Qt network backend doesn't send these messages until the CPU is idle? As a workaround, don't define the channel until is needed, making it less likely this will happen. # # _`Bug 2`: Since ``qt`` may not be defined (Qt 5.7.0 doesn't provide the # ``qt`` object to JavaScript when loading per https://bugreports.qt.io/browse/QTBUG-53411), # wrap it in a try/except block. 'function init_qwebchannel() {' # Switch event listeners, part 1/2 -- now that this init is done, don't call it again. 'window.removeEventListener("click", init_qwebchannel);' 'try {' 'new QWebChannel(qt.webChannelTransport, function(channel) {' # Save a reference to the previewSync object. 'window.previewSync = channel.objects.previewSync;' # Switch event listeners, part 2/2 -- Invoke the usual onclick handler. This will only be run if the QWebChannel init succeeds. 'window.addEventListener("click", window_onclick);' # Now that the QWebChannel is ready, use it to handle the click. 'window_onclick();' '});' '} catch (err) {' # Re-throw unrecognized errors. When ``qt`` isn't defined, # JavaScript reports ``js: Uncaught ReferenceError: qt is not # defined``; this works around `bug 2`_. 'throw err;' #if (!(err instanceof ReferenceError)) throw err;' '}' '}' # Set up the sync system after a click. This works around `bug 1`_. See https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener. 'window.addEventListener("click", init_qwebchannel);' ) def _initPreviewToTextSync(self): """Initialize the system per items 1, 2, and 4 above.""" # When a web page finishes loading, reinsert our JavaScript. page = self._dock._widget.webEngineView.page() # Insert our scripts into every loaded page. qwebchannel_js = QFile(':/qtwebchannel/qwebchannel.js') if not qwebchannel_js.open(QIODevice.ReadOnly): raise SystemExit( 'Failed to load qwebchannel.js with error: %s' % qwebchannel_js.errorString()) qwebchannel_js = bytes(qwebchannel_js.readAll()).decode('utf-8') # Set up the QWebChannel. See http://doc.qt.io/qt-5/qtwebchannel-javascript.html. # Run the script containing QWebChannel.js first. beforeScript = QWebEngineScript() beforeScript.setSourceCode(qwebchannel_js + self._jsPreviewSync + self._qtJsInit) beforeScript.setName('qwebchannel.js, previewSync') # Run this JavaScript separated from any JavaScript present in the loaded web page. This provides better security (rogue pages can't access the QWebChannel) and better isolation (handlers, etc. won't conflict, I hope). beforeScript.setWorldId(QWebEngineScript.ApplicationWorld) beforeScript.setInjectionPoint(QWebEngineScript.DocumentCreation) # Per `setWebChannel <http://doc.qt.io/qt-5/qwebenginepage.html#setWebChannel>`_, only one channel is allowed per page. So, don't run this on sub-frames, since it will attempt the creation of more channels for each subframe. beforeScript.setRunsOnSubFrames(False) page.scripts().insert(beforeScript) # Set up the web channel. See https://riverbankcomputing.com/pipermail/pyqt/2015-August/036346.html # and http://stackoverflow.com/questions/28565254/how-to-use-qt-webengine-and-qwebchannel. # For debug, ``set QTWEBENGINE_REMOTE_DEBUGGING=port`` then browse to # http://127.0.0.1:port, where port=60000 works for me. See https://riverbankcomputing.com/pipermail/pyqt/2015-August/036346.html. self.channel = QWebChannel(page) self.channel.registerObject("previewSync", self) # Expose the ``qt.webChannelTransport`` object in the world where these scripts live. page.setWebChannel(self.channel, QWebEngineScript.ApplicationWorld) @pyqtSlot(str, int) def _onWebviewClick(self, tc, webIndex): self._onWebviewClick_(tc, webIndex) # Get the qutepart text. qp = core.workspace().currentDocument().qutepart # Perform an approximate match between the clicked webpage text and the # qutepart text. textIndex = findApproxTextInTarget(tc, webIndex, qp.text) # Move the cursor to textIndex in qutepart, assuming corresponding text # was found. if textIndex >= 0: self._moveTextPaneToIndex(textIndex) # Used for testing -- this will be replaced by a mock. Does nothing. def _onWebviewClick_(self, tc, webIndex): pass def _moveTextPaneToIndex(self, textIndex, noWebSync=True): """Given an index into the text pane, move the cursor to that index. Params: - textIndex - The index into the text pane at which to place the cursor. - noWebSync - True to prevent the web-to-text sync from running as a result of calling this routine. """ # Move the cursor to textIndex. qp = core.workspace().currentDocument().qutepart cursor = qp.textCursor() # Tell the text to preview sync to ignore this cursor position change. cursor.setPosition(textIndex, QtGui.QTextCursor.MoveAnchor) self._previewToTextSyncRunning = noWebSync qp.setTextCursor(cursor) self._previewToTextSyncRunning = False # Scroll the document to make sure the cursor is visible. qp.ensureCursorVisible() # Sync the cursors. self._scrollSync() # Focus on the editor so the cursor will be shown and ready for typing. core.workspace().focusCurrentDocument() # Text-to-preview sync ##-------------------- # The opposite direction is easier, since all the work can be done in Python. # When the cursor moves in the text pane, find its matching location in the # preview pane using an approximate match. Select several characters before and # after the matching point to make the location more visible, since the preview # pane lacks a cursor. Specifically: # # #. initTextToPreviewSync sets up a timer and connects the _onCursorPositionChanged method. # #. _onCursorPositionChanged is called each time the cursor moves. It starts or # resets a short timer. The timer's expiration calls syncTextToWeb. # #. syncTextToWeb performs the approximate match, then calls moveWebPaneToIndex # to sync the web pane with the text pane. # #. moveWebToPane uses QWebFrame.find to search for the text under the anchor # then select (or highlight) it. def _initTextToPreviewSync(self): """Called when constructing the PreviewDoc. It performs item 1 above.""" # Create a timer which will sync the preview with the text cursor a # short time after cursor movement stops. self._cursorMovementTimer = QTimer() self._cursorMovementTimer.setInterval(300) self._cursorMovementTimer.timeout.connect(self.syncTextToPreview) # Restart this timer every time the cursor moves. core.workspace().cursorPositionChanged.connect(self._onCursorPositionChanged) # Set up a variable to tell us when the preview to text sync just fired, # disabling this sync. Otherwise, that sync would trigger this sync, # which is unnecessary. self._previewToTextSyncRunning = False # Run the approximate match in a separate thread. Cancel it if the # document changes. self._runLatest = RunLatest('QThread', self) self._runLatest.ac.defaultPriority = QThread.LowPriority core.workspace().currentDocumentChanged.connect(self._onDocumentChanged) def _onDocumentChanged(self, old, new): self._runLatest.future.cancel(True) self._callbackManager.skipAllCallbacks() self._cursorMovementTimer.stop() def _onCursorPositionChanged(self): """Called when the cursor position in the text pane changes. It (re)schedules a text to web sync per item 2 above. Note that the signal connected to this slot must be updated when the current document changes, since we only want cursor movement notification from the active text document. This is handled in _onDocumentChanged. """ # Ignore this callback if a preview to text sync caused it or if the # preview dock is closed. if not self._previewToTextSyncRunning and self._dock.isVisible(): self._cursorMovementTimer.stop() self._cursorMovementTimer.start() def syncTextToPreview(self): """When the timer above expires, this is called to sync text to preview per item 3 above. It can also be called when a sync is needed (when switching windows, for example). """ # Only run this if we TRE is installed. if not findApproxTextInTarget: return # Stop the timer; the next cursor movement will restart it. self._cursorMovementTimer.stop() # Get a plain text rendering of the web view. Continue execution in a callback. qp = core.workspace().currentDocument().qutepart qp_text = qp.text self._dock._widget.webEngineView.page().toPlainText( self._callbackManager.callback(self._havePlainText)) # Perform an approximate match in a separate thread, then update # the cursor based on the match results. def _havePlainText(self, html_text): # Performance notes: findApproxTextInTarget is REALLY slow. Scrolling # through preview.py with profiling enabled produced:: # # Output from Enki: # 41130 function calls in 3.642 seconds # # Ordered by: standard name # # ncalls tottime percall cumtime percall filename:lineno(function) # 13 0.000 0.000 0.000 0.000 __init__.py:406(text) # 13 0.000 0.000 3.398 0.261 approx_match.py:138(findApproxText) # 13 0.000 0.000 3.432 0.264 approx_match.py:175(findApproxTextInTarget) # 13 0.029 0.002 0.034 0.003 approx_match.py:252(refineSearchResult) # 26 0.000 0.000 0.000 0.000 core.py:177(workspace) # ...snip lots more 0.000 or very small times... # # Therefore, finding ways to make this faster or run it in another # thread should significantly improve the GUI's responsiveness. qp = core.workspace().currentDocument().qutepart qp_text = qp.text qp_position = qp.textCursor().position() self._runLatest.start(self._movePreviewPaneToIndex, # Call findApproxTextInTarget, returning the index and the HTML text searched. lambda: (findApproxTextInTarget(qp_text, qp_position, html_text), html_text)) def _movePreviewPaneToIndex(self, future): """Highlights webIndex in the preview pane, per item 4 above. Params: - webIndex - The index to move the cursor / highlight to in the preview pane. - txt - The text of the webpage, returned by mainFrame.toPlainText(). """ # Retrieve the return value from findApproxTextInTarget. webIndex, txt = future.result view = self._dock._widget.webEngineView page = view.page() ft = txt[:webIndex] def callback(found): if found: # Sync the cursors. self._scrollSync(False) self.textToPreviewSynced.emit() if webIndex >= 0: self._dock._afterLoaded.afterLoaded(lambda: page.runJavaScript('highlightFind({});'.format(repr(ft)), QWebEngineScript.ApplicationWorld, self._callbackManager.callback(callback))) else: self.clearHighlight() def clearHighlight(self): self._dock._afterLoaded.afterLoaded(self._dock._widget.webEngineView.page().runJavaScript, 'clearHighlight();', QWebEngineScript.ApplicationWorld)
from PyQt5.QtWebChannel import QWebChannel import sys # 创建一个 application实例 app = QApplication(sys.argv) win = QWidget() win.setWindowTitle('Web页面中的JavaScript与 QWebEngineView交互例子') # 创建一个垂直布局器 layout = QVBoxLayout() win.setLayout(layout) # 创建一个 QWebEngineView 对象 view = QWebEngineView() htmlUrl = 'http://127.0.0.1:8020/web/index.html' view.load( QUrl( htmlUrl )) # 创建一个 QWebChannel对象,用来传递pyqt参数到JavaScript channel = QWebChannel( ) myObj = MySharedObject() channel.registerObject( "bridge", myObj ) view.page().setWebChannel(channel) # 把QWebView和button加载到layout布局中 layout.addWidget(view) # 显示窗口和运行app win.show() sys.exit(app.exec_())