Введение
О кроссплатформенной библиотеке Qt слышали, наверное, многие. О движке отображения веб-страниц WebKit тем более. Не так давно первое стало содержать обертку над вторым, примеры создания браузеров в 50 строчек найти не сложно. Тем не менее о том, как получать доступ к отдельным элементам веб-страницы из Qt-кода написано не много.
В данном описании я предполагаю, что люди обладают начальными познаниями в PyQt (я учил по Саммерфилду), и смутным представлением о JavaScript. Свой уровень я характеризую, именно таким, так что заранее извиняюсь за ошибки, особенно в описании ява-скрипта. Несмотря на то что в качестве языка использован Python у программистов C++/Qt вопросов тоже быть не должно.
Тестовые примеры запускались на PyQt-4.7.3, версия Python-2.6.6-r1 под ОС GNU/Linux. Из программ понадобится браузер с отладкой JS (Chrome, например) и PyQt IDE на ваше усмотрение, я использую Eric4.
Пример 1. Браузер, над которым мы будем издеваться
# -*- coding: utf-8 -*-
from PyQt4.QtCore import *
from PyQt4.QtNetwork import *
from PyQt4.QtGui import *
from PyQt4.QtWebKit import *
class BaseBrowser(QWidget):
def __init__(self, parent = None):
super(BaseBrowser, self).__init__(parent)
self.__progress = 0
QNetworkProxyFactory.setUseSystemConfiguration(True)
self.webView = QWebView()
self.webView.load(QUrl("http://www.yandex.ru"))
self.connect(self.webView, SIGNAL("loadFinished(bool)"), self.adjustLocation)
self.connect(self.webView, SIGNAL("titleChanged(QString)"), self.adjustTitle)
self.connect(self.webView, SIGNAL("loadProgress(int)"), self.setProgress)
self.connect(self.webView, SIGNAL("loadFinished(bool)"), self.finishLoading)
self.locationEdit = QLineEdit()
self.locationEdit.setSizePolicy(QSizePolicy.Expanding, self.locationEdit.sizePolicy().verticalPolicy())
self.connect(self.locationEdit, SIGNAL("returnPressed()"), self.changeLocation)
self.goButton = QPushButton("Go")
self.connect(self.goButton, SIGNAL("clicked()"), self.changeLocation)
self.layout = QGridLayout(self)
self.layout.addWidget(self.locationEdit, 0, 0)
self.layout.addWidget(self.goButton, 0, 1)
self.layout.addWidget(self.webView, 1, 0, 1, 2)
self.setLayout(self.layout)
def adjustLocation(self):
self.locationEdit.setText(self.webView.url().toString())
def changeLocation(self):
url = self.locationEdit.text()
if url[0:7] != 'http://':
url = 'http://' + url
self.webView.load(QUrl(url))
self.webView.setFocus()
def adjustTitle(self):
if self.__progress <= 0 or self.__progress >= 100:
self.setWindowTitle(self.webView.title())
else:
self.setWindowTitle(QString("%1 (%2%)").arg(self.webView.title()).arg(self.__progress))
def setProgress(self, p):
self.__progress = p
self.adjustTitle()
def finishLoading(self):
self.__progress = 100
self.adjustTitle()
if __name__ == "__main__":
import sys
app = QApplication(sys.argv)
prog = BaseBrowser()
prog.show()
sys.exit(app.exec_())
* This source code was highlighted with Source Code Highlighter.
Код примера 1: pastebin.com/GVQ4dw1M
Браузер представляет некую вариацию на тему браузеров из обучающих примеров по C++/Qt и PyQt, в последующих двух примеров мы будем его наследовать. Я понимаю, что так программы, даже маленькие, не пишут, и программа не должна быть одним классом, но баланс между кол-вом кода, его наглядностью и правильностью архитектуру я соблюдаю как могу.
Итак, браузер наш умеет не многое, но может загружать и отображать введенную страницу, для этого используется виджет QWebView, стандартные сигналы создаваемые этим виджетом мы привязали к слотам нашего браузера, что позволяет программе знать программе о смене заголовку текущей веб-страницы SIGNAL(«titleChanged(QString)»), прогрессе загрузки SIGNAL(«loadProgress(int)») и окончании загрузки — SIGNAL(«loadFinished(bool)»). Кроме этого создается поле QlineEdit для ввода адресса страницы и кнопка для перехода к этой веб-странице, либо по нажатию «Enter» либо по щелчку на кнопке.
Запускаем браузер, пробуем в работе, офигиваем от скорости работы «голого» WebKit. Пока ничего особенного мы не написали. Наш браузер даже по ссылкам не по всем переходит.
Пример 2. DOM-деревья и доступ к их элементам из Qt
Вообще, о структуре HTML страниц лучше бы, почитать отдельно, в двух предложениях это описать проблематично. В общем-то, если вы будете делать из офлайновую оболочку к какому либо веб-интерфейсу, ява-скрипт нужно будет все-таки выучить, по-крайней мере ту его часть, которая относится к доступу к данным. Итак, любой современный браузер позволяет получить доступ к содержимому веб-страницы представляя его в в виде дерева узлов, каждый узел которого представляет собой элемент, атрибут, текстовый, графический или любой другой объект. Узлы связаны между собой отношениями родительский-дочерний (да, эта строка из википедии). При помощи интерпретатора JavaScript к узлам этого дерева можно получить доступ. Откроем наш браузер и зайдем на все тот же yandex.ru (надеюсь их не накроет хабраэффектом). Сколько вы видите ссылок над поисковой строкой?
Щелкните по списку ссылок и откройте их в меню разработчика (в Chrome это — «проверить элемент» в контекстном списке). Так мы увидим положение текущего элемента в дереве. Список имеет незамысловатый id = «tabs» и является таблицей. Переключитесь в JavaScript консоль и попробуйте выбрать эту таблицу:
document.getElementById("tabs").
Посмотрите сколько в ней строк:
document.getElementById("tabs").rows.length
И сколько столбцов:
document.getElementById("tabs").rows(0).cells.length.
Теперь получим такой же результат в нашем браузере.
# -*- coding: utf-8 -*-
from basebrowser import *
class SimpleJavaScript(BaseBrowser):
def __init__(self, parent = None):
super(SimpleJavaScript, self).__init__(parent)
self.jsButton = QPushButton("ExecuteJS")
self.connect(self.jsButton, SIGNAL("clicked()"), self.jsScript)
self.jsStringEdit = QLineEdit()
self.jsStringEdit.setSizePolicy(QSizePolicy.Expanding, self.jsStringEdit.sizePolicy().verticalPolicy())
self.jsStringEdit.setText("document.getElementById(\"tabs\").rows(0).cells.length")
self.connect(self.jsStringEdit, SIGNAL("returnPressed()"), self.jsScript)
self.jsReturnText = QTextEdit()
self.layout.addWidget(self.jsStringEdit, 2, 0, 1, 1)
self.layout.addWidget(self.jsButton, 2, 1, 1, 1)
self.layout.addWidget(self.jsReturnText, 3, 0, 1, 2)
def jsScript(self):
jsString = self.jsStringEdit.text()
jsReturn = self.webView.page().currentFrame().evaluateJavaScript(jsString)
self.jsReturnText.setPlainText(jsReturn.toString())
if __name__ == "__main__":
import sys
app = QApplication(sys.argv)
ui = SimpleJavaScript()
ui.show()
sys.exit(app.exec_())
* This source code was highlighted with Source Code Highlighter.
Код примера 2: pastebin.com/p4P1ZEtS
Итак вычисление JS кода происходит в функции webView.page().currentFrame().evaluateJavaScript(jsString)
Функция evaluateJavaScript(string) принимает в качестве единственного аргумента строку QString, содержащую код на языке JavaScript. Этот код будет выполнен на текущей странице а результат выполнения будет возвращен в виде переменной QVariant. При этом, к сожалению, получить в качестве результата поддерево DOM-элементов у вас не получится, но любую текстовую или числовую информацию — пожалуйста.
Пример 3. Создание офлайн контролов
Адрес домашней страницы на этот раз выбран таким поскольку у меня карточка ATI и сижу я под Линуксом, кто знает, тот поймет, что это не от большой любви. На самом деле на странице множество контролов типа Select, для одного из которых мы создадим эквивалент.
# -*- coding: utf-8 -*-
from basebrowser import *
from PyQt4.QtGui import *
from PyQt4.QtCore import *
class JSSelectList(QAbstractListModel):
def __init__ (self, _id, _jsFunc, parent = None):
super(JSSelectList, self).__init__(parent)
self.id = _id
self.jsFunc = _jsFunc
def data(self, index, role=Qt.DisplayRole):
if not index.isValid():
return QVariant()
if role == Qt.DisplayRole:
jsstring = QString("document.getElementById('%1').options[%2].textContent").arg(self.id).arg(index.row())
jsreturn = self.jsFunc(jsstring)
return jsreturn.toString().trimmed()
def rowCount(self, index=QModelIndex()):
jsstring = QString("document.getElementById('%1').length").arg(self.id)
jsreturn = self.jsFunc(jsstring)
ok = False
count, ok = jsreturn.toInt()
return count if ok else 0
def headerData(self, section, orientation, role=Qt.DisplayRole):
if role != Qt.DisplayRole:
return QVariant()
else:
return self.id
class JSComboBoxDemo(BaseBrowser):
def __init__(self, parent = None):
super(JSComboBoxDemo, self).__init__(parent)
self.vendorComboBox = QComboBox()
id = QString("productLine")
self.vendorListModel = JSSelectList(id, self.webView.page().currentFrame().evaluateJavaScript)
self.vendorComboBox.setModel(self.vendorListModel)
self.connect(self.vendorComboBox, SIGNAL("currentIndexChanged(int)"), self.setSelectOnWebPage);
self.connect(self.webView, SIGNAL("loadFinished(bool)"), self.initComboBox)
self.layout.addWidget(self.vendorComboBox, 2, 0, 1, 1)
self.webView.load(QUrl("http://www.amd.com"))
def setSelectOnWebPage(self, new_id):
jsstring = QString("document.getElementById('productLine').selectedIndex=%1").arg(new_id)
self.webView.page().currentFrame().evaluateJavaScript(jsstring)
def initComboBox(self):
self.vendorComboBox.setCurrentIndex(0)
if __name__ == "__main__":
import sys
app = QApplication(sys.argv)
ui = JSComboBoxDemo()
ui.show()
sys.exit(app.exec_())
* This source code was highlighted with Source Code Highlighter.
Код примера 3: pastebin.com/YzA9hL3H
При создании таких элементов GUI как таблица, список, dropdown (не знаю как правильно перевести) Qt позволяет использовать удобный MVC подход. Вам нужно лишь описать доступ к вашей модели данных — вам нужно лишь наследовать ваше представление данных от встроенного абстрактного класса и прицепить его к стандартному контролу (у Саммерфилда, это вроде бы 14 глава). В данном случае используется QAbstractListModel, из параметров ей передается только функция исполнения JS и название select`а на странице. Все переопределения стандарты.
В самом примере тоже все достаточно понятно, кроме двух соединений типа сигнал-слот, на которые хотелось бы обратить ваше внимание.
Во-первых, бесполезно пытаться выполнить JavaScript до загрузки страницы, поэтому воспользуемся тем, что при окончании загрузки виджет QWebView формирует сигнал SIGNAL(«loadFinished(bool)»), о котором я уже говорил в первом примере.
self.connect(self.webView, SIGNAL("loadFinished(bool)"), self.initComboBox)
В противном случае, если запихнуть строку
self.vendorComboBox.setCurrentIndex(0)
в __init__ ни какой инициализации первым значением не произойдет — evaluateJavaScript ничего не вернет, так как страница еще не успеет загрузиться.
Во-вторых, нам нужна синхронизация в обе стороны:
self.connect(self.vendorComboBox, SIGNAL("currentIndexChanged(int)"), self.setSelectOnWebPage)
Аналогичным образом можно синхронизировать практически всю информацию на странице, нажимать кнопки, загружать информацию.
Буду рад, если информация окажется для кого-то полезной. Всех с Рождеством и прошедшим Новым Годом.
Использованная литература:
Ж. Бланшет, М. Саммерфилд. Qt 4: Программирование GUI на C++.
Mark Summerfield. Rapid GUI Programming with Python and Qt.
Другие источники:
Различные интернет сайты по JavaScript и PyQt, исходный код интернет-браузера Arora.