文章目录
- 1 基本使用
- 1.1 WebEngine Widgets
- 1.2 QWebEngineView
- 1.3 QWebEnginePage
- 1.4 QWebEngineSettings
- 1.5 QWebEngineScript
- 1.6 QWebEngineScriptCollection
- 1.7 QWebEngineFullScreenRequest
- 1.8 QWebEngineCertificateError
- 1.9 QWebEngineClientCertificateSelection
- 1.10 QWebEngineDownloadItem
- 1.11 QWebEngineContextMenuData
- 1.12 QChannel
- 1.13 Chromium
- 1.14 URI解析
- 1.15 QtWebEngineProcessd.exe
- 1.16 QWebEngineHistory
- 2 问题解决
- 3 透明失效
- 4 未完待续
- 5 参考链接
1 基本使用
本节先完整梳理下Qt
内嵌Web
相关模块、类和基本用法,
模块架构
1.1 WebEngine Widgets
WebEngine Widgets
提供了用于将Web
内容嵌入QWidget
等窗口控件,
1)组件组成
类名 | 功能 |
---|---|
QWebEngineView | 用于展示web ,提供加载url 、导航历史、页面缩放等接口 |
QWebEnginePage | 用于表示web 页面,提供执行js 方法、处理表单、截图等接口 |
QWebEngineProfile | 用于表示浏览器配置文件,提供设置缓存、cookie 和安全策略等接口 |
QWebEngineSettings | 用于表示浏览器设置,提供设置web 字体、颜色、js 和插件等接口 |
2)组件关系
Qt WebEngine
基于QNetworkAccessManager
和QSslSocket
处理网络通信、证书验证等。
1.2 QWebEngineView
QWebEngineView
是Qt
内嵌web
的关键控件,支持HTML5
、js
和CSS3
。
1)示例代码
QWebEngineView view;
view.load(QUrl("https://www.baidum.com"));
view.show();
1.3 QWebEnginePage
QWebEnginePage
用于表示加载到QWebEngineView
中的web
页面。
1)示例代码
QWebEngineView view;
QWebEnginePage page;
view.setPage(&page);
page.load(QUrl("https://www.baidu.com"));
view.show();
2)常用方法
名称 | 作用 |
---|---|
loadStarted /loadProgress /loadFinished | 用于监听页面加载状态 |
urlChanged | 用于监听页面url 变更 |
runJavaScript | 用于执行页面中的js 代码 |
3)懒加载
从page
源码学习下Qt
懒加载C++
代码写法,
QWebEnginePage* QWebEngineView::page() const
{
Q_D(const QWebEngineView);
if (!d->page) {
QWebEngineView *that = const_cast<QWebEngineView*>(this);
that->setPage(new QWebEnginePage(that));
d->m_ownsPage = true;
}
return d->page;
}
1.4 QWebEngineSettings
QWebEngineSettings
用于配置web
设置选项,包括字体、缩放、js
支持和插件支持等。
1)常用方法
名称 | 作用 |
---|---|
setFontFamily | 用于设置字体、样式 |
setFontSize | 用于设置字体大小 |
setAttribute | 用于开启/关闭特性,包括js 支持、插件支持(flash 和视频解码器等)、本地存储、离线web 应用、错误页面等 |
setUnknownUrlSchemePolicy | 用于配置跨域访问策略 |
2)示例代码
QWebEngineView view;
QWebEngineSettings *settings = view.page()->settings();
settings->setFontFamily(QWebEngineSettings::StandardFont, "黑体");
settings->setFontSize(QWebEngineSettings::DefaultFontSize, 20);
settings->setAttribute(QWebEngineSettings::JavascriptEnabled, true);
settings->setAttribute(QWebEngineSettings::PluginsEnabled, true);
view.load(QUrl("https://www.baidu.com"));
view.show();
1.5 QWebEngineScript
QWebEngineScript
支持通过注入js
脚本到网页来新增/修改现有功能,可以配置脚本执行时机(web
加载开始或结束)。
1)示例代码
QWebEngineView view;
QWebEnginePage page;
QWebEngineScript script;
script.setName("testScript");
script.setSourceCode("alert('test script');");
script.setInjectionPoint(QWebEngineScript::DocumentReady);
script.setRunsOnSubFrames(false);
script.setWorldId(QWebEngineScript::ApplicationWorld);
page.scripts().insert(script);
view.setPage(&page);
page.load(QUrl("https://www.baidu.com"));
view.show();
1.6 QWebEngineScriptCollection
用于管理 QWebEnginePage
实例中的所有脚本。
1.7 QWebEngineFullScreenRequest
当web
触发全屏模式请求时,如HTML5``video
元素的全屏按钮,QWebEnginePage
发出fullScreenRequested
信号,QWebEngineFullScreenRequest
对象作为该信号的惨数传递,用于处理web
发出的全屏显示请求。
1)常用方法
名称 | 作用 |
---|---|
reject | 用于拒绝全屏请求 |
accept | 用于接受全屏请求 |
toggleOn | 用于·表示请求是进入全屏模式还是退出全屏模式 |
origin | 用于获取发出全屏请求的web 元素 |
2)示例代码
#include <QWebEngineView>
#include <QWebEnginePage>
#include <QWebEngineFullScreenRequest>
class TestWebEngineView : public QWebEngineView
{
Q_OBJECT
public:
MyWebEngineView() {
connect(page(), SIGNAL(fullScreenRequested(QWebEngineFullScreenRequest)),
this, SLOT(slotFullScreenRequested(QWebEngineFullScreenRequest));
}
private slots:
void slotFullScreenRequested(QWebEngineFullScreenRequest request) {
if (request.toggleOn()) {
showFullScreen();
} else {
showNormal();
}
request.accept();
}
};
1.8 QWebEngineCertificateError
QWebEngineCertificateError
用于处理SSL
证书错误,当QWebEnginePage
加载使用 SSL
加密的网页时,遇到证书错误,函数certificateError
会被执行,QWebEngineCertificateError
对象作为参数传递。
1)常用方法
名称 | 作用 |
---|---|
error | 用于获取错误类型 |
errorDescription | 用于获取错误描述 |
url | 用于获取错误url |
ignoreCertificateError | 用于忽略错误并继续加载web |
2)示例代码
#include <QWebEngineView>
#include <QWebEnginePage>
#include <QWebEngineCertificateError>
class TestWebEnginePage : public QWebEnginePage
{
Q_OBJECT
protected:
bool certificateError(const QWebEngineCertificateError &error) override
{
if (error.error() == QWebEngineCertificateError::CertificateAuthorityInvalid)
{
qDebug() << "Ignoring certificate error:" << error.errorDescription();
return true;
}
return false;
}
};
1.9 QWebEngineClientCertificateSelection
QWebEngineClientCertificateSelection
用于处理客户端证书选择,当服务器要求客户端提供证书进行身份验证时,QWebEnginePage 会发出一个 selectClientCertificate
信号,携带参数 QWebEngineClientCertificateSelection
对象,可使用该类选择一个合适的客户端证书,返回给服务器。
1)常用方法
名称 | 作用 |
---|---|
certificates | 用于获取客户端可用证书列表 |
select | 用于从可用证书列表中选择1个,返回给服务器 |
host | 用于获取服务器发请求的url |
2)示例代码
#include <QWebEngineView>
#include <QWebEnginePage>
#include <QWebEngineClientCertificateSelection>
class TestWebEnginePage : public QWebEnginePage
{
Q_OBJECT
public:
TestWebEnginePage()
{
connect(this, &QWebEnginePage::selectClientCertificate,
this, &MyWebEnginePage::handleClientCertificateSelection);
}
private slots:
void handleClientCertificateSelection(QWebEngineClientCertificateSelection selection)
{
if (!selection.certificates().isEmpty())
{
selection.select(selection.certificates().first());
} else
{
selection.select(QSslCertificate());
}
}
};
1.10 QWebEngineDownloadItem
QWebEngineDownloadItem
用于表示下载项。当用户触发下载请求(点击下载链接),QWebEngineProfile
发出downloadRequested
信号,携带参数QWebEngineDownloadItem
对象,可用于获取下载项信息、控制下载过程。
1)常用方法
名称 | 作用 |
---|---|
state | 用于获取下载项的当前状态,如下载中、已完成、已取消等 |
totalBytes | 用于获取下载项的总字节信息 |
receivedBytes | 用于获取提供了下载项的已接收字节信息 |
url | 用于获取下载项的源url |
setPath | 用于设置下载项的保存路径 |
2)示例代码
#include <QApplication>
#include <QWebEngineView>
#include <QWebEngineProfile>
#include <QWebEngineDownloadItem>
#include <QStandardPaths>
class MyWebEngineProfile : public QWebEngineProfile
{
Q_OBJECT
public:
MyWebEngineProfile()
{
connect(this, &QWebEngineProfile::downloadRequested,
this, &MyWebEngineProfile::handleDownloadRequested);
}
private slots:
void handleDownloadRequested(QWebEngineDownloadItem *download)
{
QString downloadPath = QStandardPaths::writableLocation(QStandardPaths::DownloadLocation);
download->setPath(downloadPath + "/" + download->downloadFileName());
download->accept();
connect(download, &QWebEngineDownloadItem::downloadProgress,
this, &MyWebEngineProfile::handleDownloadProgress);
connect(download, &QWebEngineDownloadItem::finished,
this, &MyWebEngineProfile::handleDownloadFinished);
}
void handleDownloadProgress(qint64 bytesReceived, qint64 bytesTotal)
{
qDebug() << "Download progress:" << bytesReceived << "/" << bytesTotal;
}
void handleDownloadFinished()
{
qDebug() << "Download finished.";
}
};
1.11 QWebEngineContextMenuData
QWebEngineContextMenuData
用于提供上下文菜单数据,当用户在QWebEngineView
中右击,QWebEnginePage
发出contextMenuRequested
信号,携带参数QWebEngineContextMenuData
对象,可用该对象获取上下文菜单信息,根据需要创建和显示上下文菜单。
1)常用方法
名称 | 作用 |
---|---|
position | 用于获取触发位置对应鼠标坐标 |
selectedText | 用于获取触发位置附近的选中文本 |
linkText | 用于获取触发位置所包含链接的文本 |
linkUrl | 用于获取触发位置所包含链接的url |
mediaUrl | 用于获取触发位置所包含图像的url |
mediaType | 用于获取触发位置所包含媒体元素的类型 |
2)示例代码
#include <QWebEngineView>
#include <QWebEnginePage>
#include <QWebEngineContextMenuData>
#include <QMenu>
class TestWebEnginePage : public QWebEnginePage
{
Q_OBJECT
public:
TestWebEnginePage()
{
connect(this, &QWebEnginePage::contextMenuRequested,
this, &TestWebEnginePage::handleContextMenuRequested);
}
private slots:
void handleContextMenuRequested(const QPoint &pos)
{
QWebEngineContextMenuData contextData = contextMenuData();
QMenu menu;
if (!contextData.linkUrl().isEmpty())
menu.addAction("Open link");
if (!contextData.selectedText().isEmpty())
menu.addAction("Copy selected text");
menu.addAction("Refresh");
menu.exec(mapToGlobal(pos));
}
};
1.12 QChannel
支持js
与C++
之间的互操作。
在web
和客户端之间通过signal
/slot
绑定,用于互相之间方法调用。
1)C++侧
创建一个继承自QObject
的类,用于在Qt
和web
之间传递数据和调用函数。
TestObject.h
#include <QObject>
#include <QString>
class TestObject : public QObject
{
Q_OBJECT
public:
TestObject(QObject *parent = nullptr);
void test();
public slots:
void slotTest(const QString &message);
signals:
void signalTest(const QString &message);
};
TestObject.cpp
#include "TestObject.h"
#include <QDebug>
TestObject::MyObject(QObject *parent)
: QObject{parent}
{
}
void TestObject::test()
{
emit signalTest("test");
}
void TestObject::slotTest(const QString &message)
{
qDebug() << message;
}
main.cpp
#include <QApplication>
#include <QWebChannel>
#include <QWebEngineView>
#include "TestObject.h"
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
QWebEngineView *webView = new QWebEngineView();
QWebChannel *channel = new QWebChannel();
TestObject *myObject = new TestObject();
channel->registerObject(QStringLiteral("testObject"), myObject);
webView->page()->setWebChannel(channel);
webView->load(QUrl("file:///C:/Users/Test/Desktop/test.html"));
webView->show();
return app.exec();
}
2)web侧
在HTML
中引用qwebchannel.js
脚本,即可透明地访问QObject的属性、公共槽和方法。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>bm qt + web test</title>
<script src="./qwebchannel.js"></script>
<script type="text/javascript">
var testObject;
var webChannel = new QWebChannel(qt.webChannelTransport, function (channel) {
testObject = channel.objects.testObject;
});
function slotTest() {
testObject.slotTest("Test");
}
function signalTest() {
testObject.signalTest("Test");
}
</script>
</head>
<body>
<button id="slotTest" onclick="slotTest()">slot Test</button>
<button id="signalTest" onclick="signalTest()">signal Test</button>
</body>
</html>
3)slot未写全
web
调用客户端的slot
方法,若客户端没写全,会有错误提示;反之,客户端调用web
的signal
方法,若客户端没写全,则不会有错误提示。
4)快速响应
可以通过在url
中增加参数来通知web
该初始化成什么样子,但是这种加载往往需要重新加载web
、比较耗时,一般的变化建议通过signal
到web
,以实现web
快速响应。
1.13 Chromium
1)WebContentsAdapter
Qt WebEngineCore
模块基于Chromium
,Qt
将对Chromium
开源库接口调用逻辑封装在类WebContentsAdapter
(C:\Qt\Qt5.14.2\5.14.2\Src\qtwebengine\src\core\web_contents_adapter.h
)。
在路径C:\Qt\Qt5.14.2\5.14.2\Src\qtwebengine\src\3rdparty\chromium
下,可以看到Qt
使用了chromium
作为三方库。在该chromium
目录下,可以看到chromium
完整的源代码。
2)Version
在Qt
官网可以直接查看对应的Chromium
版本,
也可以在官方帮助文档中搜索 Qt WebEngine Overview
,其中的WebEngine Core Module
介绍如下,
使用QWebEngineView
加载网址,https://www.w3school.com.cn/js/js_browser.asp
可以直接在线检测版本,
import QtWebEngine 1.4
WebEngineView {
id: webView;
anchors.fill : parent;
url : "https://www.w3school.com.cn/js/js_browser.asp"
}
1.14 URI解析
从Qt
的parse
源码学习下uri
解析,
inline void QUrlPrivate::parse(const QString &url, QUrl::ParsingMode parsingMode)
{
// URI-reference = URI / relative-ref
// URI = scheme ":" hier-part [ "?" query ] [ "#" fragment ]
// relative-ref = relative-part [ "?" query ] [ "#" fragment ]
// hier-part = "//" authority path-abempty
// / other path types
// relative-part = "//" authority path-abempty
// / other path types here
sectionIsPresent = 0;
flags = 0;
clearError();
// find the important delimiters
int colon = -1;
int question = -1;
int hash = -1;
const int len = url.length();
const QChar *const begin = url.constData();
const ushort *const data = reinterpret_cast<const ushort *>(begin);
for (int i = 0; i < len; ++i) {
uint uc = data[i];
if (uc == '#' && hash == -1) {
hash = i;
// nothing more to be found
break;
}
if (question == -1) {
if (uc == ':' && colon == -1)
colon = i;
else if (uc == '?')
question = i;
}
}
// check if we have a scheme
int hierStart;
if (colon != -1 && setScheme(url, colon, /* don't set error */ false)) {
hierStart = colon + 1;
} else {
// recover from a failed scheme: it might not have been a scheme at all
scheme.clear();
sectionIsPresent = 0;
hierStart = 0;
}
int pathStart;
int hierEnd = qMin<uint>(qMin<uint>(question, hash), len);
if (hierEnd - hierStart >= 2 && data[hierStart] == '/' && data[hierStart + 1] == '/') {
// we have an authority, it ends at the first slash after these
int authorityEnd = hierEnd;
for (int i = hierStart + 2; i < authorityEnd ; ++i) {
if (data[i] == '/') {
authorityEnd = i;
break;
}
}
setAuthority(url, hierStart + 2, authorityEnd, parsingMode);
// even if we failed to set the authority properly, let's try to recover
pathStart = authorityEnd;
setPath(url, pathStart, hierEnd);
} else {
userName.clear();
password.clear();
host.clear();
port = -1;
pathStart = hierStart;
if (hierStart < hierEnd)
setPath(url, hierStart, hierEnd);
else
path.clear();
}
if (uint(question) < uint(hash))
setQuery(url, question + 1, qMin<uint>(hash, len));
if (hash != -1)
setFragment(url, hash + 1, len);
if (error || parsingMode == QUrl::TolerantMode)
return;
// The parsing so far was partially tolerant of errors, except for the
// scheme parser (which is always strict) and the authority (which was
// executed in strict mode).
// If we haven't found any errors so far, continue the strict-mode parsing
// from the path component onwards.
if (!validateComponent(Path, url, pathStart, hierEnd))
return;
if (uint(question) < uint(hash) && !validateComponent(Query, url, question + 1, qMin<uint>(hash, len)))
return;
if (hash != -1)
validateComponent(Fragment, url, hash + 1, len);
}
1.15 QtWebEngineProcessd.exe
1)启动
QWebEngineView
对象初始化后,就会启动QtWebEngineProcessd.exe
(debug
,release
对应不带后缀d
),
以Qt5.14.2
为例,上述QtWebEngineProcessd.exe
在目录C:\Qt\Qt5.14.2\5.14.2\msvc2017_64\bin
下。
QWebEngineView
默认的url
为"about:blank",
“about:blank” 是一个特殊的网页地址,通常表示一个空白的网页或一个没有加载任何内容的网页。
在大多数浏览器中,当你打开一个新的标签页或窗口,而没有输入任何网址时,浏览器默认会加载 “about:blank”。这个地址实际上并不指向任何实际的网页内容,而只是一个占位符,用于表示一个空的或未加载的页面。
在一些情况下,网页可能会在 “about:blank” 的基础上进行加载或显示一些默认的内容。例如,一些浏览器插件或扩展可能会在 “about:blank” 的页面上显示一些信息或广告。
总之,“about:blank” 是一个特殊的网页地址,用于表示一个空的或未加载的页面。
QtWebEngine
采用Chromium
提供的多进程模块,该模块要求QtWebEngineProcess.exe
随应用一起部署。每个QWebEngineView
或WebEngineView
实例都会启动一个QtWebEngineProcess.exe
进程,用来和内置浏览器QWebEngineView
通信。QtWebEngineProcess.exe
可以重命名,
QString webEngineProcessPath = QCoreApplication::applicationDirPath() + "/" + "TestWebEngineProcess.exe";
qputenv("QTWEBENGINEPROCESS_PATH", webEngineProcessPath.toLocal8Bit());
2)退出
程序若想正常退出QtWebEngineProcessd.exe
,需要在程序退出时调用QApplication
的quit()
方法,像Visual Studio
停止调试或进程强杀等都不会退出QtWebEngineProcessd.exe
,造成内存、CPU
被无效占用。
3)通信
QtWebEngineProcessd.exe
跟主进程之间是如何通信的?
渲染进程和主进程之间的通信机制主要基于Chromium
的多进程架构。
Chromium
采用了一种分离式的多进程设计,每个Web
页面(或标签页)都在一个独立的渲染进程中运行。这种设计可以提高安全性和稳定性,但同时也要求实现一套有效的进程间通信(IPC
)机制。
Chromium
的IPC
(Inter-Process Communication
)基于消息传递,涉及以下组件,
组件名 | 作用 |
---|---|
Channel | 用于传递消息,采用管道(pipe )或共享内存(shared memory )等底层技术实现 |
MessageLoop | 事件循环,监听Channel 消息,并将其分发给相应的处理器。 |
RendererHost /Renderer | 封装渲染进程和主进程之间通信逻辑,分别位于主进程和渲染进程中,定义了一系列消息类型(加载url 、执行js 等)和相应的处理函数。 |
1.16 QWebEngineHistory
QWebEngineHistory
用于管理浏览历史记录,其中每个历史记录项都由 QWebEngineHistoryItem
表示。
1)常用方法
名称 | 作用 |
---|---|
clear | 用于清除浏览历史记录 |
items | 用于访问和管理浏览历史记录中的每个历史记录项 |
back 、forward | 用于在浏览历史记录中前进和后退 |
2)示例代码
#include <QApplication>
#include <QWebEngineView>
#include <QWebEngineHistory>
#include <QDebug>
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
QWebEngineView view;
view.load(QUrl("https://www.test.com"));
view.show();
view.page()->loadFinished([&](bool success)
{
if (!success)
{
qDebug() << "Load failed!";
return;
}
QWebEngineHistory *history = view.page()->history();
if (history->canGoBack())
{
history->back();
}
if (history->canGoForward())
{
history->forward();
}
history->clear();
});
return app.exec();
}
2 问题解决
本节就实际使用Qt
内嵌web
实现相关效果过程中遇到的问题,进行了一些分析,并给出对应解决或绕过办法。
2.1 鼠标事件
直接用eventFilter
捕获QWebEngineView
对象的鼠标事件失败,因为实际渲染的并非QWebEngineView
这个对象。
在包含QWebEngineView
的QWidget
的web
区域内,想捕获QEvent::Type::MouseButtonDblClick
事件。通过bool event(QEvent* event)
、bool eventFilter(QObject* watched, QEvent* event)
都无法捕获到。
QWebEngineView
初始化到show
之间的消息类型输出如下,
而QWidget
初始化到show
之间的消息类型如下,
发现QWebEngineView
存在一个QEvent::ChildAdded
事件,难道这个child
才能响应鼠标事件?
Qt
对事件QEvent::ChildAdded
的注释是new child widget
,说明QWebEngineView
确实新建了一个子窗体,我们可以通过断点看这个子窗体的类型、大小、位置,
这是一个多继承自QQuickWidget
、QWebEngineCore::RenderWidgetHostViewQtDelegate
的子类,其中QQuickWidget
是QWidget
的子类,可以看到这个子窗体的大小、位置,
可以看到,子窗体实际覆盖了QWebEngineView
所在区域。
由此推想得到解决办法,监听这个子窗体的鼠标双击事件。
具体做法,先重写QWebEngineView
的event
、eventFilter
方法,以捕获QWebEngineView
中的鼠标双击事件为例,
WebViewWithMouseEvent.h
#pragma once
#include <QWebEngineView>
class WebViewWithMouseEvent : public QWebEngineView
{
Q_OBJECT
public:
WebViewWithMouseEvent(QWidget *parent);
~WebViewWithMouseEvent() override = default;
private:
bool event(QEvent* pEvent) override;
bool eventFilter(QObject* watched, QEvent* event) override;
QObject* pChildObj_ = nullptr;
signals:
void signalMouseDblClickEvent();
};
WebViewWithMouseEvent.cpp
#include "WebViewWithMouseEvent.h"
#include <QEvent>
WebViewWithMouseEvent::WebViewWithMouseEvent(QWidget *parent)
: QWebEngineView(parent)
{}
bool WebViewWithMouseEvent::event(QEvent* pEvent)
{
if (pEvent->type() == QEvent::ChildPolished)
{
QChildEvent* pChildEvent = static_cast<QChildEvent*>(pEvent);
pChildObj_ = pChildEvent->child();
if (pChildObj_)
pChildObj_->installEventFilter(this);
}
return QWebEngineView::event(pEvent);
}
bool WebViewWithMouseEvent::eventFilter(QObject* watched, QEvent* event)
{
if (watched == pChildObj_ && event->type() == QEvent::Type::MouseButtonDblClick)
{
emit signalMouseDblClickEvent();
return true;
}
return QWebEngineView::eventFilter(watched, event);
}
另外,QWebEngineView
不响应鼠标事件,或许是Qt
存在的Bug
,
链接:[QTBUG-43602] Mouse events are not handled in WebEngineView - Qt Bug Tracker
2.2 设置背景
在加载页面前控件默认背景色是白色,如果加载的url是其他背景颜色,就会出现先闪现零点几秒的白色背景然后再加载url对应的背景色,ui体验不好。要改变背景色使用如下代码,
ui.webEngineView->page()->setBackgroundColor(QColor(44, 50, 61));
并且代码必须在show之前调用,否则仍然会出现同样的问题
2.3 隐藏滚动条
如果不想要页面滚动条,html页面可以如下设置,
<!DOCTYPE html>
<html style="height: 100%">
<head>
<meta charset="utf-8">
</head>
<body style="height: 100%;">
</body>
</html>
2.4 视频播放
由于QWebEngineView
没有视频解码器,无法播放mp4
等视频,需要重新编译QWebEngineView
库,具体编译流程参考链接即可,
编译QtWebEngine模块支持音视频播放_qtwebengine 声音-CSDN博客
或者直接下载别人编译好的版本。
经测试,Qt6.6.1
依然不支持,
3 透明失效
本节其实也是“问题解决”的一小节,但是由于篇幅较长,我单拎一节讲述,
3.1 抛出问题
某窗体对象A需要设置为透明,
setAttribute(Qt::WA_TranslucentBackground);
注意,对象A设置了上述属性后,A本身变成了透明,因此对A设置background-color
会没效果,可以在A上嵌套一个QWidget
对象B,对B设置background-color
,或者对A设置border-image
,icon
得是透明图片,png
、jpg
等均可。
创建QWidget
名为TestWindow
,添加控件、布局如下,
TestWindow
的.h
、.cpp
如下,
.h
#pragma once
#include <QWidget>
#include "ui_TestWindow.h"
class TestWindow : public QWidget
{
Q_OBJECT
public:
TestWindow(QWidget *parent = nullptr);
~TestWindow();
private:
Ui::TestWindow ui;
private slots:
void slotBtnClicked();
};
.cpp
#include "TestWindow.h"
TestWindow::TestWindow(QWidget *parent)
: QWidget(parent)
{
ui.setupUi(this);
setAttribute(Qt::WA_TranslucentBackground);
connect(ui.pushButton, SIGNAL(clicked()), this, SLOT(slotBtnClicked()));
}
TestWindow::~TestWindow()
{}
void TestWindow::slotBtnClicked()
{
ui.testWidget->winId();
}
main.cpp
#include <QtWidgets/QApplication>
#include "TestWindow.h"
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
TestWindow w;
w.show();
return a.exec();
}
上述代码中,ui.testWidget
对象类型是QOpenGLWidget
。
为了有背景色,我在TestWindow
和testWidget
之间放了widget
,并为widget
设置了背景色background-color: rgba(100, 100, 100, 150)
。
另外,为ui.pushButton
绑定了槽函数slotBtnClicked()
,所执行操作仅为执行了ui.testWidget
的winId()
,该槽函数执行前,效果如下,
该槽函数执行后,效果如下,
可以看到,TestWindow
原本设置的透明,在testWidget
执行了winId
后就失效了。
然后,把testWidget
的类型改为QWidget
,为了让人能看出testWidget
所在区域,给testWidget
设置背景色background-color: rgba(0, 0, 0, 150)
,
结果,TestWindow
透明没生效。
给TestWindow
加上无边框属性,
setWindowFlags(Qt::FramelessWindowHint | windowFlags());
透明恢复正常(另一个坑,后续讨论),
不过,当testWidget
改为QWidget
再执行winId()
,TestWindow
同样顺带被设置了Qt::WA_NativeWindow
,但透明效果依然正常。
那么,testWidget
改为QWidget
,就一点没问题了吗?实际使用这时候testWidget
返回的winId
去渲染视频,发现视频根本没渲染出来,又是个坑。
3.2 Qt6.6.1
为了排除本问题是旧Qt
已修复的Bug
,下载了最新稳定版本进行验证,存在同样的问题,
3.3 源码分析
先回到testWidget
是QOpenGLWidget
类型的情况,虽然透明效果异常,但对于视频类业务,起码这时候返回的winId
用于渲染视频是正常的。
winId
源码如下,
/*!
\fn WId QWidget::internalWinId() const
\internal
Returns the window system identifier of the widget, or 0 if the widget is not created yet.
*/
/*!
\fn WId QWidget::winId() const
Returns the window system identifier of the widget.
Portable in principle, but if you use it you are probably about to
do something non-portable. Be careful.
If a widget is non-native (alien) and winId() is invoked on it, that widget
will be provided a native handle.
This value may change at run-time. An event with type QEvent::WinIdChange
will be sent to the widget following a change in window system identifier.
\sa find()
*/
WId QWidget::winId() const
{
if (!data->in_destructor
&& (!testAttribute(Qt::WA_WState_Created) || !internalWinId()))
{
#ifdef ALIEN_DEBUG
qDebug() << "QWidget::winId: creating native window for" << this;
#endif
QWidget *that = const_cast<QWidget*>(this);
that->setAttribute(Qt::WA_NativeWindow);
that->d_func()->createWinId();
return that->data->winid;
}
return data->winid;
}
可以看到,winId
会执行setAttribute(Qt::WA_NativeWindow)
,setAttribute
源码中Qt
将attribute
保存的源码如下,
/*!
\internal
This just sets the corresponding attribute bit to 1 or 0
*/
static void setAttribute_internal(Qt::WidgetAttribute attribute, bool on, QWidgetData *data,
QWidgetPrivate *d)
{
if (attribute < int(8*sizeof(uint))) {
if (on)
data->widget_attributes |= (1<<attribute);
else
data->widget_attributes &= ~(1<<attribute);
} else {
const int x = attribute - 8*sizeof(uint);
const int int_off = x / (8*sizeof(uint));
if (on)
d->high_attributes[int_off] |= (1<<(x-(int_off*8*sizeof(uint))));
else
d->high_attributes[int_off] &= ~(1<<(x-(int_off*8*sizeof(uint))));
}
}
从上述代码中可以看到,attribute
以位图方式被保存起来,因此多个attribute
可以共同作用于一个QWidget
。
为了确认ui.testWidget
、TestWindow
的attribute
变化,我分别创建了临时变量记录如下,
可以看到,虽然是ui.testWidget
执行的winId
,但是TestWindow
也被设置了Qt::WA_NativeWindow
属性,不过其Qt::WA_TranslucentBackground
属性依然存在。查看Qt
助手的介绍,
Qt::WA_NativeWindow
100
Indicates that a native window is created for the widget. Enabling this flag will also force a native window for the widget’s ancestors unless Qt::WA_DontCreateNativeAncestors is set.
函数winId()
确实会对作用的QWidget
及其祖父QWidget
均影响。
其中,我用到的testAttribute
函数源码如下,
inline bool QWidget::testAttribute(Qt::WidgetAttribute attribute) const
{
if (attribute < int(8*sizeof(uint)))
return data->widget_attributes & (1<<attribute);
return testAttribute_helper(attribute);
}
另外,TestWindow
的透明失效,并非是在winId
执行完后立即发生,而是在Qt
事件循环完后发生,可以通过调用QCoreApplication::processEvents()
强制更新。
直接给TestWindow
设置Qt::WA_TranslucentBackground
和Qt::WA_NativeWindow
,经testAttribute
验证TestWindow
也确实拥有了Qt::WA_NativeWindow
属性,但是TestWindow
的透明依然正常。
那么,Qt::WA_NativeWindow
属性,是如何设置到TestWindow
上的呢?
看void QWidget::setAttribute(Qt::WidgetAttribute attribute, bool on)
的源码,由于该函数代码量较多,直接看case Qt::WA_NativeWindow
,
d->createTLExtra();
if (on)
d->createTLSysExtra();
#ifndef QT_NO_IM
QWidget *focusWidget = d->effectiveFocusWidget();
if (on && !internalWinId() && this == QGuiApplication::focusObject()
&& focusWidget->testAttribute(Qt::WA_InputMethodEnabled)) {
QGuiApplication::inputMethod()->commit();
QGuiApplication::inputMethod()->update(Qt::ImEnabled);
}
if (!QCoreApplication::testAttribute(Qt::AA_DontCreateNativeWidgetSiblings) && parentWidget())
parentWidget()->d_func()->enforceNativeChildren();
if (on && !internalWinId() && testAttribute(Qt::WA_WState_Created))
d->createWinId();
if (isEnabled() && focusWidget->isEnabled() && this == QGuiApplication::focusObject()
&& focusWidget->testAttribute(Qt::WA_InputMethodEnabled)) {
QGuiApplication::inputMethod()->update(Qt::ImEnabled);
}
#endif //QT_NO_IM
首先可以看到,有个enforceNativeChildren
,
inline void enforceNativeChildren()
{
if (!extra)
createExtra();
if (extra->nativeChildrenForced)
return;
extra->nativeChildrenForced = 1;
for (int i = 0; i < children.size(); ++i) {
if (QWidget *child = qobject_cast<QWidget *>(children.at(i)))
child->setAttribute(Qt::WA_NativeWindow);
}
}
从源码可以看出,enforceNativeChildren
将所有testWidget
同级及子孙QWidget
都设置成了Qt::WA_NativeWindow
。
关键在于,当为testWidget
同级及子孙QWidget
执行setAttribute(Qt::WA_NativeWindow)
时,会进入setAttribute
的如下逻辑分支,
if (on && !internalWinId() && testAttribute(Qt::WA_WState_Created))
d->createWinId();
进一步看createWinId
的源码,
void QWidgetPrivate::createWinId()
{
Q_Q(QWidget);
const bool forceNativeWindow = q->testAttribute(Qt::WA_NativeWindow);
if (!q->testAttribute(Qt::WA_WState_Created) || (forceNativeWindow && !q->internalWinId())) {
if (!q->isWindow()) {
QWidget *parent = q->parentWidget();
QWidgetPrivate *pd = parent->d_func();
if (forceNativeWindow && !q->testAttribute(Qt::WA_DontCreateNativeAncestors))
parent->setAttribute(Qt::WA_NativeWindow);
if (!parent->internalWinId()) {
pd->createWinId();
}
for (int i = 0; i < pd->children.size(); ++i) {
QWidget *w = qobject_cast<QWidget *>(pd->children.at(i));
if (w && !w->isWindow() && (!w->testAttribute(Qt::WA_WState_Created)
|| (!w->internalWinId() && w->testAttribute(Qt::WA_NativeWindow)))) {
w->create();
}
}
} else {
q->create();
}
}
}
会进入上述代码中的逻辑分支,
if (forceNativeWindow && !q->testAttribute(Qt::WA_DontCreateNativeAncestors))
parent->setAttribute(Qt::WA_NativeWindow);
然后就给TestWindow
也执行了setAttribute(Qt::WA_NativeWindow)
。
那么,是不是可以将上述逻辑分支中的testAttribute(Qt::WA_DontCreateNativeAncestors)
设置成true
,就能解决问题呢?
在ui.testWidget->winId()
前,我先加了
ui.testWidget->setAttribute(Qt::WA_DontCreateNativeAncestors);
经过testAttribute(Qt::WA_NativeWindow)
,TestWindow
仍然被设置了Qt::WA_NativeWindow
。
想到还有个widget
对象在TestWindow
和testWidget
之间,再加上
ui.widget->setAttribute(Qt::WA_DontCreateNativeAncestors);
断点查看,
TestWindow
不再Qt::WA_NativeWindow
,但是透明失效的影响仍在。
3.4 冲突介绍
本节所说的冲突,存在于以Qt
作为UI
框架、用winId
作为视频渲染句柄的业务中,如果不用winId
渲染视频,使用Qt
提供的QSurface
、QGraphicView
相关模块,那就不存在这个问题。
但是,目前大部分Windows
视频渲染业务,还是基于WinId
,特别是要求硬件渲染的场景。
这冲突,深入点看,是QWidget
属性Qt::WA_TranslucentBackground
与Qt::WA_NativeWindow
之间的冲突,是Qt
渲染和Windows
原生窗体之间的不兼容。
Qt
官方也有建议,
Is it possible to have transparent Qt widgets on top of a QOpenGLWidget?
Qt provides a way to paint to an OpenGL context, but this is in the end painted separately from Qt and the two painting systems are not synchronized, so Qt cannot compose (semi-)transparent images on top of a QOpenGLWidget. At best, you can have opaque widgets on top of the QOpenGLWidget, but even then you may experience some flicker due to the painting being asynchronous.
可以完全用Qt
渲染或转成QPixmap
再绘制,
OpenGL and translucent background do not work together due to a limitation
Due to a limitation in window managers, it is not possible to combine OpenGL with a translucent background window (set with WA_TranslucentBackground). The reason for this is because OpenGL is rendered directly onto the screen and the compositor used to enable the translucent background cannot ensure that the OpenGL is rendered correctly.
There is nothing that can be done in Qt to fix this as it needs to be done on the side of the window manager.
To work around this issue you can either just use Qt to render everything and not OpenGL, or you can render the OpenGL into a pixmap and draw that onto your widget instead.
QMediaPlayer
、QVideoWidget
来实现透明窗体上的视频渲染,在这不再多赘述。
3.5 填充法
关于上述TestWindow
透明失效的问题,若我执意要用winId
来处理视频渲染业务,有没有绕过办法呢?这里给出一种自创的“填充法”。
从透明失效的现象可以看到,失效的是testWidget
(类型是QOpenGLWidget
)的parentWidget
,那么如果我在TestWindow
上填充非testWidget
的parentWidget
的QWidget
,使得失效区域都都被这些填充的QWidget
覆盖,是不是就能“恢复”透明?
还有,上述填充QWidget
还需要保证不影响正常布局设计。
填充法.ui
布局如下,
为了让你看到leftWidget
、topWidget
、rightWidget
和bottomWidget
具体所在位置,我给这几个QWidget
设置border
标记出来,
3.6 冲突表现
上述Qt::WA_NativeWindow
导致的透明失效,其实不只影响QWidget
或QOpenGLWidget
,也影响本文主要介绍的QOpenGLWidget
,本节将进一步聊聊这种冲突的具体表现。
除了抛出问题的表现外,还是以TestWindow
为例,当给TestWindow
增加Qt::Window
使它成为独立窗体,
TestWindow::TestWindow(QWidget *parent)
: QWidget(parent)
{
ui.setupUi(this);
setAttribute(Qt::WA_TranslucentBackground);
setWindowFlags(Qt::FramelessWindowHint | Qt::Window | windowFlags());
connect(ui.pushButton, SIGNAL(clicked()), this, SLOT(slotBtnClicked()));
}
此时TestWindow
若存在parentWidget
,也存在透明失效影响。目前测试下来,TestWindow
对parentWidget
并非直接导致透明失效,比如parentWidget
只是简单的QWidget
,透明就正常。
基于上述经过填充法改造的TestWindow
,新增TestParentWidget
类,
TestParentWidget.ui
其中,widget
对象设置背景色background-color: rgba(0, 0, 150, 100)
方便看效果。
TestParentWidget.h
#pragma once
#include <QWidget>
#include "ui_TestParentWidget.h"
class TestParentWidget : public QWidget
{
Q_OBJECT
public:
TestParentWidget(QWidget *parent = nullptr);
~TestParentWidget();
private:
Ui::TestParentWidgetClass ui;
};
TestParentWidget.cpp
#include "TestParentWidget.h"
#include "TestWindow.h"
TestParentWidget::TestParentWidget(QWidget *parent)
: QWidget(parent)
{
ui.setupUi(this);
setAttribute(Qt::WA_TranslucentBackground);
setWindowFlags(Qt::FramelessWindowHint | windowFlags());
auto* pWidget = new TestWindow(this);
pWidget->show();
}
TestParentWidget::~TestParentWidget()
{}
/* void TestParentWidget::slotBtnClicked()
{
新的坑:如果主窗体是QWidget,且内含QWebEngineWidget或QOpenGLWidget对象,在QApplication::exec()之后弹出TestWindow,然后执行winId(),会触发Qt渲染模块的崩溃。但是,如果主窗体不是TestParentWidget,另有不含QWebEngineWidget或QOpenGLWidget对象的QWidget,则能正常执行winId()。
auto* pWidget = new TestWindow(this);
pWidget->show();
} */
main.cpp
修改如下,
#include <QtWidgets/QApplication>
#include "TestParentWidget.h"
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
TestParentWidget w;
w.show();
return a.exec();
}
TestParentWidget
透明效果如下,
给TestParentWidget
添加一个QWebEngineView
或QOpenGLWidget
对象后,情况就不同了,
执行完ui.testWidget->winId()
,TestParentWidget
的透明就失效了,
3.7 背景法
上一小节已经基本给出了问题的出现条件,
既然parentWidget
是因为子控件执行了winId
,也被强行设置了Qt::WA_NativeWindow
,那么如果把该属性设置false
(setAttribute(Qt::WA_NativeWindow, false)
),能否恢复parentWidget
的透明效果呢?
经过测试,即使对parentWidget
执行了setAttribute(Qt::WA_NativeWindow, false)
,通过testAttribute
也验证生效,但透明效果依然无法恢复。
经过对比测试,像TestWindow
影响带QWebEngineView
或QOpenGLWidget
的TestParentWidget
,只影响一层parentWidget
,
TestParentWidget::TestParentWidget(QWidget *parent)
: QWidget(parent)
{
ui.setupUi(this);
setAttribute(Qt::WA_TranslucentBackground);
setWindowFlags(Qt::FramelessWindowHint | windowFlags());
auto* pMiddleWidegt = new QWidget(this);
pMiddleWidegt->setWindowFlags(Qt::Window | windowFlags());
pMiddleWidegt->show();
auto* pWidget = new TestWindow(pMiddleWidegt);
pWidget->show();
}
如上述代码所示,在TestWindow
和TestParentWidget
之间夹一个QWidget
,透明失效就不会影响到TestParentWidget
。
这也算是种绕过办法,我称之为“背景法”,不过由于加的pMiddleWidget
非透明,实际上可以给它设置成透明无边框,并且甚至不需要是visible
,
TestParentWidget::TestParentWidget(QWidget *parent)
: QWidget(parent)
{
ui.setupUi(this);
setAttribute(Qt::WA_TranslucentBackground);
setWindowFlags(Qt::FramelessWindowHint | windowFlags());
pMiddleWidget = new QWidget(this);
pMiddleWidget->setAttribute(Qt::WA_TranslucentBackground);
pMiddleWidget->setWindowFlags(Qt::Window | windowFlags());
pMiddleWidget->hide();
auto* pWidget = new TestWindow(pMiddleWidget);
connect(pWidget, SIGNAL(signalTest()), this, SLOT(slotTest()));
pWidget->show();
}
给TestWindow
加个signal
用于testAttribute
一下TestParentWidget
执行完winId()
前后的attribute
,执行前,
执行后,
可以看到,TestParentWidget
以及夹在中间的pMiddleWidget
都没有被带上Qt::WA_NativeWindow
,不过结合前边源码,Qt::WA_NativeWindow
的属性本身就只影响到了TestWindow
这个底层QWidget
而已。
3.8 以毒攻毒
除了加个pMiddleWidget
,还有种情况TestParentWidget
的透明也能恢复正常,那就是以毒攻毒,TestParentWidget
也加上QOpenGLWidget
执行winId()
并用填充法解决自身的透明问题。
经测试,“以毒攻毒”效果良好~
不过,这种绕过办法使用场景相对更局限些,适用于TestParentWidget
自身也需要视频渲染的场景。
总结下,
- 如果
TestParentWidget
布局简单,可以基于填充法调整布局解决,只不过必须得在底层QWidget
上填充,而前边填充法并无底层QWidget
要求。 - 如果
TestParentWidget
主体是QStackedWidget
,而且其中某个page
包含了QWebEngineView
或QOpenGLWidget
对象。这种情况下,QStackedWidget
本身及全部page
透明都会失效,那么只能用“背景法“了。 - 如果刚好,
TestParentWidget
也有视频渲染需求,那么可以使用QOpenGLWidget
预先执行setAttribute(Qt::WA_NativeWindow)
结合填充法绕过。
3.9 属性被冲掉
修改parentWidget
窗导致QWidget
的窗体属性被冲掉,如下代码,
auto* pWidget = new TestWindow(this);
pMiddleWidget = new QWidget(this);
pMiddleWidget->setAttribute(Qt::WA_TranslucentBackground);
pMiddleWidget->setWindowFlags(Qt::Window | windowFlags());
pMiddleWidget->hide();
pWidget->setParent(pMiddleWidget);
connect(pWidget, SIGNAL(signalTest()), this, SLOT(slotTest()));
pWidget->show();
原本TestWindow
的parentWidget
是TestParentWidget
,由于解决透明问题的需要,中途修改了parentWidget
,结果你会发现TestWindow
构造函数中执行的setWindowFlags(Qt::FramelessWindowHint | Qt::Window | windowFlags())
失去效果,恢复成非独立窗体。
遇到上述问题,在修改parentWidget
,重新执行一遍setWindowFlags(Qt::FramelessWindowHint | Qt::Window | windowFlags())
即可解决。
3.10 effectiveWinId
既然winId()
会强制给QWidget
设置Qt::WA_NativeWindow
,那么是否有直接拿到winId
的办法呢?
有,QWidget
还有个接口名叫effectiveWinId
,该接口不会改变QWidget
的attribute
,源码如下,
/*!
\since 4.4
Returns the effective window system identifier of the widget, i.e. the
native parent's window system identifier.
If the widget is native, this function returns the native widget ID.
Otherwise, the window ID of the first native parent widget, i.e., the
top-level widget that contains this widget, is returned.
\note We recommend that you do not store this value as it is likely to
change at run-time.
\sa nativeParentWidget()
*/
WId QWidget::effectiveWinId() const
{
const WId id = internalWinId();
if (id || !testAttribute(Qt::WA_WState_Created))
return id;
if (const QWidget *realParent = nativeParentWidget())
return realParent->internalWinId();
return 0;
}
不过,effectiveWinId()
会尝试查找nativeParentWidget()
然后返回有效winId
,nativeParentWidget()
源码如下,
/*!
\since 4.4
Returns the native parent for this widget, i.e. the next ancestor widget
that has a system identifier, or \nullptr if it does not have any native
parent.
\sa effectiveWinId()
*/
QWidget *QWidget::nativeParentWidget() const
{
QWidget *parent = parentWidget();
while (parent && !parent->internalWinId())
parent = parent->parentWidget();
return parent;
}
注意,上述源码并未要求这个parentWidget
是Qt::WA_NativeWindow
,而只判断是否拥有有效的winId
。
回顾最开始抛出问题的TestWindow
和testWidget
例子,看看testWidget
的effectiveWinId
最终会从哪里获取到,
可以看到,虽然TestWindow
最初并未设置成Qt::WA_NativeWindow
,但拥有有效的winId
。
这给我们一个启发,其实所有独立窗体都应该拥有有效的winId
,虽然我们直接将渲染视频逻辑绑定到内部某个QWidget
或QOpenGLWidget
对象,存在透明失效问题,那是否可以直接绑定到有有效winId
的窗体上?如果这个窗体是底层QWidget
,其区域并非我们预期,我们可以给我们的渲染业务指定winId
对应窗体实际渲染的区域(起始位置x
、y
,区域w
、h
)。
不过,当遇到QStackedWidegt
或其它绘制区域,需要随所在QWidget
隐藏的情况,也需要扩展我们的渲染业务,使得能够及时响应不显示。
4 未完待续
还有一些点,限于篇幅、时间,先记录如下,后续博文更新。
4.1 对象复用
由于每个QWebEngineView
对象都会启动一个QtWebEngineProcessed.exe
,而且还需要重新加载web
,不管是资源占用,还是加载耗时都感觉“浪费”。
能否多页面复用QWebEngineView
对象?比如,直接从一个QWidget
拖走QWebEngineView
对象,嵌入另一个QWidget
,结果已加载的网页无法正常显示。
举个例子,当在stackWidget上某个页面上工作时,假设QWebEngineView在第1页,而当前stackWidget在其他页面,这时候如果使用第1页的QWebEngineView去加载本地html和js文件,会发现js脚本部分完全无法运行,但是其他静态的内容都能解析成功。
目前我的解决方法是只有在当前页面是QWebEngineView时才加载html和js文件,这样即使之后切换到别的页面,也不会导致js无法执行。但这样一来用户体验会变差,并且总是要规避第一次加载页面时不在QWebEngineView页面的问题,非常麻烦。不过暂时也没别的更好的解决方法。
4.2 像素级缩放
倘若web
未适配缩放,如何直接操作QWebEngineView
控件,对所显示的web
进行缩放?
比如通过QGraphicView
画布。
通常如果web
本身支持缩放,可以重写QWebEngineView
的sizeHint
来操作,
QSize sizeHint() const override;
4.3 自动登录
Qt
容易实现跳转浏览器web
,也容易实现web
嵌入,但是对于已经通过HTTP
协议完成账号密码登录,并获取到token
的情况,希望不需要再在浏览器或内嵌web
再次输入账号、密码即可登录,实现流程上的统一、简化。
浏览器上登录,通常会把token
、session
等相关内容存储到缓存中,Qt
也支持setCookie
等,但是比如chrome浏览器上的session storage
在QtWebEngine
模块中就没找到方法可以设置。
除了直接设置浏览器缓存,还有个思路就是直接调用web
上的js
方法后台执行登录操作。
当然,如果服务端本身支持url
中加参数的方式获取对应页面,也可以实现自动登录web
。
4.4 WebAssembly
基于Qt6.6.1
,聊下WebAssebly
。
4.5 主窗体透明不生效
前面已经说过,主窗体(QApplication
最底层QWidget
)若只设置了Qt::WA_TranslucentBackground
,透明并未生效,但若拥有QOpenGLWidegt
对象作为childWidget
或设置Qt::FramelessWindowHint
,就能恢复主窗体的透明。
另外,即使这时候给testWidget
设置不透明的背景色也不能生效。