Plugging into the Web

Материал из Wiki.crossplatform.ru

Перейти к: навигация, поиск
В этой категории собраны статьи из издания Qt Quarterly

__NOTOC__

Содержание

Plugging into the Web

by David Boddie

The appearance of WebKit in Qt 4.4 opens up the online world toQt applications, blurring the boundaries between traditional desktopapplications and online services. In this article, we'll look at one wayto use this hybrid approach to application building.

As Web browsers become more feature-rich, and user interfaces created withWeb 2.0 technologies become more common, many developersnow use the Web as a graphical toolkit for their applications. Though onlineuser interfaces have improved since the early days of the Web, sometimesusers really do need to have access to fully native widgets and controls.

center

Through simple examples, we will show how to include Qt widgets inWeb-centric user interfaces, and take a look at one way to integrate Qtcomponents with dynamic Web content.

QtWebKit Basics

QtWebKit provides integration between Qt and WebKit on two different levels.On a low level, Qt provides widgets for Web pages to be rendered onto; on ahigh level, a set of classes are provided that represent all the key componentsof a Web browser.

QWebView is a widget that is used to display Web pages, QWebPage represents the content in a page, and QWebFrame representsan individual frame in a Web page. The code to display a Web page is verysimple:

    int main(int argc, char *argv[])
    {
        QApplication app(argc, argv);
        QWebView view;
        view.load(QUrl("http://www.trolltech.com/"));
        view.show();
        return app.exec();
    }

The widget provides fundamental Web browsing features, such as Cascading StyleSheet and JavaScript support. Other technologies can be added to providea more comprehensive experience.

Widgets on a Page

Since Qt is used to render pages, it is plain sailing to add both standard andcustom widgets to pages. All we need is some markup to indicate where a widgetis expected in a page and a mechanism that lets us know when it needs tobe created.

The markup used involves the <object> element, described in the HTML 4specification, which is used to include generic objects in Web pages. Whendescribing an object to represent a widget, there are typically threeattributes this element can have: a data attribute that indicates whereany relevant data can be obtained; width and height attributes canbe used to set the size of the widget on the page.

Here's how we might describe such an object:

<object type="text/csv;header=present;charset=utf8"
        data="qrc:/data/accounts.csv"
        width="100%" height="300"></object>

The mechanism used by QtWebKit to insert widgets into pages is a plugin factorythat is registered with a given WebPage instance. Factories are subclassesof QWebPluginFactory and can be equipped to supply more than one type ofwidget.

A Simple Example

To demonstrate how the factory is used, we create a simple widget that can beused to display Comma-Separated Values (CSV) files. The widget class,CSVView, is just a subclass of QTableView with extra functions toset up an internal data model. Instances of the factory class, CSVFactory,are responsible for creating CSVView widgets and requesting data on theirbehalf.

The CSVFactory class is defined in the following way:

    class CSVFactory : public QWebPluginFactory
    {
        Q_OBJECT
 
    public:
        CSVFactory(QObject *parent = 0);
        QObject *create(const QString &mimeType,
            const QUrl &url, const QStringList &argumentNames,
            const QStringList &argumentValues) const;
        QList<QWebPluginFactory::Plugin> plugins() const;
 
    private:
        QNetworkAccessManager *manager;
    };

The public functions give a good overview of how QtWebKit will use the factoryto create widgets. We begin by looking at the factory's constructor:

    CSVFactory::CSVFactory(QObject *parent)
        : QWebPluginFactory(parent)
    {
        manager = new QNetworkAccessManager(this);
    };

The factory contains a network access manager which we will use to obtain datafor each of the plugin widgets created.

The plugins() function is used to report informationabout the kinds of widget plugins it can create; our implementation reportsthe MIME type it expects and provides a description of the plugin:

    QList<QWebPluginFactory::Plugin> CSVFactory::plugins()
                                                     const
    {
        QWebPluginFactory::MimeType mimeType;
        mimeType.name = "text/csv";
        mimeType.description = "Comma-separated values";
        mimeType.fileExtensions = QStringList() << "csv";
 
        QWebPluginFactory::Plugin plugin;
        plugin.name = "CSV file viewer";
        plugin.description = "A CSV file Web plugin.";
        plugin.mimeTypes = QList<MimeType>() << mimeType;
 
        return QList<QWebPluginFactory::Plugin>() << plugin;
    }

