QML Book 第十六章 用 C++ 擴展 QML

16.用 C++ 擴展 QML(Extending QML with C++

本章的作者:jryannel

注意:
最新的構建時間:2016/03/21
這章的源代碼能夠在assetts folder找到。

在 QML 作為語言提供的有限空間內執行 QML 有時可能會受到限制。通過使用 C++ 編寫的本機功能擴展 QML 運行時環境,應用程序可以利用基礎平臺的全面性能和自由度。

16.1 了解QML運行時環境

當運行 QML 時,它在一個運行時環境下執行。這個運行時環境是由 QtQml 模塊下的 C++ 代碼實現的。它由一個負責執行 QML 的引擎,持有訪問每個組件屬性的上下文和實例化的 QML 元素組件構成。

#include <QtGui>
#include <QtQml>

int main(int argc, char **argv)
{
    QGuiApplication app(argc, argv);
    QUrl source(QStringLiteral("qrc:/main.qml"));
    QQmlApplicationEngine engine;
    engine.load(source);
    return app.exec();
}

在該示例中,QGuiApplication 封裝了與應用程序實例相關的所有內容(例如,應用程序名稱,命令行參數和管理事件循環)。QQmlApplicationEngine 管理上下文和組件的分層次序。它需要典型的 qml 文件作為應用程序的起點加載。在這種情況下,它是一個包含窗口和文本類型的 main.qml。

注意:
通過 QmlApplicationEngine 加載一個簡單的項目作為根類型的 main.qml 將不會在我們的顯示器上顯示任何內容,因為它需要一個窗口來管理表面以進行渲染。引擎能夠加載不包含任何用戶界面(例如普通對象)的 qml 代碼。因此,默認情況下不會為我們創建一個窗口。qmlscene 或新的 qml 運行時環境將在內部首先檢查主 qml 文件是否包含一個窗口作為根項目,如果沒有為我們創建一個,并將根項目設置為新創建的窗口的子項。

import QtQuick 2.5
import QtQuick.Window 2.2

Window {
    visible: true
    width: 512
    height: 300

    Text {
        anchors.centerIn: parent
        text: "Hello World!"
    }
}

在 qml 文件中,我們聲明我們的依賴關系是 QtQuick 和 QtQuick.Window。這些聲明將觸發在導入路徑中對這些模塊進行查找,而成功則將引擎引導所需的插件。然后將新加載的類型提供給由 qmldir 控制的 qml 文件。

也可以通過將引擎類型直接添加到引擎來快捷插入創建。這里我們假設我們有一個基于 CurrentTime QObject 的類。

QQmlApplicationEngine engine;

qmlRegisterType<CurrentTime>("org.example", 1, 0, "CurrentTime");

engine.load(source);

現在我們也可以在 qml 文件中使用 CurrentTime 類型。

import org.example 1.0

CurrentTime {
    // access properties, functions, signals
}

對于真正的懶惰者,還可以通過上下文屬性的非常直接的方法。

QScopedPointer<CurrentTime> current(new CurrentTime());

QQmlApplicationEngine engine;

engine.rootContext().setContextProperty("current", current.value())

engine.load(source);

注意:
不要混合 setContextProperty() 和 setProperty()。第一個在 qml 上下文中設置上下文屬性,setProperty() 在 QObject 上設置動態屬性值,后者不會在使用 C++ 擴展 QML 時幫到我們。

現在,我們可以在應用程序中隨處可見的當前屬性。感謝上下文繼承。

import QtQuick 2.5
import QtQuick.Window 2.0

Window {
    visible: true
    width: 512
    height: 300

    Component.onCompleted: {
        console.log('current: ' + current)
    }
}

以下是擴展 QML 的不同方式:

  • Context properties —— setContextProperty()
  • Register type with engine —— 在main.cpp中調用qmlRegisterType
  • QML extension plugins —— 接下來會討論

Context properties 易于用于小型應用程序。他們不需要太多的努力,只需要暴露我們的系統 API 與各種全局對象。確保不會有任何命名沖突(例如通過使用此特殊字符 $ 例如 $.currentTime)是有幫助的。$ 是 JS 變量的有效字符。

Registering QML types 允許用戶從 QML 控制 C++ 對象的生命周期。這不可能與上下文屬性。它也不會污染全局命名空間。仍然需要首先注冊所有類型,所有這些庫需要在應用程序啟動時進行鏈接,這在大多數情況下并不是一個問題。

最靈活的系統由QML extension plugins提供。它們允許我們在第一個 QML 文件調用導入標識符時加載的插件中注冊類型。同樣通過使用 QML 單例,不需要再污染全局命名空間。插件允許我們跨項目重用模塊,當我們使用 Qt 執行多個項目時,該模塊非常方便。

本章的其余部分將重點介紹 qml 擴展插件。因為它們提供了靈活性和重用性。

16.2 插件內容

插件是一個具有定義接口的庫,它是按需加載的。這與庫的不同之處在于庫在啟動應用程序時被鏈接和加載。在 QML 案例中,該界面稱為 QQmlExtensionPlugin。 有兩種方法對我們 initializeEngine() 和 registerTypes() 有興趣。當首先加載插件時,將調用 initializeEngine(),這允許我們訪問引擎以將插件對象暴露給根上下文。在大多數情況下,我們將只使用 registerTypes() 方法。這允許我們使用提供的網址上的引擎注冊自定義 QML 類型。

我們稍微退一步考慮一個潛在的文件 IO 類型,它允許我們在 QML 中讀取/寫入一個小型文本文件。第一次的迭代可能看起來像在嘲笑 QML 的實現。

// FileIO.qml (good)
QtObject {
    function write(path, text) {};
    function read(path) { return "TEXT"}
}

這是一個用于探索 API 的可能的基于 C++ 的 QML API 的純 qml 實現。我們看到我們應該有一個讀寫功能。寫功能采用路徑和文本,讀函數采用路徑并返回文本。因為它看起來路徑和文本是常見的參數,也許我們可以提取它們作為屬性。

// FileIO.qml (better)
QtObject {
    property url source
    property string text
    function write() { // open file and write text };
    function read() { // read file and assign to text };
}

是的,這看起來更像是一個 QML API。我們使用屬性來允許我們的環境綁定到我們的屬性并對更改做出反應。

要在 C++ 中創建這個 API,我們需要創建一個這樣的接口。

class FileIO : public QObject {
    ...
    Q_PROPERTY(QUrl source READ source WRITE setSource NOTIFY sourceChanged)
    Q_PROPERTY(QString text READ text WRITE setText NOTIFY textChanged)
    ...
public:
    Q_INVOKABLE void read();
    Q_INVOKABLE void write();
    ...
}

該 FileIO 類型需要向 QML 引擎注冊。我們想在“org.example.io”模塊下使用它。

import org.example.io 1.0

FileIO {
}

一個插件可以使用相同的模塊暴露幾種類型。但是它不能從一個插件暴露幾個模塊。 因此,模塊和插件之間存在一對一的關系。該關系由模塊標識符表示。

16.3 創建插件

Qt Creator 包含一個創建 QtQuick 2 QML 擴展插件的向導,我們使用它來創建一個名為 fileio 的插件,其中 FileIO 對象以 “org.example.io” 模塊開頭。

該插件類是從 QQmlExtensionPlugin 衍生出來的,并實現了 registerTypes() 函數。Q_PLUGIN_METADATA 行是將插件標識為 qml 擴展插件必須的。除此之外,還沒有什么壯觀的。

#ifndef FILEIO_PLUGIN_H
#define FILEIO_PLUGIN_H

#include <QQmlExtensionPlugin>

class FileioPlugin : public QQmlExtensionPlugin
{
    Q_OBJECT
    Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QQmlExtensionInterface")

public:
    void registerTypes(const char *uri);
};

#endif // FILEIO_PLUGIN_H

在 registerTypes 的實現中,我們使用 qmlRegisterType 函數簡單地注冊我們的 FileIO 類。

#include "fileio_plugin.h"
#include "fileio.h"

#include <qqml.h>

void FileioPlugin::registerTypes(const char *uri)
{
    // @uri org.example.io
    qmlRegisterType<FileIO>(uri, 1, 0, "FileIO");
}

有趣的是,我們看不到這里的模塊 URI(例如 org.example.io)。這似乎是從外面來的。

當我們查看項目目錄時,我們將找到一個 qmldir 文件。此文件指定我們的 qml 插件的內容,或更好地向 QML 端插入插件。它應該看起來像這樣。

module org.example.io
plugin fileio

該模塊是我們的插件可被其他人訪問的 URI,插件行必須與我們的插件文件名相同(在mac下,這將是文件系統上的 libfileio_debug.dylib 和 qmldir 中的 fileio)。這些文件由 Qt Creator 基于給定的信息創建。模塊 uri 也可以在 .pro 文件中使用。有用于建立安裝目錄。

當您在構建文件夾中調用 make install 時,庫將被復制到 Qt qml 文件夾(對于Mac上的 Qt 5.4,這將是 “~/Qt/5.4/clang_64/qml”,確切的路徑取決于我們的 Qt 安裝位置和我們系統上使用的編譯器)。在那里我們將在 “org/example/io” 文件夾中找到一個庫。內容是目前這兩個文件:

libfileio_debug.dylib
qmldir

當導入稱為 “org.example.io” 的模塊時,qml 引擎將查找其中一個導入路徑,并嘗試使用 qmldir 查找 “org/example/io” 路徑。然后,qmldir 會告訴引擎哪個庫加載為 qml 擴展插件,使用哪個模塊 URI。具有相同 URI 的兩個模塊將相互覆蓋。

16.4 FileIO 實現

FileIO 的實現很簡單。記住我們要創建的 API 應該是這樣的。

class FileIO : public QObject {
    ...
    Q_PROPERTY(QUrl source READ source WRITE setSource NOTIFY sourceChanged)
    Q_PROPERTY(QString text READ text WRITE setText NOTIFY textChanged)
    ...
public:
    Q_INVOKABLE void read();
    Q_INVOKABLE void write();
    ...
}

我們將保留屬性,因為它們是簡單的設置者和獲取者。

讀取方法以讀取模式打開文件,并使用文本流讀取數據。

void FileIO::read()
{
    if(m_source.isEmpty()) {
        return;
    }
    QFile file(m_source.toLocalFile());
    if(!file.exists()) {
        qWarning() << "Does not exits: " << m_source.toLocalFile();
        return;
    }
    if(file.open(QIODevice::ReadOnly)) {
        QTextStream stream(&file);
        m_text = stream.readAll();
        emit textChanged(m_text);
    }
}

當文本更改時,有必要使用 emit textChanged(m_text) 通知他人有關更改。否則屬性綁定將不起作用。

寫入方法執行相同操作,但以寫入模式打開文件,并使用流寫入內容。

void FileIO::write()
{
    if(m_source.isEmpty()) {
        return;
    }
    QFile file(m_source.toLocalFile());
    if(file.open(QIODevice::WriteOnly)) {
        QTextStream stream(&file);
        stream << m_text;
    }
}

不要忘了在最后調用 make install。否則我們的插件文件將不會被復制到 qml 文件夾,而 qml 引擎將無法找到該模塊。

由于讀取和寫入會阻塞程序運行,我們應該只使用該 FileIO 處理小型文本,否則我們將阻塞 Qt 的 UI 線程。請一定要注意!

16.5 使用 FileIO

現在我們可以使用我們新創建的文件來訪問一些不錯的數據。在這個例子中,我們想以 JSON 格式讀取一些城市數據,并將其顯示在表格中。我們將使用兩個項目,一個是擴展插件(稱為fileio),它為我們提供了一種從文件中讀取和寫入文本的方法,另一個是通過使用文件 io 來讀取和顯示表中的數據(CityUI) 寫文件,此示例中使用的數據位于 cities.json 文件中。

cityui_mock

JSON 只是文本,它被格式化為可以轉換成有效的 JS 對象/數組并返回到文本。我們使用我們的 FileIO 來讀取 JSON 格式的數據,并使用 JSON.parse() 將其轉換為 JS 對象。數據后來用作表視圖的模型。這大概是我們讀取文檔功能的內容。為了保存,將數據轉換為文本格式,并使用寫入功能進行保存。

城市 JSON 數據是一個格式化的文本文件,包含一組城市數據條目,其中每個條目都包含有關城市的有趣數據。

[
    {
        "area": "1928",
        "city": "Shanghai",
        "country": "China",
        "flag": "22px-Flag_of_the_People's_Republic_of_China.svg.png",
        "population": "13831900"
    },
    ...
]

16.5.1 應用程序窗口

我們使用 Qt Creator QtQuick 應用程序向導創建基于 Qt Quick Controls 的應用程序。我們不會使用新的 QML 表單,因為這在一本書中很難解釋,盡管使用 ui.qml 文件的新表單方法比以前更有用。所以你現在可以移除/刪除表單文件。

基本設置是一個 ApplicationWindow,它可以包含工具欄,菜單欄和狀態欄。我們將只使用菜單欄來創建一些標準的菜單條目來打開和保存文檔。基本設置只會顯示一個空的窗口。

import QtQuick 2.5
import QtQuick.Controls 1.3
import QtQuick.Window 2.2
import QtQuick.Dialogs 1.2

ApplicationWindow {
    id: root
    title: qsTr("City UI")
    width: 640
    height: 480
    visible: true
}

16.5.2 使用 Actions

為了更好地使用/重用我們的命令,我們使用 QML Action 類型。這將允許我們以后對潛在的工具欄使用相同的操作。開放和保存退出操作是退出標準。打開和保存動作不包含任何邏輯,我們稍后再來。使用文件菜單和這三個操作條目創建菜單。另外我們準備了一個文件對話框,這樣我們可以稍后選擇我們的城市文件。聲明時,對話框不可見,我們需要使用 open() 方法來顯示。

...
Action {
    id: save
    text: qsTr("&Save")
    shortcut: StandardKey.Save
    onTriggered: { }
}

Action {
    id: open
    text: qsTr("&Open")
    shortcut: StandardKey.Open
    onTriggered: {}
}

Action {
    id: exit
    text: qsTr("E&xit")
    onTriggered: Qt.quit();
}

menuBar: MenuBar {
    Menu {
        title: qsTr("&File")
        MenuItem { action: open }
        MenuItem { action: save }
        MenuSeparator { }
        MenuItem { action: exit }
    }
}

...

FileDialog {
    id: openDialog
    onAccepted: { }
}

16.5.3 格式化表格

城市數據的內容應顯示在表格中。為此,我們使用 TableView 控件并聲明4列:城市,國家,地區,人口。 每列都是標準的 TableViewColumn。稍后我們將添加標記列和刪除操作,這將需要自定義列代理。

TableView {
    id: view
    anchors.fill: parent
    TableViewColumn {
        role: 'city'
        title: "City"
        width: 120
    }
    TableViewColumn {
        role: 'country'
        title: "Country"
        width: 120
    }
    TableViewColumn {
        role: 'area'
        title: "Area"
        width: 80
    }
    TableViewColumn {
        role: 'population'
        title: "Population"
        width: 80
    }
}

現在應用程序應該顯示一個菜單欄,文件菜單和一個帶有 4 個表頭的空表。下一步將使用我們的 FileIO 擴展名填充有用數據的表。

cityui_empty

cities.json 文檔是一系列城市條目。這是一個例子。

[
    {
        "area": "1928",
        "city": "Shanghai",
        "country": "China",
        "flag": "22px-Flag_of_the_People's_Republic_of_China.svg.png",
        "population": "13831900"
    },
    ...
]

我們的工作是允許用戶選擇文件,讀取它,轉換并將其設置到表視圖上。

16.5.4 讀取數據

為此,我們讓打開的操作打開文件對話框。當用戶選擇文件時,在文件對話框中調用 onAccepted 方法。在那里我們稱之為 readDocument() 函數。readDocument() 函數將文件對話框中的 url 設置為 FileIO 對象,并調用 read() 方法。然后使用 JSON.parse() 方法解析來自 FileIO 的加載文本,并將生成的對象作為模型直接設置到表視圖上。這樣非常方便。

Action {
    id: open
    ...
    onTriggered: {
        openDialog.open()
    }
}

...

FileDialog {
    id: openDialog
    onAccepted: {
        root.readDocument()
    }
}

function readDocument() {
    io.source = openDialog.fileUrl
    io.read()
    view.model = JSON.parse(io.text)
}


FileIO {
    id: io
}

16.5.5 寫入數據

為了保存文檔,我們將保存操作掛接到 saveDocument() 函數。保存文檔函數從視圖中獲取模型,該視圖是一個 JS 對象,并使用 JSON.stringify() 函數將其轉換為字符串。生成的字符串設置為 FileIO 對象的 text 屬性,我們調用 write() 將數據保存到磁盤。 stringify 函數中的 “null” 和 “4” 參數將使用 4 個空格的縮進格式化生成的 JSON 數據。這只是為了更好地閱讀保存的文檔。

Action {
    id: save
    ...
    onTriggered: {
        saveDocument()
    }
}

function saveDocument() {
    var data = view.model
    io.text = JSON.stringify(data, null, 4)
    io.write()
}

FileIO {
    id: io
}

這基本上是閱讀,編寫和顯示 JSON 文檔的應用程序。想想通過編寫 XML 讀者和作家花費的所有時間。使用 JSON,我們需要的是讀取和寫入文本文件或發送接收文本緩沖區的方式。

cityui_table

16.5.6 畫龍點睛

該應用程序尚未完成準備。我們仍然希望顯示標志,并允許用戶通過從模型中移除城市來修改文檔。

相對于 flags 文件夾中的 main.qml 文檔,存儲此示例的標志。為了能夠顯示它們,表列需要定義一個用于渲染標志圖像的自定義代理。

TableViewColumn {
    delegate: Item {
        Image {
            anchors.centerIn: parent
            source: 'flags/' + styleData.value
        }
    }
    role: 'flag'
    title: "Flag"
    width: 40
}

就這些。它將 JS 模型的 flag 屬性作為 styleData.value 公開給代理。然后代理將圖像路徑調整為預先掛起 'flags /' 并顯示它。

為了刪除,我們使用類似的技術來顯示刪除按鈕。

TableViewColumn {
    delegate: Button {
        iconSource: "remove.png"
        onClicked: {
            var data = view.model
            data.splice(styleData.row, 1)
            view.model = data
        }
    }
    width: 40
}

對于數據刪除操作,我們將保持視圖模型,然后使用 JS 拼接函數刪除一個條目。這種方法對于我們來說是可用的,因為模型來自 JS 類型的數組。拼接方法通過刪除現有元素和/或添加新元素來更改數組的內容。

不幸的是,JS 數組不如像 QAbstractItemModel 這樣的 Qt 模型那么聰明,它將通知有關行更改或數據更改的視圖。該視圖現在不會顯示任何更新的數據,因為它從未被通知任何更改。只有當我們將數據設置回視圖時,視圖才能識別出新數據并刷新視圖內容。使用 view.model = data 再次設置模型是讓視圖知道有一個數據更改的一種方式。

cityui_populated

16.6 本章總結

插件的創建非常簡單,但是它可以復用,并且為不同的應用程序擴展類型。使用創建的插件是非常靈活的解決方案。例如你可以只使用 qmlscene 開始創建 UI。打開 CityUI 項目文件夾,從 qmlscene 的 main.qml 開始。我真的鼓勵大家使用與 qmlscene 一起工作的方式寫應用程序。對于 UI 開發者,這將是一個巨大的改變,同時也是保持清晰分離的好習慣。

使用插件有一個缺點,對于簡單的應用程序開發增加了難度。我們需要為我們的應用程序開發插件。如果這是一個問題,我們也可以使用與 FileIO 對象相同的機制使用qmlRegisterType 直接注冊到你的 main.cpp 中。QML 代碼保持一樣就可以了。

通常在大型項目中,你不會像這樣使用應用程序。你有一個與 qmlscene 類似的簡單的 qml 運行環境,并且需要所有本地的功能插件。你的項目使用這些 qml 擴展插件,也是簡單純粹的 qml 項目。這為 UI 的變換提供了最大的靈活性并移除了編譯步驟。在編輯完成一個 QML 文件后,你只需要運行 UI。這允許用戶界面開發者保持靈活性并迅速的使所有的小修改立刻得到響應。

插件在 C++ 后端開發和 QML 前端開發之間提供了一個很好和干凈的分離。在開發 QML 插件時,始終將 QML 方面考慮在內,并且在使用 C++ 實現之前,首先要使用僅限 QML 的模型來驗證您的 API。如果一個 API 是用 C++ 編寫的,那么人們經常會猶豫改變它,或者說不要重寫它。在 QML 中模擬 API 提供了更多的靈活性和更少的初始資源。當使用插件時,模擬的 API 和真正的 API 之間的切換只是改變 qml 運行時的導入路徑。

本章完。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,786評論 6 534
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,656評論 3 419
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,697評論 0 379
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,098評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,855評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,254評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,322評論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,473評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,014評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,833評論 3 355
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,016評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,568評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,273評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,680評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,946評論 1 288
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,730評論 3 393
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,006評論 2 374

推薦閱讀更多精彩內容

  • Spring Cloud為開發人員提供了快速構建分布式系統中一些常見模式的工具(例如配置管理,服務發現,斷路器,智...
    卡卡羅2017閱讀 134,787評論 18 139
  • 2.概覽 本章將介紹如果開始使用 Qt 5 進行開發。將展示如何安裝 Qt SDK,以及如何使用 Qt Creat...
    趙者也閱讀 1,533評論 3 2
  • 15.Qt 和 C++(Qt and C++) 本章的作者:jryannel ** 注意: **最新的構建時間:2...
    趙者也閱讀 1,249評論 0 3
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,666評論 25 708
  • 孩子上班心歡喜, 鍥而不舍直前行 腳踏實地接地氣 一絲不茍挺認真 于心不忍打擾她 努力工作有志氣 愿她工作有成績 ...
    淺若燦陽閱讀 229評論 8 6