自定义 Qt Creator 向导

我们在使用 Qt Creator 创建项目时,弹出的 New File or Project 对话框,便是创建项目的向导。我们可以自定义自己的向导,使用自己的项目模板,来生成自己的初始项目。

向导

Qt Creator 支持json、xml两种格式的向导描述文件,但是xml格式官方已不推荐使用,以下只介绍json格式。

向导文件存放于 Qt Creator 安装目录中的 share/qtcreator/templates/wizards 目录下,向导文件包含用于生成目标的模板文件和 wizard.json 向导配置文件。开发者还可以将制作的向导文件放置在 $HOME/.config/QtProject/qtcreator/templates/wizards 中,放在此目录下的向导只对当前用户有效。
向导文件中支持使用变量,形如:%\{<variableName>\},在 wizard.json 文件中可以定义 options 区域,用于申明新变量和值。
变量支持JS表达式,形如:%\{JS:<JavaScript expression>\},表达式会被计算并将结果转换成字符串。在表达式中也可以使用定义过的变量,形如:value('<variableName>'),此返回的值可以使字符串、列表、字典或布尔。

另外,除了能使用 wizard.json 中定义的变量以外,Qt Creator 还提供了一些内置的变量。

  • WizardDir - wizard.json 文件的绝对路径
  • Features - Qt Creator 中可用的工具包的所有功能列表
  • Plugins - 当前 Qt Creator 中运行的所有插件的列表
  • Platform - New File or Project 对话框中选择的平台。可能为空
  • InitialPath - 在项目视图中的节点上下文菜单触发向导时,选定节点的路径
  • ProjectExplorer.Profile.Ids - 选定节点的项目配置的工具包列表

开发向导时,官方推荐在启动 Qt Creator 时添加 -customwizard-verbose 参数,以便输出更多调试信息。

添加向导

Qt Creator 提供了一个快捷键 Factory.Reset,用于在不重启IDE的情况下重新加载向导。默认情况下,该快捷键没有对应实际的按键,在 工具 > 选项... > 环境 > 键盘 中搜索 Factory.Reset,设置自己习惯的按键即可,方便调试。

下面我们通过官方的C++类创建向导,来分析模板文件的内容。
示例位于 Qt Creator 安装目录下 share/qtcreator/templates/wizards/classes/cpp 中。

C++类创建向导中的模板文件

首先打开 wizard.json 配置文件

