天天看點

用QTextBrowser實作的Web浏覽器

qt5.6上的webEngine暫時無法用gcc編譯,也沒法用在android上,用了很多辦法都沒法用jni來調用android java的webview,無奈之下用QTextBrowser實作一個最簡單的僅支援HTML4子集的Web浏覽器,QTextBrowser是沒有網絡讀取功能的,我添加一個網絡下載下傳函數,用QtConcurrent異步加載,再用正規表達式解析img标簽,然後signal把QTextDocument呈現出來。

這是原型代碼,寫得比較草率,有興趣的可以擴充一下QTextBrowser/QTextDocument的源代碼得到更多的HTML/CSS渲染效果

#ifndef QHTMLBROWSER_H
#define QHTMLBROWSER_H
 
//** for more information, please refer to http://www.one-lab.net
 
#include <QtConcurrent>
#include <QtNetwork/QtNetwork>
#include <QTextBrowser>
#include <QTextDocument>
#include <QMap>
#include <QRegExp>
#include <QCryptographicHash>
#include <QApplication>
 
class QHtmlBrowser : public QObject
{
    Q_OBJECT
     
private:
    QTextBrowser* mBrowser;
    QString mUrl;
    QMap<QString, QByteArray> mResources;
 
signals:
    void loadFinished();
 
public slots:
    void onLoadFinished();
    void onRequestUrl(const QUrl& url);
 
public:
    ~QHtmlBrowser();
    explicit QHtmlBrowser(QTextBrowser* browser);
    void load(const QString& url);
    void addResource(const QString& key, const QByteArray& bytes);
};
 
#endif // QHTMLBROWSER_H
           
#include "qhtmlbrowser.h"
//** for more information, please refer to http://www.one-lab.net
 