The create() function is where most of the action happens. It iscalled with a MIME type that describes the kind of data to be displayed,a URL that refers to the data, and information about any additional argumentsthat were specified in the Web page. We begin by checking the basic MIMEtype information passed in the mimeType parameter, and only continue ifwe recognize it.

    QObject *CSVFactory::create(const QString &mimeType,
        const QUrl &url, const QStringList &argumentNames,
        const QStringList &argumentValues) const
    {
        if (mimeType != "text/csv")
            return 0;
 
        CSVView *view = new CSVView(
                argumentValues[argumentNames.indexOf("type")]);

We construct a view widgetusing the fully-specified MIME type, which is guaranteed to be in the list ofarguments if a MIME type has been supplied.

      QNetworkRequest request(url);
        QNetworkReply *reply = manager->get(request);
        connect(reply, SIGNAL(finished()),
                view, SLOT(updateModel()));
        connect(reply, SIGNAL(finished()),
                reply, SLOT(deleteLater()));
 
        return view;
    }

Lastly, we use the network access manager to request the data specified by theurl parameter, connecting its finished() signal to the view'supdateModel() slot so that it can collect the data. The reply object isintentionally created on the heap; the finished() signal is connected toits deleteLater() slot, ensuring that Qt will dispose of it when it is nolonger needed.

The CSVView class provides only minor extensions to the functionality of QTableView, with a public slot to handle incoming data and a privatevariable to record exact MIME type information:

    class CSVView : public QTableView
    {
        Q_OBJECT
 
    public:
        CSVView(const QString &mimeType, QWidget *parent = 0);
 
    public slots:
        void updateModel();
 
    private:
        void addRow(bool firstLine, QStandardItemModel *model,
                    const QList<QStandardItem *> &items);
 
        QString mimeType;
    };

The constructor is simply used to record the MIME type of the data:

    CSVView::CSVView(const QString &mimeType, QWidget *parent)
        : QTableView(parent)
    {
        this->mimeType = mimeType;
    }

To save space, we will only look at parts of the updateModel() function,which begins by obtaining the QNetworkReply object that caused the slotto be invoked before checking for errors:

    void CSVView::updateModel()
    {
        QNetworkReply *reply =
            static_cast<QNetworkReply *>(sender());
 
        if (reply->error() != QNetworkReply::NoError)
            return;
 
        bool hasHeader = false;
        QString charset = "latin1";

Assuming that the data is correct, we need to determine whether theCSV file includes a table header, and to find out which character encoding wasused to store the data. Both these pieces of information may be included inthe complete MIME type information, so we parse this before continuing—thisis shown in the online example code.

      ...
        QTextStream stream(reply);
        stream.setCodec(
            QTextCodec::codecForName(charset.toLatin1()));
 
        QStandardItemModel *model =
            new QStandardItemModel(this);
        ...

Since QNetworkReply is a QIODevice subclass, the reply can be readusing a suitably configured text stream, and the data fed into a standardmodel. The mechanics of this are outside the scope of this article, so weskip to the end of the function where we close the reply object and set themodel on the view:

      reply->close();
 
        setModel(model);
        resizeColumnsToContents();
        horizontalHeader()->setStretchLastSection(true);
    }

Once the reply has been read, and the model populated with data, very littleneeds to be done by the plugin. Ownership of the view widget is handledelsewhere, and we have ensured that the model will be destroyed when it is nolonger needed by making it a child object of the view.

Let's look quickly at the MainWindow implementation:

    MainWindow::MainWindow(QWidget *parent)
        : QMainWindow(parent)
    {
        QWebSettings::globalSettings()->setAttribute(
            QWebSettings::PluginsEnabled, true);
 
        QWebView *webView = new QWebView;
        CSVFactory *factory = new CSVFactory(webView, this);
        webView->page()->setPluginFactory(factory);
        QFile file(":/pages/index.html");
        file.open(QFile::ReadOnly);
        webView->setHtml(file.readAll());
 
        setCentralWidget(webView);
    }

Apart from creating and setting a factory on the QWebPage object, themost important task is to enable Web plugins. If this global setting is notenabled, plugins will not be used and our <object> elements will simplybe ignored.

Bridging the Gap

Being able to insert widgets into a Web page is quite a useful trick, butwe may also want to let them communicate with or modify other elements onthe page.Currently, the way we enable this is via WebKit's JavaScript engine. To showthis, we modify the previous example so that selecting a row in the tableview causes three input elements in an HTML form to be updated.

To perform this communication, we require an updated CSVView widget thatcan emit a signal whenever a row is selected, a JavaScript function to modifyelements on the page, and some glue code to make the connection.

