Qt中QPA插件,Qt跨平台的基础

前言

Qt是一个跨平台的C++框架,各位小伙伴在使用Qt的时候有没有想过Qt是怎么实现跨平台的呢?Qt为了实现跨平台做了哪些工作。现在我们就来探讨一下Qt跨平台的基础,QPA插件。文章有点长,请各位小伙伴耐心看完。

QPA简介

QPA的全称是Qt Platform Abstraction,即Qt平台抽象层。在Qt5中替代QWS(Qt Window System)。QPA插件是通过对各种QPlatform* 类进行子类化来实现的。有几个根类,例如用于窗口系统集成的QPlatformIntegrationQPlatformWindow,以及用于更深入的平台主题和集成的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

真正创建是在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(customMarginsV);

    QWindowsWindowData obtained =
        QWindowsWindowData::create(window, requested,
                                   QWindowsWindow::formatWindowTitle(window->title()));
    qCDebug(lcQpaWindows).nospace()
        << __FUNCTION__ << ' ' << window
        << "
    Requested: " << requested.geometry << " frame incl.="
        << QWindowsGeometryHint::positionIncludesFrame(window)
        << ' ' << requested.flags
        << "
    Obtained : " << obtained.geometry << " margins=" << obtained.frame
        << " handle=" << obtained.hwnd << ' ' << obtained.flags << '
';

    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 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(title.utf16());
    const wchar_t *classNameUtf16 = reinterpret_cast(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
        << '
' << *this << "
requested: " << 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(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(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(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

页面更新:2024-04-10

标签:列子   插件   平台   窗体   指针   底层   源码   窗口   文件   基础   系统

1 2 3 4 5

上滑加载更多 ↓
更多:

本站资料均由网友自行发布提供,仅用于学习交流。如有版权问题,请与我联系,QQ:4156828  

© CopyRight 2020-2024 All Rights Reserved. Powered By 71396.com 闽ICP备11008920号-4
闽公网安备35020302034903号

Top