{
    "version": 1,
    "supportedProjectTypes": [ ],
    "id": "A.Class",
    "category": "O.C++",
    "trDescription": "Creates a C++ header and a source file for a new class that you can add to a C++ project.",
    "trDisplayName": "C++ Class",
    "trDisplayCategory": "C++",
    "iconText": "h/cpp",
    "enabled": "%{JS: value('Plugins').indexOf('CppEditor') >= 0}",

    "options":
    [
        { "key": "TargetPath", "value": "%{Path}" },
        { "key": "HdrPath", "value": "%{Path}/%{HdrFileName}" },
        { "key": "SrcPath", "value": "%{Path}/%{SrcFileName}" },
        { "key": "CN", "value": "%{JS: Cpp.className(value('Class'))}" },
        { "key": "Base", "value": "%{JS: value('BaseCB') === '' ? value('BaseEdit') : value('BaseCB')}" },
        { "key": "isQObject", "value": "%{JS: [ 'QObject', 'QWidget', 'QMainWindow', 'QDeclarativeItem', 'QQuickItem'].indexOf(value('Base')) >= 0 }" },
        { "key": "GUARD", "value": "%{JS: Cpp.classToHeaderGuard(value('Class'), Util.suffix(value('HdrFileName')))}" },
        { "key": "SharedDataInit", "value": "%{JS: (value('IncludeQSharedData')) ? 'data(new %{CN}Data)' : '' }" }
    ],

    "pages":
    [
        {
            "trDisplayName": "Define Class",
            "trShortTitle": "Details",
            "typeId": "Fields",
            "data" :
            [
                {
                    "name": "Class",
                    "trDisplayName": "Class name:",
                    "mandatory": true,
                    "type": "LineEdit",
                    "data": { "validator": "(?:(?:[a-zA-Z_][a-zA-Z_0-9]*::)*[a-zA-Z_][a-zA-Z_0-9]*|)" }
                },
                {
                    "name": "BaseCB",
                    "trDisplayName": "Base class:",
                    "type": "ComboBox",
                    "data":
                    {
                        "items": [ { "trKey": "<Custom>", "value": "" },
                                   "QObject", "QWidget", "QMainWindow", "QDeclarativeItem", "QQuickItem" ]
                    }
                },
                {
                    "name": "BaseEdit",
                    "type": "LineEdit",
                    "enabled": "%{JS: value('BaseCB') === ''}",
                    "mandatory": false,
                    "data":
                    {
                        "trText": "%{BaseCB}",
                        "trDisabledText": "%{BaseCB}"
                    }
                },

                {
                    "name": "Sp1",
                    "type": "Spacer",
                    "data": { "factor": 2 }
                },
                {
                    "name": "IncludeQObject",
                    "trDisplayName": "Include QObject",
                    "type": "CheckBox",
                    "data":
                    {
                        "checkedValue": "QObject",
                        "uncheckedValue": "",
                        "checked": "%{JS: value('BaseCB') === 'QObject'}"
                    }
                },
                {
                    "name": "IncludeQWidget",
                    "trDisplayName": "Include QWidget",
                    "type": "CheckBox",
                    "data":
                    {
                        "checkedValue": "QWidget",
                        "uncheckedValue": "",
                        "checked": "%{JS: value('BaseCB') === 'QWidget'}"
                    }
                },
                {
                    "name": "IncludeQMainWindow",
                    "trDisplayName": "Include QMainWindow",
                    "type": "CheckBox",
                    "data":
                    {
                        "checkedValue": "QMainWindow",
                        "uncheckedValue": "",
                        "checked": "%{JS: value('BaseCB') === 'QMainWindow'}"
                    }
                },
                {
                    "name": "IncludeQDeclarativeItem",
                    "trDisplayName": "Include QDeclarativeItem - Qt Quick 1",
                    "type": "CheckBox",
                    "data":
                    {
                        "checkedValue": "QDeclarativeItem",
                        "uncheckedValue": "",
                        "checked": "%{JS: value('BaseCB') === 'QDeclarativeItem'}"
                    }
                },
                {
                    "name": "IncludeQQuickItem",
                    "trDisplayName": "Include QQuickItem - Qt Quick 2",
                    "type": "CheckBox",
                    "data":
                    {
                        "checkedValue": "QQuickItem",
                        "uncheckedValue": "",
                        "checked": "%{JS: value('BaseCB') === 'QQuickItem'}"
                    }
                },
                {
                    "name": "IncludeQSharedData",
                    "trDisplayName": "Include QSharedData",
                    "type": "CheckBox",
                    "data":
                    {
                        "checkedValue": "QSharedData",
                        "uncheckedValue": "",
                        "checked": false
                    }
                },
                {
                    "name": "Sp2",
                    "type": "Spacer"
                },

                {
                    "name": "HdrFileName",
                    "type": "LineEdit",
                    "trDisplayName": "Header file:",
                    "mandatory": true,
                    "data": { "trText": "%{JS: Cpp.classToFileName(value('Class'), Util.preferredSuffix('text/x-c++hdr'))}" }
                },
                {
                    "name": "SrcFileName",
                    "type": "LineEdit",
                    "trDisplayName": "Source file:",
                    "mandatory": true,
                    "data": { "trText": "%{JS: Cpp.classToFileName(value('Class'), Util.preferredSuffix('text/x-c++src'))}" }
                },
                {
                    "name": "Path",
                    "type": "PathChooser",
                    "trDisplayName": "Path:",
                    "mandatory": true,
                    "data":
                    {
                        "kind": "directory",
                        "basePath": "%{InitialPath}",
                        "path": "%{InitialPath}"
                    }
                }
            ]
        },
        {
            "trDisplayName": "Project Management",
            "trShortTitle": "Summary",
            "typeId": "Summary"
        }
    ],

    "generators":
    [
        {
            "typeId": "File",
            "data":
            [
                {
                    "source": "file.h",
                    "target": "%{HdrPath}",
                    "openInEditor": true,
                    "options": [
                        { "key": "Cpp:License:FileName", "value": "%{HdrFileName}" },
                        { "key": "Cpp:License:ClassName", "value": "%{CN}" }
                    ]
                },
                {
                    "source": "file.cpp",
                    "target": "%{SrcPath}",
                    "openInEditor": true,
                    "options": [
                        { "key": "Cpp:License:FileName", "value": "%{SrcFileName}" },
                        { "key": "Cpp:License:ClassName", "value": "%{CN}" }
                    ]
                }
            ]
        }
    ]
}

  • version - 文件的版本号,不要修改该值
  • supportedProjectTypes - 向已存在的项目中添加新的生成内容时,用于筛选是否可用,支持的值:
    AutotoolsProjectManager.AutotoolsProject
    CMakeProjectManager.CMakeProject
    GenericProjectManager.GenericProject
    PythonProject, Qbs.QbsProject
    Qt4ProjectManager.Qt4Project (qmake project)
    QmlProjectManager.QmlProject
  • id - 唯一标识符,多个向导会按照同一分类下的id排序。可作为变量%\{id\}使用
  • category - 向导的分类。可作为变量%\{category\}使用
  • trDescription - 在 New File or Project 对话框中选中对应类型时,在右侧显示的提示信息。可作为变量 %\{trDescription\} 使用
  • trDisplayName - New File or Project 对话框中间显示的名称。可作为变量 %\{trDisplayName\} 使用
  • trDisplayCategory - New File or Project 对话框左侧显示的分类名称。可作为变量 %\{trDisplayCategory\} 使用
  • icon - New File or Project 对话框中间显示的图标,建议使用基于 wizard.json 文件的相对路径,也可以使用绝对路径
  • iconText - 与icon相同,但此处指定的是文字,显示的效果为空白文件图标中存在此处指定的文件
  • image - 显示在 trDescription 下的图像的路径
  • featuresRequired - 指定依赖的Qt Creator功能,如果缺少所需的功能,将被隐藏。使用 enabled 可以表达更丰富的判断逻辑。可作为变量 %\{RequiredFeatures\} 使用
  • featuresPreferred - 指定首选的功能。可作为变量 %\{PreferredFeatures\} 使用
  • platformIndependent - 如果所有目标平台都支持,则设置为true。默认设置为false
  • enabled - 可使用JS表达式判断当前向导是否显示在 New File or Project 对话框中。默认设置为true
  • options - 自定义变量,可在配置文件和模板文件中使用。该区域是一个数组,数组中每个对象定义了一个变量,使用 key 定义变量名, value 定义变量值
  • pages - 定义向导页面。可以使用标准页面,或者使用有效的 widget 定义新的页面
    • typeId - 指定要使用的标准页面。可用值:Fields, File, Form, Kits, Project, VcsConfiguration, VcsCommand, Summary。每个值的具体意义,【页面】部分会详细说明
    • trDisplayName - 页面的标题
    • trShortTitle - 向导侧栏中使用的标题
    • trSubTitle - 页面的副标题
    • index - 指定页面的ID,必须是数字。不指定时自动分配
    • enabled - 页面是否显示。true显示,false隐藏
    • data - 配置向导页面。该区域是一个数组,数组中的每个对象代表了一张页面。当前C++类向导使用了FieldsSummary 两张页面,Fields 页面中使用了 CheckBox, ComboBox, LineEdit, PathChooser, Spacer 组件。每个组件如何使用,【组件】部分会详细说明
  • generators - 生成项目时,添加到项目中的文件
    • typeId - 生成器的类型。目前,只支持 FileScanner
    • data - 详细配置。【生成器】部分会详细说明

