前言
之前工作中有开发过qt内嵌浏览器页面的功能,接触过QWebEngine 模块,但由于qt版本较旧,自己的理解也不深入,导致开发效果不佳,最终使用了将外部浏览器窗口内嵌进qt界面的伪内嵌方案。最近学习转到了Qt6的较新版本开发,于是想重拾一下这个模块,做一个可以真正意义上的Qt内嵌浏览器。
本项目基于 Qt 6.10.2 + MSVC 2022 + QMake 构建。虽然功能不如 Chrome 强大,但实现了核心的多标签页管理、地址栏导航、网页缩放以及拦截新窗口请求(将弹窗转为标签页)等功能。
写这篇博客主要是为了记录实现过程中的关键思路和代码片段,方便以后复习 Qt 的信号槽机制和 QWebEngine 模块的使用。
一、原理与核心类介绍
Qt 提供了 QtWebEngineWidgets 模块,其底层基于 Chromium,能渲染绝大多数现代网页。本项目主要用到了以下三个核心类:
QWebEngineView :主视图组件,用于显示网页内容。它像一个容器,负责渲染。
QWebEnginePage :页面的逻辑核心。这是实现"多标签页"的关键。默认情况下,当网页触发 window.open 或点击 target="_blank" 链接时,它会创建一个独立的新窗口。我们需要继承这个类,重写 createWindow 函数,拦截这个行为,改为发射信号通知主窗口创建新标签。
QTabWidget:Qt 自带的标签页容器,用于管理多个 QWebEngineView 实例,实现切换、关闭标签页等功能。
核心逻辑流程图 :
用户点击"新建标签" -> 主窗口创建 WebView -> 添加到 QTabWidget。
网页内部请求新窗口 -> WebPage::createWindow 被调用 -> 发射 newTabRequest 信号 -> 主窗口接收信号 -> 创建新 WebView 并接管该 Page。
二、代码演示
1.项目配置 (.pro)
需要在 .pro 文件中引入 webenginewidgets 模块,并开启 C++11 支持。
cpp
QT += core gui widgets webenginewidgets
greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
CONFIG += c++11
SOURCES += main.cpp browser.cpp webview.cpp
HEADERS += browser.h webview.h
2.main函数
cpp
#include <QApplication>
#include "browser.h"
int main(int argc, char *argv[])
{
// 设置高 DPI 支持
QApplication::setHighDpiScaleFactorRoundingPolicy(Qt::HighDpiScaleFactorRoundingPolicy::PassThrough);
// 设置组织信息(可选,用于保存配置等)
QApplication::setOrganizationName("MyCompany");
QApplication::setOrganizationDomain("mycompany.com");
QApplication::setApplicationName("QtTabBrowser");
QApplication app(argc, argv);
Browser browser;
browser.show();
return app.exec();
}
3.核心拦截机制 (webview.h/cpp)
这是本项目最核心的部分。自定义 WebPage 类来拦截新窗口请求。
webview.h:
cpp
#ifndef WEBVIEW_H
#define WEBVIEW_H
#include <QWebEngineView>
#include <QWebEnginePage>
#include <QWebEngineProfile>
// 前置声明
class WebView;
/**
* @brief 自定义 WebPage 类
* 主要用途:拦截网页中通过 JS (window.open) 或 HTML target="_blank" 发起的新窗口请求,
* 将其转换为内部信号,以便在主窗口中以新标签页形式打开。
*/
class WebPage : public QWebEnginePage
{
Q_OBJECT
public:
explicit WebPage(QWebEngineProfile *profile, QObject *parent = nullptr);
protected:
/**
* @brief 重写 createWindow 以拦截新窗口请求
* @param type 窗口类型 (WebDialogType, WebBrowserTabType 等)
* @return 返回新创建的 Page 指针,由调用者接管或内部处理
*/
QWebEnginePage *createWindow(WebWindowType type) override;
signals:
/**
* @brief 当检测到新窗口请求时发射此信号
* @param page 新创建的 WebPage 对象指针
*/
void newTabRequest(WebPage *page);
};
/**
* @brief 自定义 WebView 类
* 封装 QWebEngineView,强制使用自定义的 WebPage 以实现标签页管理功能。
*/
class WebView : public QWebEngineView
{
Q_OBJECT
public:
explicit WebView(QWidget *parent = nullptr);
/**
* @brief 获取当前关联的自定义 WebPage 对象
* @return WebPage 指针,如果转换失败返回 nullptr
*/
WebPage *webPage() const;
};
#endif // WEBVIEW_H
webview.cpp:
cpp
#include "webview.h"
#include <QDebug>
// --- WebPage 实现 ---
WebPage::WebPage(QWebEngineProfile *profile, QObject *parent)
: QWebEnginePage(profile, parent)
{
// 初始化自定义 Page
}
QWebEnginePage *WebPage::createWindow(WebWindowType type)
{
Q_UNUSED(type);
qDebug() << "[WebPage] 拦截到新窗口请求,准备创建新标签页...";
// 1. 使用当前 Profile 创建新的 WebPage 实例
// 注意:parent 设置为 this 是为了方便调试追踪,实际所有权由 Browser 接管
WebPage *newPage = new WebPage(this->profile(), this);
// 2. 发射信号,通知 Browser 主窗口创建对应的 WebView 并接管此 Page
emit newTabRequest(newPage);
// 3. 返回新页面指针,QWebEngineView 会将它用于渲染新内容
return newPage;
}
// --- WebView 实现 ---
WebView::WebView(QWidget *parent)
: QWebEngineView(parent)
{
// 【关键】强制替换默认的 QWebEnginePage 为自定义的 WebPage
// 这样才能拦截 createWindow 信号
WebPage *customPage = new WebPage(this->page()->profile(), this);
setPage(customPage);
// 可选:设置默认缩放因子
setZoomFactor(1.0);
}
WebPage *WebView::webPage() const
{
// 安全转换,确保获取的是我们自定义的 Page
return qobject_cast<WebPage*>(page());
}
四.主窗口逻辑 (browser.h/cpp)
主窗口负责界面布局(工具栏 + 标签页)和信号槽的连接。
关键功能点:
地址栏联动 :监听 urlChanged 信号,实时更新地址栏文本。
标签页管理 :监听 newTabRequest 信号,动态添加 Tab。
缩放控制:通过 setZoomFactor 调整视图大小,并用 eventFilter 实现点击标签重置缩放。
browser.h:
cpp
#ifndef BROWSER_H
#define BROWSER_H
#include <QMainWindow>
#include <QTabWidget>
#include <QLineEdit>
#include <QPushButton>
#include <QToolBar>
#include <QLabel>
#include <QUrl>
#include <QEvent>
#include <QMouseEvent>
#include "webview.h"
/**
* @brief 浏览器主窗口类
* 负责管理标签页、地址栏导航、缩放控制及事件分发。
*/
class Browser : public QMainWindow
{
Q_OBJECT
public:
explicit Browser(QWidget *parent = nullptr);
~Browser();
protected:
// 事件过滤器:用于处理 ZoomLabel 的点击重置事件
bool eventFilter(QObject *obj, QEvent *event) override;
private slots:
// 导航相关
void loadUrl(); // 加载地址栏 URL
void createNewTab(const QUrl &url = QUrl("about:blank")); // 创建新标签
void handleNewTabRequest(WebPage *page); // 处理来自 WebPage 的新标签请求
void closeCurrentTab(int index = -1); // 关闭指定标签
// 缩放相关
void zoomIn(); // 放大
void zoomOut(); // 缩小
void resetZoom(); // 重置缩放
void updateZoomLabel(); // 更新缩放比例显示标签
private:
void setupUi(); // 初始化界面
// 界面组件指针
QTabWidget *tabWidget;
QLineEdit *urlBar;
QPushButton *goButton;
QToolBar *toolBar;
QLabel *zoomLabel;
// 配置常量
static const QString DEFAULT_HOME_URL;
};
#endif // BROWSER_H
browser.cpp:
cpp
#include "browser.h"
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QWebEngineProfile>
#include <QDebug>
#include <QShortcut>
#include <QFont>
#include <QMessageBox>
// 定义默认主页
const QString Browser::DEFAULT_HOME_URL = QStringLiteral("https://www.baidu.com");
Browser::Browser(QWidget *parent)
: QMainWindow(parent)
{
setupUi();
// 启动时创建一个默认标签页
createNewTab(QUrl(DEFAULT_HOME_URL));
}
Browser::~Browser()
{
// Qt 的对象树机制会自动销毁子对象,此处通常无需手动删除
}
/**
* @brief 事件过滤器
* 专门用于捕获 zoomLabel 的鼠标点击事件,实现点击重置缩放功能
*/
bool Browser::eventFilter(QObject *obj, QEvent *event)
{
if (obj == zoomLabel && event->type() == QEvent::MouseButtonPress) {
auto *mouseEvent = static_cast<QMouseEvent*>(event);
if (mouseEvent->button() == Qt::LeftButton) {
resetZoom();
return true; // 事件已消费
}
}
return QMainWindow::eventFilter(obj, event);
}
void Browser::setupUi()
{
setWindowTitle(QStringLiteral("Qt 6.10 Advanced Browser"));
resize(1200, 800);
// --- 工具栏初始化 ---
toolBar = addToolBar(QStringLiteral("Navigation"));
toolBar->setMovable(false);
toolBar->setIconSize(QSize(16, 16));
// 1. 导航按钮组
auto *backBtn = new QPushButton(QStringLiteral("←"));
auto *forwardBtn = new QPushButton(QStringLiteral("→"));
auto *reloadBtn = new QPushButton(QStringLiteral("↻"));
auto *homeBtn = new QPushButton(QStringLiteral("🏠"));
auto *newTabBtn = new QPushButton(QStringLiteral("+"));
auto *closeTabBtn = new QPushButton(QStringLiteral("×"));
// 设置提示文本
backBtn->setToolTip(QStringLiteral("后退 (Alt+Left)"));
forwardBtn->setToolTip(QStringLiteral("前进 (Alt+Right)"));
reloadBtn->setToolTip(QStringLiteral("刷新 (F5)"));
homeBtn->setToolTip(QStringLiteral("主页"));
newTabBtn->setToolTip(QStringLiteral("新标签页 (Ctrl+T)"));
closeTabBtn->setToolTip(QStringLiteral("关闭当前标签 (Ctrl+W)"));
toolBar->addWidget(backBtn);
toolBar->addWidget(forwardBtn);
toolBar->addWidget(reloadBtn);
toolBar->addWidget(homeBtn);
toolBar->addSeparator();
// 2. 地址栏
urlBar = new QLineEdit();
urlBar->setPlaceholderText(QStringLiteral("输入网址并按回车..."));
urlBar->setMinimumWidth(300);
toolBar->addWidget(urlBar);
goButton = new QPushButton(QStringLiteral("Go"));
toolBar->addWidget(goButton);
toolBar->addSeparator();
// 3. 缩放控制组
auto *zoomOutBtn = new QPushButton(QStringLiteral("-"));
zoomOutBtn->setToolTip(QStringLiteral("缩小 (Ctrl+-)"));
zoomOutBtn->setFont(QFont(QStringLiteral("Arial"), 14, QFont::Bold));
zoomOutBtn->setFixedWidth(30);
zoomLabel = new QLabel(QStringLiteral("100%"));
zoomLabel->setAlignment(Qt::AlignCenter);
zoomLabel->setMinimumWidth(45);
zoomLabel->setFont(QFont(QStringLiteral("Arial"), 10));
zoomLabel->setCursor(Qt::PointingHandCursor);
zoomLabel->setToolTip(QStringLiteral("点击重置为 100%"));
// 安装事件过滤器以支持点击
zoomLabel->installEventFilter(this);
auto *zoomInBtn = new QPushButton(QStringLiteral("+"));
zoomInBtn->setToolTip(QStringLiteral("放大 (Ctrl++)"));
zoomInBtn->setFont(QFont(QStringLiteral("Arial"), 14, QFont::Bold));
zoomInBtn->setFixedWidth(30);
toolBar->addWidget(zoomOutBtn);
toolBar->addWidget(zoomLabel);
toolBar->addWidget(zoomInBtn);
toolBar->addSeparator();
// 4. 标签管理按钮
toolBar->addWidget(newTabBtn);
toolBar->addWidget(closeTabBtn);
// --- 中心区域:标签页容器 ---
tabWidget = new QTabWidget();
tabWidget->setTabsClosable(true);
tabWidget->setDocumentMode(true); // 现代扁平化风格
tabWidget->setMovable(true); // 允许拖拽排序
setCentralWidget(tabWidget);
// --- 辅助 Lambda:获取当前活动的 WebView ---
auto getCurrentView = [this]() -> WebView* {
return qobject_cast<WebView*>(tabWidget->currentWidget());
};
// --- 信号与槽连接 ---
// 1. 基础导航
connect(goButton, &QPushButton::clicked, this, &Browser::loadUrl);
connect(urlBar, &QLineEdit::returnPressed, this, &Browser::loadUrl);
connect(backBtn, &QPushButton::clicked, [getCurrentView]() {
if (auto *view = getCurrentView()) view->back();
});
connect(forwardBtn, &QPushButton::clicked, [getCurrentView]() {
if (auto *view = getCurrentView()) view->forward();
});
connect(reloadBtn, &QPushButton::clicked, [getCurrentView]() {
if (auto *view = getCurrentView()) view->reload();
});
connect(homeBtn, &QPushButton::clicked, [this]() {
createNewTab(QUrl(DEFAULT_HOME_URL));
});
connect(newTabBtn, &QPushButton::clicked, [this]() { createNewTab(); });
connect(closeTabBtn, &QPushButton::clicked, [this]() {
closeCurrentTab(tabWidget->currentIndex());
});
// 2. 标签页关闭信号
connect(tabWidget, &QTabWidget::tabCloseRequested, this, &Browser::closeCurrentTab);
// 3. 标签切换联动 (同步地址栏和缩放显示)
connect(tabWidget, &QTabWidget::currentChanged, [this, getCurrentView](int index) {
if (index == -1) {
urlBar->clear();
zoomLabel->clear();
return;
}
if (auto *view = getCurrentView()) {
urlBar->setText(view->url().toString());
updateZoomLabel();
}
});
// 4. 缩放功能连接
connect(zoomInBtn, &QPushButton::clicked, this, &Browser::zoomIn);
connect(zoomOutBtn, &QPushButton::clicked, this, &Browser::zoomOut);
// 5. 快捷键设置
new QShortcut(QKeySequence::ZoomIn, this, SLOT(zoomIn()));
new QShortcut(QKeySequence::ZoomOut, this, SLOT(zoomOut()));
new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_0), this, SLOT(resetZoom()));
new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_T), this, [this]() { createNewTab(); });
new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_W), this, [this]() { closeCurrentTab(); });
}
// --- 缩放功能实现 ---
void Browser::zoomIn()
{
if (auto *view = qobject_cast<WebView*>(tabWidget->currentWidget())) {
double factor = view->zoomFactor();
if (factor < 5.0) { // 最大限制 500%
view->setZoomFactor(factor + 0.1);
updateZoomLabel();
}
}
}
void Browser::zoomOut()
{
if (auto *view = qobject_cast<WebView*>(tabWidget->currentWidget())) {
double factor = view->zoomFactor();
if (factor > 0.1) { // 最小限制 10%
view->setZoomFactor(factor - 0.1);
updateZoomLabel();
}
}
}
void Browser::resetZoom()
{
if (auto *view = qobject_cast<WebView*>(tabWidget->currentWidget())) {
view->setZoomFactor(1.0);
updateZoomLabel();
}
}
void Browser::updateZoomLabel()
{
auto *view = qobject_cast<WebView*>(tabWidget->currentWidget());
if (!view) {
zoomLabel->setText(QStringLiteral("--"));
return;
}
int percent = qRound(view->zoomFactor() * 100);
zoomLabel->setText(QString(QStringLiteral("%1%")).arg(percent));
// 视觉反馈:非 100% 时高亮显示
if (percent == 100) {
zoomLabel->setStyleSheet(QStringLiteral("color: black; font-weight: normal;"));
} else {
zoomLabel->setStyleSheet(QStringLiteral("color: #007ACC; font-weight: bold;"));
}
}
// --- 标签页管理实现 ---
void Browser::createNewTab(const QUrl &url)
{
auto *view = new WebView(this);
// 连接 URL 变化信号,同步地址栏
connect(view, &QWebEngineView::urlChanged, [this, view](const QUrl &u) {
if (tabWidget->currentWidget() == view) {
urlBar->setText(u.toString());
}
});
// 连接标题变化信号,同步标签页标题
connect(view, &QWebEngineView::titleChanged, [this, view](const QString &title) {
int idx = tabWidget->indexOf(view);
if (idx != -1) {
tabWidget->setTabText(idx, title.isEmpty() ? QStringLiteral("新标签页") : title);
}
});
// 连接自定义 Page 的新窗口请求信号
if (auto *page = view->webPage()) {
connect(page, &WebPage::newTabRequest, this, &Browser::handleNewTabRequest);
}
int index = tabWidget->addTab(view, QStringLiteral("新标签页"));
tabWidget->setCurrentIndex(index);
updateZoomLabel(); // 初始化缩放显示
if (!url.isEmpty()) {
view->load(url);
}
}
void Browser::handleNewTabRequest(WebPage *page)
{
// 当网页请求新窗口时,复用该 Page 创建新的 Tab
auto *view = new WebView(this);
view->setPage(page); // 接管 Page 所有权
view->setZoomFactor(1.0);
// 绑定必要的信号
connect(view, &QWebEngineView::urlChanged, [this, view](const QUrl &u) {
if (tabWidget->currentWidget() == view) urlBar->setText(u.toString());
});
connect(view, &QWebEngineView::titleChanged, [this, view](const QString &title) {
int idx = tabWidget->indexOf(view);
if (idx != -1) tabWidget->setTabText(idx, title.isEmpty() ? QStringLiteral("新窗口") : title);
});
// 递归连接,支持链式打开新标签
connect(page, &WebPage::newTabRequest, this, &Browser::handleNewTabRequest);
int index = tabWidget->addTab(view, QStringLiteral("加载中..."));
tabWidget->setCurrentIndex(index);
updateZoomLabel();
}
void Browser::loadUrl()
{
auto *view = qobject_cast<WebView*>(tabWidget->currentWidget());
if (!view) return;
QString text = urlBar->text().trimmed();
if (text.isEmpty()) return;
// 简单的协议补全逻辑
if (!text.startsWith(QStringLiteral("http://")) && !text.startsWith(QStringLiteral("https://"))) {
// 如果包含点号可能是域名,否则视为搜索关键词 (这里简化处理,统一加 https)
text = QStringLiteral("https://") + text;
}
view->load(QUrl(text));
view->setFocus(); // 加载后焦点回到网页
}
void Browser::closeCurrentTab(int index)
{
if (index == -1) index = tabWidget->currentIndex();
if (index != -1 && index < tabWidget->count()) {
QWidget *widget = tabWidget->widget(index);
tabWidget->removeTab(index);
if (widget) {
widget->deleteLater(); // 安全延迟删除
}
// 如果关闭了最后一个标签,自动新建一个
if (tabWidget->count() == 0) {
createNewTab();
} else {
// 切换到其他标签后,更新状态栏信息
updateZoomLabel();
}
}
}
三、效果展示
运行程序后,界面如下所示:
-
基础浏览与多标签
支持输入网址访问,点击 + 号新建标签。

