Qt中QPA插件,Qt跨平台的基础
yuyutoo 2024-11-26 10:38 1 浏览 0 评论
前言
Qt是一个跨平台的C++框架,各位小伙伴在使用Qt的时候有没有想过Qt是怎么实现跨平台的呢?Qt为了实现跨平台做了哪些工作。现在我们就来探讨一下Qt跨平台的基础,QPA插件。文章有点长,请各位小伙伴耐心看完。
QPA简介
QPA的全称是Qt Platform Abstraction,即Qt平台抽象层。在Qt5中替代QWS(Qt Window System)。QPA插件是通过对各种QPlatform* 类进行子类化来实现的。有几个根类,例如用于窗口系统集成的QPlatformIntegration和QPlatformWindow,以及用于更深入的平台主题和集成的QPlatformTheme。QStyle不是QPA的一部分。QPA类没有源代码或二进制兼容性保证,这意味着平台插件只能保证与它开发的Qt版本一起使用。
为什么要使用QPA
Qt一直宣称是跨平台的,但是Qt的底层却不是跨平台,在Qt底层需要调用各个系统的api完成。以QWidget为例,在Qt5中,除了qwidget.cpp还有qwidget_win.cpp、qwidget_x11.cpp、qwidget_wince.cpp等一系列各平台相关的文件。在kernel.pri中使用win32 unix:x11等包含不同的cpp文件,在编译Qt的时候,不同的平台的代码就参与编译。下面是Qt4中源码kernel.pri的一段。
win32 {
DEFINES += QT_NO_DIRECTDRAW
HEADERS += \
kernel/qwinnativepangesturerecognizer_win_p.h
SOURCES += \
kernel/qapplication_win.cpp \
kernel/qclipboard_win.cpp \
kernel/qcursor_win.cpp \
kernel/qdesktopwidget_win.cpp \
kernel/qdnd_win.cpp \
kernel/qmime_win.cpp \
kernel/qsound_win.cpp \
kernel/qwidget_win.cpp \
kernel/qole_win.cpp \
kernel/qkeymapper_win.cpp \
kernel/qwinnativepangesturerecognizer_win.cpp
!contains(DEFINES, QT_NO_DIRECTDRAW):LIBS += ddraw.lib
}
symbian {
exists(${EPOCROOT}epoc32/include/platform/mw/akntranseffect.h): DEFINES += QT_SYMBIAN_HAVE_AKNTRANSEFFECT_H
SOURCES += \
kernel/qapplication_s60.cpp \
kernel/qeventdispatcher_s60.cpp \
kernel/qwidget_s60.cpp \
kernel/qcursor_s60.cpp \
kernel/qdesktopwidget_s60.cpp \
kernel/qkeymapper_s60.cpp\
kernel/qclipboard_s60.cpp\
kernel/qdnd_s60.cpp \
kernel/qsound_s60.cpp
HEADERS += \
kernel/qt_s60_p.h \
kernel/qeventdispatcher_s60_p.h
LIBS += -lbafl -lestor
INCLUDEPATH += $MW_LAYER_SYSTEMINCLUDE
INCLUDEPATH += ../3rdparty/s60
contains(QT_CONFIG, s60) {
SOURCES += kernel/qsoftkeymanager_s60.cpp
HEADERS += kernel/qsoftkeymanager_s60_p.h
}
}
unix:x11 {
INCLUDEPATH += ../3rdparty/xorg
HEADERS += \
kernel/qx11embed_x11.h \
kernel/qx11info_x11.h \
kernel/qkde_p.h
SOURCES += \
kernel/qapplication_x11.cpp \
kernel/qclipboard_x11.cpp \
kernel/qcursor_x11.cpp \
kernel/qdnd_x11.cpp \
kernel/qdesktopwidget_x11.cpp \
kernel/qmotifdnd_x11.cpp \
kernel/qsound_x11.cpp \
kernel/qwidget_x11.cpp \
kernel/qwidgetcreate_x11.cpp \
kernel/qx11embed_x11.cpp \
kernel/qx11info_x11.cpp \
kernel/qkeymapper_x11.cpp \
kernel/qkde.cpp
contains(QT_CONFIG, glib) {
SOURCES += \
kernel/qguieventdispatcher_glib.cpp
HEADERS += \
kernel/qguieventdispatcher_glib_p.h
QMAKE_CXXFLAGS += $QT_CFLAGS_GLIB
LIBS_PRIVATE +=$QT_LIBS_GLIB
}
SOURCES += \
kernel/qeventdispatcher_x11.cpp
HEADERS += \
kernel/qeventdispatcher_x11_p.h
}
虽然这样也可以实现跨平台,但是这一切都使得将Qt移植到一个新的窗口系统变的不太容易。例如我们要移植Qt到新的系统MyOS上,就需要建一系列的xxxx_myos.cpp文件。为了解决上面的问题,Qt5引入了QPA来替代,这样我们在移植Qt到MyOS上的时候就只需要新建一个MyOS的QPA插件。QPA插件在$QTDIR/plugins/platforms下。在windows上我们可以看到qwindows.dll,在linux下有libxcb.so,他们就是QPA插件。
QPA插件的使用
我们不会直接使用QPA插件,QPA插件时Qt自己调用的,建一个简单的列子来说明
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
Widget w;
w.show();
return a.exec();
}
我们都知道QApplication是继承QGuiApplication,注意QApplication a(argc, argv),该方法直接调用QApplicationPrivate::init(),(不知道QApplicationPrivate的伙伴们可以查阅Qt-D指针和Q指针及使用)QApplicationPrivate::init()中调用QGuiApplicationPrivate::init();在QGuiApplicationPrivate::init里面会创建QPlatformIntegration实例。QPlatformIntegration就是Qt上层与各个平台底层的桥梁,通过他或者他提供的一些接口返回的实例对象来实现平台功能。查看Qt的源码可以知道Qt先获取的QPA插件的路径platformPluginPath,该值一般为空,QPA插件的名称platformName,该值是在编译Qt的时候确定的,QT_QPA_DEFAULT_PLATFORM_NAME在qtgui-config.h中定义,比如在windows上在qtgui-config.h会有#define QT_QPA_DEFAULT_PLATFORM_NAME "windows"
void QGuiApplicationPrivate::createPlatformIntegration()
{
QHighDpiScaling::initHighDpiScaling();
// Load the platform integration
QString platformPluginPath = QString::fromLocal8Bit(qgetenv("QT_QPA_PLATFORM_PLUGIN_PATH"));
QByteArray platformName;
#ifdef QT_QPA_DEFAULT_PLATFORM_NAME
platformName = QT_QPA_DEFAULT_PLATFORM_NAME;
#endif
#if defined(Q_OS_UNIX) && !defined(Q_OS_DARWIN)
QByteArray sessionType = qgetenv("XDG_SESSION_TYPE");
if (!sessionType.isEmpty()) {
if (sessionType == QByteArrayLiteral("x11") && !platformName.contains(QByteArrayLiteral("xcb"))) {
platformName = QByteArrayLiteral("xcb");
} else if (sessionType == QByteArrayLiteral("wayland") && !platformName.contains(QByteArrayLiteral("wayland"))) {
QByteArray currentDesktop = qgetenv("XDG_CURRENT_DESKTOP").toLower();
QByteArray sessionDesktop = qgetenv("XDG_SESSION_DESKTOP").toLower();
if (currentDesktop.contains("gnome") || sessionDesktop.contains("gnome")) {
qInfo() << "Warning: Ignoring XDG_SESSION_TYPE=wayland on Gnome."
<< "Use QT_QPA_PLATFORM=wayland to run on Wayland anyway.";
} else {
platformName = QByteArrayLiteral("wayland");
}
}
}
#ifdef QT_QPA_DEFAULT_PLATFORM_NAME
// Add it as fallback in case XDG_SESSION_TYPE is something wrong
if (!platformName.contains(QT_QPA_DEFAULT_PLATFORM_NAME))
platformName += QByteArrayLiteral(";" QT_QPA_DEFAULT_PLATFORM_NAME);
#endif
#endif
QByteArray platformNameEnv = qgetenv("QT_QPA_PLATFORM");
if (!platformNameEnv.isEmpty()) {
platformName = platformNameEnv;
}
QString platformThemeName = QString::fromLocal8Bit(qgetenv("QT_QPA_PLATFORMTHEME"));
// Get command line params
QString icon;
int j = argc ? 1 : 0;
for (int i=1; i<argc; i++) {
if (!argv[i])
continue;
if (*argv[i] != '-') {
argv[j++] = argv[i];
continue;
}
const bool xcbIsDefault = platformName.startsWith("xcb");
const char *arg = argv[i];
if (arg[1] == '-') // startsWith("--")
++arg;
if (strcmp(arg, "-platformpluginpath") == 0) {
if (++i < argc)
platformPluginPath = QString::fromLocal8Bit(argv[i]);
} else if (strcmp(arg, "-platform") == 0) {
if (++i < argc)
platformName = argv[i];
} else if (strcmp(arg, "-platformtheme") == 0) {
if (++i < argc)
platformThemeName = QString::fromLocal8Bit(argv[i]);
} else if (strcmp(arg, "-qwindowgeometry") == 0 || (xcbIsDefault && strcmp(arg, "-geometry") == 0)) {
if (++i < argc)
windowGeometrySpecification = QWindowGeometrySpecification::fromArgument(argv[i]);
} else if (strcmp(arg, "-qwindowtitle") == 0 || (xcbIsDefault && strcmp(arg, "-title") == 0)) {
if (++i < argc)
firstWindowTitle = QString::fromLocal8Bit(argv[i]);
} else if (strcmp(arg, "-qwindowicon") == 0 || (xcbIsDefault && strcmp(arg, "-icon") == 0)) {
if (++i < argc) {
icon = QString::fromLocal8Bit(argv[i]);
}
} else {
argv[j++] = argv[i];
}
}
if (j < argc) {
argv[j] = 0;
argc = j;
}
init_platform(QLatin1String(platformName), platformPluginPath, platformThemeName, argc, argv);
真正创建是在init_platform中。下面试部分代码
static void init_platform(const QString &pluginNamesWithArguments, const QString &platformPluginPath, const QString &platformThemeName, int &argc, char **argv)
{
QStringList plugins = pluginNamesWithArguments.split(QLatin1Char(';'));
QStringList platformArguments;
QStringList availablePlugins = QPlatformIntegrationFactory::keys(platformPluginPath);
for (auto pluginArgument : plugins) {
// Split into platform name and arguments
QStringList arguments = pluginArgument.split(QLatin1Char(':'));
const QString name = arguments.takeFirst().toLower();
QString argumentsKey = name;
argumentsKey[0] = argumentsKey.at(0).toUpper();
arguments.append(QLibraryInfo::platformPluginArguments(argumentsKey));
// Create the platform integration.
QGuiApplicationPrivate::platform_integration = QPlatformIntegrationFactory::create(name, arguments, argc, argv, platformPluginPath);
if (Q_UNLIKELY(!QGuiApplicationPrivate::platform_integration)) {
if (availablePlugins.contains(name)) {
qCInfo(lcQpaPluginLoading).nospace().noquote()
<< "Could not load the Qt platform plugin \"" << name << "\" in \""
<< QDir::toNativeSeparators(platformPluginPath) << "\" even though it was found.";
} else {
qCWarning(lcQpaPluginLoading).nospace().noquote()
<< "Could not find the Qt platform plugin \"" << name << "\" in \""
<< QDir::toNativeSeparators(platformPluginPath) << "\"";
}
} else {
QGuiApplicationPrivate::platform_name = new QString(name);
platformArguments = arguments;
break;
}
}
我们发布Qt程序的之后运行程序,经常会看到这样的警告Could not find the Qt platform plugin "windows" in xxxxx。就是没有QPA插件拷贝到程序目录下的platforms下。
之前我们说到platformPluginPath一般为空,因为Qt的自动在程序所在的目录下的/platforms去查找QPA插件。比如windows下,程序目录下一般会有platforms/qwindows.dll。找到QPA插件之后使用QPlatformIntegrationFactory::create来创建QPlatformIntegration实例,各个平台下都会有一个这样的实例。比如Windows下QWindowsIntegration,Android下的QAndroidPlatformIntegration和Linux下的QXcbIntegration,他们都是继承QPlatformIntegration。
窗口的创建
以Windows为例,介绍QWidget怎么与系统的窗口关联的和创建流程。QWidget::setVisible->QWidgetPrivate::setVisible->QWidget::create->QWidgetPrivate::create->QWindow::create->QWindowPrivate::create->QWindowsIntegration::createPlatformWindow当我们在调用QWidget::setVisible()的时候,如果该窗口是一个顶层窗口或设置了本地窗口就会创建底层系统窗口。我们主要看一下最后两步
void QWindowPrivate::create(bool recursive, WId nativeHandle)
{
Q_Q(QWindow);
if (platformWindow)
return;
if (q->parent())
q->parent()->create();
QPlatformIntegration *platformIntegration = QGuiApplicationPrivate::platformIntegration();
platformWindow = nativeHandle ? platformIntegration->createForeignWindow(q, nativeHandle)
: platformIntegration->createPlatformWindow(q);
Q_ASSERT(platformWindow);
if (!platformWindow) {
qWarning() << "Failed to create platform window for" << q << "with flags" << q->flags();
return;
}
在这里我们可以看到使用了QPA来创建createPlatformWindow。如果是Window上就会调用QWindowsIntegration来创建,如果是Linux下使用的xcb就会调用QXcbIntegration来创建。
QPlatformWindow *QWindowsIntegration::createPlatformWindow(QWindow *window) const
{
if (window->type() == Qt::Desktop) {
QWindowsDesktopWindow *result = new QWindowsDesktopWindow(window);
qCDebug(lcQpaWindows) << "Desktop window:" << window
<< showbase << hex << result->winId() << noshowbase << dec << result->geometry();
return result;
}
QWindowsWindowData requested;
requested.flags = window->flags();
requested.geometry = QHighDpi::toNativePixels(window->geometry(), window);
// Apply custom margins (see QWindowsWindow::setCustomMargins())).
const QVariant customMarginsV = window->property("_q_windowsCustomMargins");
if (customMarginsV.isValid())
requested.customMargins = qvariant_cast<QMargins>(customMarginsV);
QWindowsWindowData obtained =
QWindowsWindowData::create(window, requested,
QWindowsWindow::formatWindowTitle(window->title()));
qCDebug(lcQpaWindows).nospace()
<< __FUNCTION__ << ' ' << window
<< "\n Requested: " << requested.geometry << " frame incl.="
<< QWindowsGeometryHint::positionIncludesFrame(window)
<< ' ' << requested.flags
<< "\n Obtained : " << obtained.geometry << " margins=" << obtained.frame
<< " handle=" << obtained.hwnd << ' ' << obtained.flags << '\n';
if (Q_UNLIKELY(!obtained.hwnd))
return Q_NULLPTR;
QWindowsWindow *result = createPlatformWindowHelper(window, obtained);
QWindowsWindowData::create里面会调用系统的API创建真正的窗口
QWindowsWindowData
QWindowsWindowData::create(const QWindow *w,
const QWindowsWindowData ?meters,
const QString &title)
{
WindowCreationData creationData;
creationData.fromWindow(w, parameters.flags);
QWindowsWindowData result = creationData.create(w, parameters, title);
// Force WM_NCCALCSIZE (with wParam=1) via SWP_FRAMECHANGED for custom margin.
creationData.initialize(w, result.hwnd, !parameters.customMargins.isNull(), 1);
return result;
}
QWindowsWindowData
WindowCreationData::create(const QWindow *w, const WindowData &data, QString title) const
{
typedef QSharedPointer<QWindowCreationContext> QWindowCreationContextPtr;
WindowData result;
result.flags = flags;
const HINSTANCE appinst = (HINSTANCE)GetModuleHandle(0);
const QString windowClassName = QWindowsContext::instance()->registerWindowClass(w);
const QRect rect = QPlatformWindow::initialGeometry(w, data.geometry, defaultWindowWidth, defaultWindowHeight);
if (title.isEmpty() && (result.flags & Qt::WindowTitleHint))
title = topLevel ? qAppName() : w->objectName();
const wchar_t *titleUtf16 = reinterpret_cast<const wchar_t *>(title.utf16());
const wchar_t *classNameUtf16 = reinterpret_cast<const wchar_t *>(windowClassName.utf16());
// Capture events before CreateWindowEx() returns. The context is cleared in
// the QWindowsWindow constructor.
const QWindowCreationContextPtr context(new QWindowCreationContext(w, rect, data.customMargins, style, exStyle));
QWindowsContext::instance()->setWindowCreationContext(context);
qCDebug(lcQpaWindows).nospace()
<< "CreateWindowEx: " << w << " class=" << windowClassName << " title=" << title
<< '\n' << *this << "\nrequested: " << rect << ": "
<< context->frameWidth << 'x' << context->frameHeight
<< '+' << context->frameX << '+' << context->frameY
<< " custom margins: " << context->customMargins;
result.hwnd = CreateWindowEx(exStyle, classNameUtf16, titleUtf16,
style,
context->frameX, context->frameY,
context->frameWidth, context->frameHeight,
parentHandle, NULL, appinst, NULL);
我们可以看到调用了Windows的API CreateWindowEx来创建窗口。回到QPlatformWindow *QWindowsIntegration::createPlatformWindow(QWindow *window) const中,我们可以看到他最后创建了一个QWindowsWindow,QWindowsWindow是继承于QWindowsBaseWindow,而QWindowsBaseWindow是继承于QPlatformWindow。这样就完成了系统窗口的创建。QWindowsWindow就是通过Windows的API来控制窗口。比如
void QWindowsWindow::setWindowIcon(const QIcon &icon)
{
if (m_data.hwnd) {
destroyIcon();
m_iconSmall = createHIcon(icon, GetSystemMetrics(SM_CXSMICON), GetSystemMetrics(SM_CYSMICON));
m_iconBig = createHIcon(icon, GetSystemMetrics(SM_CXICON), GetSystemMetrics(SM_CYICON));
if (m_iconBig) {
SendMessage(m_data.hwnd, WM_SETICON, 0 /* ICON_SMALL */, LPARAM(m_iconSmall));
SendMessage(m_data.hwnd, WM_SETICON, 1 /* ICON_BIG */, LPARAM(m_iconBig));
} else {
SendMessage(m_data.hwnd, WM_SETICON, 0 /* ICON_SMALL */, LPARAM(m_iconSmall));
SendMessage(m_data.hwnd, WM_SETICON, 1 /* ICON_BIG */, LPARAM(m_iconSmall));
}
}
}
QPainter和QPA插件
现在我们知道本地窗口是怎么创建的和QWidget与本地窗体的关系。那就还有一个主要的问题就是,我在QWidget上绘制的东西怎么显示在本地窗体上的?原理其实很简单QPA插件提供了一个QPaintDevice,一般是QImage,也就是说Qt先绘制到QImage上,然后再通过系统API或者OpenGL绘制到窗体上。还是以Windows为例,我们可以看到最后使用了Windows API BitBlt函数来显示。
QPaintDevice *QWindowsBackingStore::paintDevice()
{
Q_ASSERT(!m_image.isNull());
return &m_image->image();
}
void QWindowsBackingStore::flush(QWindow *window, const QRegion ?ion,
const QPoint &offset)
{
Q_ASSERT(window);
const QRect br = region.boundingRect();
if (QWindowsContext::verbose > 1)
qCDebug(lcQpaBackingStore) << __FUNCTION__ << this << window << offset << br;
QWindowsWindow *rw = QWindowsWindow::windowsWindowOf(window);
Q_ASSERT(rw);
const bool hasAlpha = rw->format().hasAlpha();
const Qt::WindowFlags flags = window->flags();
if ((flags & Qt::FramelessWindowHint) && QWindowsWindow::setWindowLayered(rw->handle(), flags, hasAlpha, rw->opacity()) && hasAlpha) {
// Windows with alpha: Use blend function to update.
QRect r = QHighDpi::toNativePixels(window->frameGeometry(), window);
QPoint frameOffset(QHighDpi::toNativePixels(QPoint(window->frameMargins().left(), window->frameMargins().top()),
static_cast<const QWindow *>(Q_NULLPTR)));
QRect dirtyRect = br.translated(offset + frameOffset);
SIZE size = {r.width(), r.height()};
POINT ptDst = {r.x(), r.y()};
POINT ptSrc = {0, 0};
BLENDFUNCTION blend = {AC_SRC_OVER, 0, BYTE(qRound(255.0 * rw->opacity())), AC_SRC_ALPHA};
RECT dirty = {dirtyRect.x(), dirtyRect.y(),
dirtyRect.x() + dirtyRect.width(), dirtyRect.y() + dirtyRect.height()};
UPDATELAYEREDWINDOWINFO info = {sizeof(info), NULL, &ptDst, &size, m_image->hdc(), &ptSrc, 0, &blend, ULW_ALPHA, &dirty};
const BOOL result = UpdateLayeredWindowIndirect(rw->handle(), &info);
if (!result)
qErrnoWarning("UpdateLayeredWindowIndirect failed for ptDst=(%d, %d),"
" size=(%dx%d), dirty=(%dx%d %d, %d)", r.x(), r.y(),
r.width(), r.height(), dirtyRect.width(), dirtyRect.height(),
dirtyRect.x(), dirtyRect.y());
} else {
const HDC dc = rw->getDC();
if (!dc) {
qErrnoWarning("%s: GetDC failed", __FUNCTION__);
return;
}
if (!BitBlt(dc, br.x(), br.y(), br.width(), br.height(),
m_image->hdc(), br.x() + offset.x(), br.y() + offset.y(), SRCCOPY)) {
const DWORD lastError = GetLastError(); // QTBUG-35926, QTBUG-29716: may fail after lock screen.
if (lastError != ERROR_SUCCESS && lastError != ERROR_INVALID_HANDLE)
qErrnoWarning(int(lastError), "%s: BitBlt failed", __FUNCTION__);
}
rw->releaseDC();
}
而backStore的创建在QWidgetPrivate::create_sys()中,如代码如下
QBackingStore *store = q->backingStore();
if (!store) {
if (win && q->windowType() != Qt::Desktop) {
if (q->isTopLevel())
q->setBackingStore(new QBackingStore(win));
} else {
q->setAttribute(Qt::WA_PaintOnScreen, true);
}
}
QBackingStore会调用QPA来创建QPlatformBackingStore,在Windows就会创建QWindowsBackingStore
QPlatformBackingStore *QBackingStore::handle() const
{
if (!d_ptr->platformBackingStore) {
d_ptr->platformBackingStore = QGuiApplicationPrivate::platformIntegration()->createPlatformBackingStore(d_ptr->window);
d_ptr->platformBackingStore->setBackingStore(const_cast<QBackingStore*>(this));
}
return d_ptr->platformBackingStore;
}
当调用QWidgetPrivate::drawWidget(QPaintDevice *pdev, const QRegion &rgn, const QPoint &offset, int flags,QPainter *sharedPainter, QWidgetBackingStore *backingStore)的时候,传进来的QPanitDevice就是QPA里面返回的QImage。如果QPA插件没有提供绘制引擎,就会使用Qt自己的QRasterPaintEngine。
QPaintEngine *QImage::paintEngine() const
{
if (!d)
return 0;
if (!d->paintEngine) {
QPaintDevice *paintDevice = const_cast<QImage *>(this);
QPaintEngine *paintEngine = 0;
QPlatformIntegration *platformIntegration = QGuiApplicationPrivate::platformIntegration();
if (platformIntegration)
paintEngine = platformIntegration->createImagePaintEngine(paintDevice);
d->paintEngine = paintEngine ? paintEngine : new QRasterPaintEngine(paintDevice);
}
return d->paintEngine;
}
当绘制结束的时候就调用QWindowsBackingStore::flush显示到本地窗口上。这样QPainter绘制的内容和QWidget的子控件就绘制到本地窗体上了。
QPlatformIntegration部分接口
class Q_GUI_EXPORT QPlatformIntegration
{
public:
// 创建本地窗体
virtual QPlatformWindow *createPlatformWindow(QWindow *window) const = 0;
// 通过WId创建窗体,比如在Windows上HWND
virtual QPlatformWindow *createForeignWindow(QWindow *, WId) const { return 0; }
// 主要是提供绘制设备 QImage
virtual QPlatformBackingStore *createPlatformBackingStore(QWindow *window) const = 0;
#ifndef QT_NO_OPENGL
virtual QPlatformOpenGLContext *createPlatformOpenGLContext(QOpenGLContext *context) const;
#endif
// 提供一个绘制引擎,如果不提供,将使用Qt的QRasterPaintEngine
virtual QPaintEngine *createImagePaintEngine(QPaintDevice *paintDevice) const;
// Event dispatcher:
// 事件相关
virtual QAbstractEventDispatcher *createEventDispatcher() const = 0;
// 字体相关
virtual QPlatformFontDatabase *fontDatabase() const;
#ifndef QT_NO_CLIPBOARD
// 剪切板相关
virtual QPlatformClipboard *clipboard() const;
#endif
#ifndef QT_NO_DRAGANDDROP
// 拖拽相关
virtual QPlatformDrag *drag() const;
#endif
相关推荐
- 史上最全的浏览器兼容性问题和解决方案
-
微信ID:WEB_wysj(点击关注)◎◎◎◎◎◎◎◎◎一┳═┻︻▄(页底留言开放,欢迎来吐槽)●●●...
-
- 平面设计基础知识_平面设计基础知识实验收获与总结
-
CSS构造颜色,背景与图像1.使用span更好的控制文本中局部区域的文本:文本;2.使用display属性提供区块转变:display:inline(是内联的...
-
2025-02-21 16:01 yuyutoo
- 写作排版简单三步就行-工具篇_作文排版模板
-
和我们工作中日常word排版内部交流不同,这篇教程介绍的写作排版主要是用于“微信公众号、头条号”网络展示。写作展现的是我的思考,排版是让写作在网格上更好地展现。在写作上花费时间是有累积复利优势的,在排...
- 写一个2048的游戏_2048小游戏功能实现
-
1.创建HTML文件1.打开一个文本编辑器,例如Notepad++、SublimeText、VisualStudioCode等。2.将以下HTML代码复制并粘贴到文本编辑器中:html...
- 今天你穿“短袖”了吗?青岛最高23℃!接下来几天气温更刺激……
-
最近的天气暖和得让很多小伙伴们喊“热”!!! 昨天的气温到底升得有多高呢?你家有没有榜上有名?...
- CSS不规则卡片,纯CSS制作优惠券样式,CSS实现锯齿样式
-
之前也有写过CSS优惠券样式《CSS3径向渐变实现优惠券波浪造型》,这次再来温习一遍,并且将更为详细的讲解,从布局到具体样式说明,最后定义CSS变量,自定义主题颜色。布局...
- 你的自我界限够强大吗?_你的自我界限够强大吗英文
-
我的结果:A、该设立新的界限...
- 行内元素与块级元素,以及区别_行内元素和块级元素有什么区别?
-
行内元素与块级元素首先,CSS规范规定,每个元素都有display属性,确定该元素的类型,每个元素都有默认的display值,分别为块级(block)、行内(inline)。块级元素:(以下列举比较常...
-
- 让“成都速度”跑得潇潇洒洒,地上地下共享轨交繁华
-
去年的两会期间,习近平总书记在参加人大会议四川代表团审议时,对治蜀兴川提出了明确要求,指明了前行方向,并带来了“祝四川人民的生活越来越安逸”的美好祝福。又是一年...
-
2025-02-21 16:00 yuyutoo
- 今年国家综合性消防救援队伍计划招录消防员15000名
-
记者24日从应急管理部获悉,国家综合性消防救援队伍2023年消防员招录工作已正式启动。今年共计划招录消防员15000名,其中高校应届毕业生5000名、退役士兵5000名、社会青年5000名。本次招录的...
- 一起盘点最新 Chrome v133 的5大主流特性 ?
-
1.CSS的高级attr()方法CSSattr()函数是CSSLevel5中用于检索DOM元素的属性值并将其用于CSS属性值,类似于var()函数替换自定义属性值的方式。...
- 竞走团体世锦赛5月太仓举行 世界冠军杨家玉担任形象大使
-
style="text-align:center;"data-mce-style="text-align:...
- 学物理能做什么?_学物理能做什么 卢昌海
-
作者:曹则贤中国科学院物理研究所原标题:《物理学:ASourceofPowerforMan》在2006年中央电视台《对话》栏目的某期节目中,主持人问过我一个的问题:“学物理的人,如果日后不...
-
- 你不知道的关于这只眯眼兔的6个小秘密
-
在你们忙着给熊本君做表情包的时候,要知道,最先在网络上引起轰动的可是这只脸上只有两条缝的兔子——兔斯基。今年,它更是迎来了自己的10岁生日。①关于德艺双馨“老艺...
-
2025-02-21 16:00 yuyutoo
你 发表评论:
欢迎- 一周热门
- 最近发表
- 标签列表
-
- mybatis plus (70)
- scheduledtask (71)
- css滚动条 (60)
- java学生成绩管理系统 (59)
- 结构体数组 (69)
- databasemetadata (64)
- javastatic (68)
- jsp实用教程 (53)
- fontawesome (57)
- widget开发 (57)
- vb net教程 (62)
- hibernate 教程 (63)
- case语句 (57)
- svn连接 (74)
- directoryindex (69)
- session timeout (58)
- textbox换行 (67)
- extension_dir (64)
- linearlayout (58)
- vba高级教程 (75)
- iframe用法 (58)
- sqlparameter (59)
- trim函数 (59)
- flex布局 (63)
- contextloaderlistener (56)