页面

配置在 wizard.json 文件的 pages 区域中

Field

字段页。使用指定的组件定义页面,组件的说明详见【组件】部分

{
        "trDisplayName": "Define Class",
        "trShortTitle": "Details",
        "typeId": "Fields",
        "data" :
        [
            {
                "name": "Class",
                "trDisplayName": "Class name:",
                "mandatory": true,
                "type": "LineEdit",
                "data": { "validator": "(?:(?:[a-zA-Z_][a-zA-Z_0-9]*::)+[a-zA-Z_][a-zA-Z_0-9]*|)" }
            },
            ...
        ]
}

File

文件页。可以省略 data 项或设置 data 为空对象

{
    "trDisplayName": "Location",
    "trShortTitle": "Location",
    "typeId": "File"
}

Form

表单页。可以省略 data 项或设置 data 为空对象

{
    "trDisplayName": "Choose a Form Template",
    "trShortTitle": "Form Template",
    "typeId": "Form"
}

Kits

工具页。data 项如下:

  • projectFilePath - 项目文件路径
  • requiredFeatures - 字符串或对象的列表,来描述工具包提供的功能。为对象时,对象的属性如下:
    • feature - 描述信息
    • condition - 返回true或false,用于确定是否要显示描述
  • preferredFeatures - 列出的所有功能匹配的工具包
{
    "trDisplayName": "Kit Selection",
    "trShortTitle": "Kits",
    "typeId": "Kits",
    "enabled": "%{IsTopLevelProject}",
    "data": { "projectFilePath": "%{ProFileName}" }
}