On the page, the plugin and form are declared like this:

<object type="text/csv;header=present;charset=utf8"
        data="qrc:/data/accounts.csv"
        width="100%" height="300">
<param name="related" value="customers"></param>
</object>
...
<form> ...
<input id="customers_name" name="name" ... > ...
<input id="customers_address" name="address" ... > ...
<input id="customers_quantity" name="quantity" ... > ...
</form>

In the <object> definition, we include a <param> element that refersto the identifiers of input elements within a related <form> element.The related attribute has a value of "customers" and the relevantform element contains <input> elements with id values that arederived from this identifier.

In this example, when the factory creates the view widget, we take theopportunity to pass the identifier to the CSVView widget'sconstructor:

    QObject *CSVFactory::create(...) const
    {
        ...
 
        CSVView *view = new CSVView(arguments["type"],
                                    arguments["related"]);

We also expose the view widget to the frame in the page that containsthe elements, and set up a connection between the view and a JavaScriptfunction defined in the page header:

      QWebFrame *frame = webView->page()->mainFrame();
        frame->addToJavaScriptWindowObject(
            arguments["related"] + "_input", view);
        frame->evaluateJavaScript(
            arguments["related"] +
            "_input.rowSelected.connect(fillInForm);\n");
        ...
    }

For the page shown above, the view is added to the page ascustomers_input, and the connection code is effectively the same as

    customers_input.rowSelected.connect(fillInForm);

where fillInForm is the name of the JavaScript function to modify theform's input elements. This function expects four arguments: the identifierof the form to be modified and the name, address and quantity values for arow of data.

The rowSelected() signal is a new signal that we add to the CSVViewclass, and it is used to give the fillInForm() function some data to workwith. We also provide an internal slot which performs the work of gettingdata out of the model and emitting the signal:

    class CSVView : public QTableView
    {
       Q_OBJECT
 
    public:
        CSVView(const QString &mimeType,
                const QString &related, QWidget *parent = 0);
 
    signals:
        void rowSelected(const QString &form,
            const QString &name, const QString &address,
            const QString &quantity);
 
    public slots:
        void updateModel();
 
    private slots:
        void exportRow(const QModelIndex &current);
    ...
    };

In the view's updateModel() slot, in addition to entering the CSV datainto a QStandardItemModel, the currentChanged() signal of the view'sselection model is connected to the exportRow() slot.

Thus, whenever the user selects a row in the table, the exportRow() slotis called, the data found in the selected row is extracted from the model andemitted in the rowSelected() signal as three QStrings, and theJavaScript fillInForm() function is called with these values.The appropriate type conversions occur behind the scenes to ensure that each QString is converted to a JavaScript string object.

We now give the JavaScript fillInForm() function to show what it doeswith the strings it is given. The function itself is defined in the HTMLpage header:

    function fillInForm(form, name, address, quantity)
    {
        var nameElement =
            document.getElementById(form+"_name");
        var addressElement =
            document.getElementById(form+"_address");
        var quantityElement =
            document.getElementById(form+"_quantity");
 
        nameElement.value = name;
        addressElement.value = address;
        quantityElement.value = quantity;
    }

We use the identifier passed in the form argument to derive names forthe id attributes used in the form's input elements, and obtain theelements using the HTML Document Object Model (DOM) API. The values ofthese elements are updated with the name, address and quantitystrings supplied.

Linking Things Together

Qt provides a range of widgets that can be embedded using QWebPluginFactory to help create domain-specific Web browsers toaccompany applications.OpenGL widgets can be used to create 3D model browsers, for instance, andthe Graphics View framework provides a solid foundation for other kinds ofinteractive content viewers.

Although we have used the widgets to demonstrate the use of signals andslots for communication between Qt components and JavaScript in the browser, we do not need to embed widgets in Web pages to be able to dothis.By inserting objects into pages and evaluating JavaScript, Qtapplications can be made to examine and process information foundonline.

Since WebKit is a fully-featured browser engine, this approachcan be extended to enable integration between applications and Web services,particularly those with well-defined JavaScript APIs.The techniques shown in this article can also be used to collect,collate and merge information from multiple online services.

Qt's support for Web-based content improves as WebKit itself evolves, anddevelopment of the QtWebKit module continues apace. Many of the latestfeatures are available in the nightly Qt snapshots, and developers oftenannounce exciting new ones on their Qt Labs blogs.

The code for our examples is available from theQt Quarterly Web site. Qt snapshots are, as always, available from the Qt Software FTP server.

Обсудить на форуме...