#define hit_ext(p) \
    else if (src.endsWith(#p, Qt::CaseInsensitive)) \
        ext = #p
#define index_tag   "index"
#define images_tag   "images://"
 
QString setUrl(QUrl& parentUrl, const QString& path)
{
    QString srcUrl;
    QString hostPath = parentUrl.host();
    if (parentUrl.port() != 80)
        hostPath += ":" + QString::number(parentUrl.port());
    hostPath += parentUrl.path();
    int pos = hostPath.lastIndexOf('/');
    if (pos > -1)
        hostPath = hostPath.left(pos);
    srcUrl = hostPath + "/" + path;
    while (srcUrl.contains("//"))
        srcUrl.replace("//", "/");
    srcUrl = "http://" + srcUrl;
    return srcUrl;
}
 
void downloadHtml(QHtmlBrowser* browser, const QString& urlPath, int timeoutSeconds)
{
    QNetworkAccessManager network;
    QNetworkRequest request;
    QUrl url(urlPath);
    request.setUrl(url);
    QByteArray requestContent = urlPath.toUtf8();
    request.setHeader(QNetworkRequest::ContentLengthHeader, requestContent.length());
    bool timeout = false;
    if (QNetworkReply* reply = network.get(request))
    {
        QDateTime stamp = QDateTime::currentDateTime();
        while (reply->isRunning())
        {
            QApplication::processEvents();
            if (stamp.secsTo(QDateTime::currentDateTime()) > timeoutSeconds)
            {
                reply->abort();
                timeout = true;
                break;
            }
        }
        if (!timeout)
        {
            QString content = QString::fromUtf8(reply->readAll());
            QRegExp imgTagRegex("\\<img[^\\>]*src\\s*=\\s*\"([^\"]*)\"[^\\>]*\\>", Qt::CaseInsensitive);
            imgTagRegex.setMinimal(true);
            QStringList imgSrcList;
            QStringList imgTagList;
            int offset = 0;
            while( (offset = imgTagRegex.indexIn(content, offset)) != -1){
                offset += imgTagRegex.matchedLength();
                imgTagList.append(imgTagRegex.cap(0));
                imgSrcList.append(imgTagRegex.cap(1));
            }
            QString srcUrl;
            QString resourcePrefix(images_tag);
            QString ext;
            int index = -1;
            foreach(QString src, imgSrcList)
            {
                index++;
                srcUrl.clear();
                if (src.startsWith("http://", Qt::CaseInsensitive))
                    srcUrl = src;
                else
                {
                    if (0){}
                    hit_ext(.png);
                    hit_ext(.jpeg);
                    hit_ext(.jpg);
                    hit_ext(.gif);
                    else continue;
                    if (src.startsWith("://") || src.startsWith("file://", Qt::CaseInsensitive)) continue;
                    srcUrl = setUrl(url, src);
                    request.setUrl(srcUrl);
                    requestContent = srcUrl.toUtf8();
                    request.setHeader(QNetworkRequest::ContentLengthHeader, requestContent.length());
                    QCryptographicHash hash(QCryptographicHash::Md5);
                    if (QNetworkReply* itemReply = network.get(request))
                    {
                        timeout = false;
                        stamp = QDateTime::currentDateTime();
                        while (itemReply->isRunning())
                        {
                            QApplication::processEvents();
                            if (stamp.secsTo(QDateTime::currentDateTime()) > timeoutSeconds)
                            {
                                itemReply->abort();
                                timeout = true;
                                break;
                            }
                        }
                        if (timeout)
                        {
                            delete itemReply;
                            continue;
                        }
                        QString imgFile = resourcePrefix + "/";
                        hash.addData(srcUrl.toUtf8());
                        foreach(char c, hash.result())
                        {
                            int cInt = (int)c;
                            if (cInt < 10)
                                imgFile += "0" + QString::number(cInt, 16).left(1);
                            else
                                imgFile += QString::number(cInt, 16).left(2);
                        }
                        imgFile += ext;
                        QString target = imgTagList[index];
                        target.replace(src, imgFile);
                        browser->addResource(imgFile, itemReply->readAll());
                        content.replace(imgTagList[index], target);
                        delete itemReply;
                    }
                }
            }
            browser->addResource(index_tag, content.toUtf8());
        }
        delete reply;
        emit browser->loadFinished();
    }
}
 
void QHtmlBrowser::onLoadFinished()
{
    QTextDocument* document = new QTextDocument(mBrowser);
    if (mResources.contains(index_tag))
    {
        foreach(QString key, mResources.keys())
        {
            if (key.startsWith(images_tag))
                document->addResource(QTextDocument::ImageResource, key, QVariant(mResources[key]));
        }
        document->setHtml(QString::fromUtf8(mResources[index_tag]));
        mBrowser->setDocument(document);
    }
}
 
QHtmlBrowser::~QHtmlBrowser()
{
}
 
QHtmlBrowser::QHtmlBrowser(QTextBrowser* browser) : QObject(browser)
{
    mBrowser = browser;
    mBrowser->setReadOnly(true);
    mBrowser->setContextMenuPolicy(Qt::NoContextMenu);
    mBrowser->setAcceptRichText(true);
    mBrowser->setAutoFormatting(QTextEdit::AutoAll);
    connect(this, SIGNAL(loadFinished()), SLOT(onLoadFinished()));
    connect(mBrowser, SIGNAL(anchorClicked(QUrl)), SLOT(onRequestUrl(QUrl)));
}
 
void QHtmlBrowser::onRequestUrl(const QUrl& url)
{
    QUrl currentUrl(mUrl);
    QString requestingUrl = setUrl(currentUrl, url.toString());
    load(requestingUrl);
}
 
void QHtmlBrowser::load(const QString &url)
{
    mUrl = url;
    QtConcurrent::run(downloadHtml, this, url, 30);
}
 
void QHtmlBrowser::addResource(const QString &key, const QByteArray &bytes)
{
    if (mResources.contains(key))
        mResources[key] = bytes;
    else
        mResources.insert(key, bytes);
}