Project

项目页。不包含 data 项或者 data 项是一个对象,对象仅有一个 trDescription 属性,在生成的页面中显示

{
    "trDisplayName": "Project Location",
    "trShortTitle": "Location",
    "typeId": "Project",
    "data": { "trDescription": "A description of the wizard" }
}

Summary

摘要页。不包含 data 项
如果创建的是顶级项目,则设置变量 IsSubproject 为空字符串,否则设置为 yes,并设置 VersionControl 变量为正在使用的版本控制系统的ID

{
    "trDisplayName": "Project Management",
    "trShortTitle": "Summary",
    "typeId": "Summary"
}

VcsCommand

版本控制页。设置版本控制并显示结果。data 项为对象,属性如下:

  • vcsId - 版本控制系统的ID
  • trRunMessage - 版本控制系统运行时显示的消息
  • extraArguments - 定义传递给版本控制检出命令的附加参数的字符串或字符串列表
  • repository - 版本控制远程仓库URL
  • baseDirectory - 运行检出操作的目录
  • checkoutName - 保存检出数据的子目录
  • extraJobs - 定义初次检出后要运行的其他命令的对象列表。对象属性如下:
    • skipIfEmpty - 排除命令中的空值参数。默认为true
    • directory - 命令运行时的工作目录。默认为 baseDirectory 中定义的目录
    • command - 运行的命令
    • arguments - 传递给命令的参数
    • timeOutFactor - 用于长时间运行的命令,设置比默认超时更长的时间
    • enabled - 设置是否支持命令

VcsConfiguration

版本控制配置页。提供用户配置版本控制系统。data 项为对象,属性如下:

  • vcsId - 版本控制系统的ID。用于配置 VcsCommand 中指定的版本控制系统,与 VcsCommand 中的 vcsId 对应

组件

Field 页面中可使用的组件,包含:

  • Check Box
  • Combo Box
  • Label
  • Line Edit
  • Path Chooser
  • Spacer
  • Text Edit

通用的组件的配置如下:

  • name - 组件名
  • trDisplayName - 在标签中显示的文本(span 不为 true 时)
  • type - 组件的类型:CheckBox, ComboBox, Label, LineEdit, PathChooser, Spacer, TextEdit
  • trToolTip - 设置鼠标移动到组件上时的提示
  • isComplete - 提供JS表达式返回true或false,只有所有字段的 isComplete 为 true 时,下一步 按钮才可用
  • trIncompleteMessage - 当 isComplete 为false时显示
  • data - 组件的配置,该区域是一个对象,属性如下:
    • visible - 设置是否可见。true可见,false隐藏
    • enabled - 设置是否可用。true可用,false不可用
    • mandatory - 设置为true时,组件的值不为空则 下一步 按钮变为可用。默认为true
    • span - 设置是否隐藏标签。默认为false

以下为各个组件和组件特有的配置

CheckBox

多选框

  • checkedValue - 当 checkbox 选中时的值
  • uncheckedValue - 当 checkbox 未选中时的值
  • checked - 使 checkbox 可用时,设置为true时,否则设置false
{
    "name": "IncludeQObject",
    "trDisplayName": "Include QObject",
    "type": "CheckBox",
    "data":
    {
        "checkedValue": "QObject",
        "uncheckedValue": "",
        "checked": "%{JS: value('BaseCB') === 'QObject' ? 'true' : 'false'}"
    }
}

ComboBox

下拉列表

  • items - 数组,列表中的可选项。选项可以是字符串或者对象,对象属性如下:
    • trKey - 显示给用户开到的项
    • value - 选中时的实际值
  • index - 设置组件可用时的顺序。默认为0
  • disabledIndex - 设置组件不可用时的顺序
{
    "name": "BaseCB",
    "trDisplayName": "Base class:",
    "type": "ComboBox",
    "data":
    {
        "items": [ { "trKey": "<Custom>", "value": "" },
                   "QObject", "QWidget", "QMainWindow", "QDeclarativeItem", "QQuickItem" ]
    }
}

IconList