-
拦截新窗口测试
在百度搜索"Qt",点击某些会唤起新窗口的链接(或构造一个 target="_blank" 的测试页),可以看到并没有弹出独立窗口,而是在浏览器内部打开了新标签。

-
缩放功能
使用 Ctrl + / Ctrl - 或点击工具栏按钮,右下角百分比标签会实时变化;点击百分比标签可一键重置为 100%。

四、总结与心得
完成本次小项目的实践后,我有了一些更深的体会:
组合优于继承 :虽然继承了 QWebEngineView,但更多时候是通过组合 QTabWidget 和多个 View 实例来实现复杂功能。
信号槽的灵活性 :利用 Lambda 表达式在 connect 中直接编写简单的逻辑(如获取当前 Tab、更新文字),让代码非常紧凑易读。
事件过滤器的妙用 :原本需要为 QLabel 单独写一个点击槽函数,通过 installEventFilter 直接在父类处理鼠标事件,减少了类的耦合。
内存管理:Qt 的对象树机制(QObject Parent-Child)大大简化了内存释放,只要设置好 parent,大部分对象在窗口关闭时会自动销毁,但动态创建的 Tab 页在关闭时需要注意 deleteLater() 的使用。
后续优化方向 :
添加历史记录和书签功能。
增加下载管理器的支持。
优化加载时的 Loading 动画提示。
这个项目虽然简单,但作为一个 能在Qt项目中随时插入的简易功能,已经很有使用价值了。