带有图标的下拉列表

  • items - 数组,列表中的可选项。选项可以是字符串或者对象,对象属性如下:
    • trKey - 显示给用户开到的项
    • value - 选中时的实际值
    • icon - 可选项上的图标
    • trToolTip - 提示信息
  • index - 设置组件可用时的顺序。默认为0
  • disabledIndex - 设置组件不可用时的顺序
{
    "name": "ChosenBuildSystem",
    "trDisplayName": "Choose your build system:",
    "type": "IconList",
    "data":
    {
        "items": [
            { "trKey": "Qbs", "value": "qbs", "icon": "qbs_icon.png", "trToolTip": "Building with Qbs." },
            { "trKey": "QMake", "value": "qmake", "icon": "qmake_icon.png", "trToolTip": "Building with QMake." }
        ]
    }
}

Label

标签。直接显示文本

  • wordWrap - 是否包裹住文本,使该组件占用的高度与其他组件保持一致
  • trText - 显示的文本
{
    "name": "LabelQQC_2_0",
    "type": "Label",
    "span": true,
    "visible": "%{JS: value('CS') === 'QQC_2_0'}",
    "data":
    {
        "wordWrap": true,
        "trText": "Creates a deployable Qt Quick 2 application using Qt Quick Controls.",
    }
}

LineEdit

单行文本编辑框。占用一行,包含了标签和输入框

  • trText - 标签处的文本
  • trDisabledText - 组件不可用时显示在标签处的文本
  • trPlaceholder - 设置提示的文本
  • validator - 使用 QRegularExpression 支持的正则表达式校验输入
  • fixup - 设置修正输入的变量。例如,可以将输入的首字母大写
  • isPassword - 是否是密码输入框
{
    "name": "Class",
    "trDisplayName": "Class name:",
    "mandatory": true,
    "type": "LineEdit",
    "data": { "validator": "(?:(?:[a-zA-Z_][a-zA-Z_0-9]*::)+[a-zA-Z_][a-zA-Z_0-9]*|)" }
}
{
    "name": "BaseEdit",
    "type": "LineEdit",
    "enabled": "%{JS: value('BaseCB') === '' ? 'true' : 'false'}",
    "mandatory": false,
    "data":
    {
        "trText": "%{BaseCB}",
        "trDisabledText": "%{BaseCB}"
    }
}

PathChooser

路径选择器

  • path - 指定选定的路径。组件初始时的值
  • basePath - 指定路径选择的根目录
  • kind - 定义选择的内容:existingDirectory, directory, file, saveFile, existingCommand, command, any
{
    "name": "Path",
    "type": "PathChooser",
    "trDisplayName": "Path:",
    "mandatory": true,
    "data":
    {
        "kind": "existingDirectory",
        "basePath": "%{InitialPath}",
        "path": "%{InitialPath}"
    }
}

Spacer

空行间隔

  • factor - 设置间隔距离
{
    "name": "Sp1",
    "type": "Spacer",
    "data":
    {
        "factor": 2
    }
}

TextEdit

多行文件编辑框

  • trText - 文本框中的默认文本
  • trDisabledText - 文本框不可用时的文本
  • richText - 设置是否支持富文本
{
    "name": "TextField",
    "type": "TextEdit",
    "data" :
    {
        "trText": "This is some text",
        "richText": true
    }
}

生成器

File

使用定义好的模板文件生成项目文件。提供 data 区域,配置每一个模板文件。
data 是一个对象,属性如下:

  • source - 指定相对于 wizard.json 的模板文件路径
  • target - 指定生成的文件位置。可使用绝对路径或相对 %{TargetPath} 的路径。通常由向导页面提供
  • openInEditor - 设置为true,则在对应的编辑器中打开。默认为false
  • openAsProject - 设置为true,则在 Qt Creator 中打开项目文件。默认为false
  • isBinary - 将文件视为二进制文件,避免生成时替换文件中的内容。默认为false
  • condition - 判断当此处返回true时生成文件。默认为true

Scanner

扫描 %\{TargetPath\} 路径,生成所有此路径中找到的文件。提供 data 区域,配置筛选条件。
data 是一个对象,属性如下:

  • binaryPattern - 设置匹配二进制文件名的正则表达式。任何匹配的文件都将标记为二进制文件,将跳过此文件的模板变量替换
  • subdirectoryPatterns - 设置匹配子目录的正则表达式列表,类型为数组。任何匹配的目录中的文件都将被扫描。默认为空列表,表示不会扫描子目录
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,884评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,347评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,435评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,509评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,611评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,837评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,987评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,730评论 0 267
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,194评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,525评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,664评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,334评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,944评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,764评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,997评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,389评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,554评论 2 349

推荐阅读更多精彩内容