您当前的位置:首页 >  关注  > 正文
【VS Code+Qt6】拖放操作
来源:博客园     时间:2023-05-28 18:19:33

由于老周的示例代码都是用 VS Code + CMake + Qt 写的,为了不误导人,在标题中还是加上“VS Code”好一些。

上次咱们研究了剪贴板的基本用法,也了解了叫 QMimeData 的重要类。为啥要强调这个类?因为接下来扯到的拖放操作也是和它有关系。哦,对了,咱们先避开一下主题,关于剪贴板,咱们还要说一点:就是如何监听剪贴板内数据的变化并做出响应。这个嘛,就有点像迅雷监听剪贴板的功能,发现你复制的东西里包含有下载地址的话,就自动弹出新下载任务窗口。

QClipboard 类有好几个满足此功能的信号,说这个前咱们要先知道一下 QClipboard 类包含一个 Mode 枚举。这个枚举定义了三个成员:


(资料图片)

QClipboard::Clipboard:数据存储在全局剪贴板中。此模式是各系统通用的,尤其是 Windows。

QClipboard::Selection:通过鼠标选取数据。X 窗口系统是 C/S 架构,数据选择后会发送到目标窗口,可用鼠标中键粘贴。

QClipboard::FindBuffer:macOS 专用的粘贴方式。

所以,我们写代码时一般不刻意指定某个 Mode,以保证好的兼容性。现在,咱们回头再看看 QClipboard 类的几个信号。

selectionChanged:当全局鼠标选取的数据改变时发出,这个用在 Linux/X11 窗口系统上。

findBufferChanged:一样道理,只在 macOS 上能用到。

dataChanged:这个比较推荐,不考虑 Mode,只要剪贴板上的数据有变化就会发出,通用性好。

changed:这个最灵活,在发出信号时,会带上一个 Mode 参数,你在代码中处理时可以对 Mode 进行分析。

综上所述,要是只关心剪贴板上的数据变化,连接 dataChanged 信号最合适。下面来个例子。

CMakeLists.txt:

cmake_minimum_required(VERSION 3.0.0)set(CMAKE_AUTOMOC ON)project(myapp VERSION 0.1.0)find_package(Qt6 COMPONENTS Core Gui Widgets)# 头文件与源码文件都在当前目录下,“.”是当前目录include_directories(.)set(SRC_LIST CustWindow.cpp main.cpp)add_executable(myapp ${SRC_LIST})target_link_libraries(myapp PRIVATE    Qt6::Core    Qt6::Gui    Qt6::Widgets)

CustWindow.h:

#ifndef CUST_H#define CUST_H#include #include #include #include #include class MyWindow : public QListWidget{    Q_OBJECTpublic:    MyWindow(QWidget* parent=nullptr);private:    void onDataChanged();};#endif

onDataChanged 是私有成员,待会儿用来连接 QClipboard::dataChanged 信号。这个例子中,老周选用的基类是 QListWidget,它是 QListView 的子类,但用起来比 QListView 方便,不需要手动设置 View / Model,直接可以 addItem,很省事。此处老周是想当剪贴板上放入新的文本数据时在 QListWidget 上添加一个子项。

下面是实现代码:

#include "CustWindow.h"MyWindow::MyWindow(QWidget *parent)    : QListWidget(parent){    // 获取剪贴板引用    QClipboard* clb = QGuiApplication::clipboard();    // 连接信号    connect(clb, &QClipboard::changed, this, &MyWindow::onDataChanged);}void MyWindow::onDataChanged(){    QClipboard* clipbd = QApplication::clipboard();    QString s = clipbd ->text();    // 如果剪贴板中包含文本,那么字符串不为空    if(!s.isEmpty())    {        // 显示文本        this->addItem("你复制了:" + s);    }}

代码并不复杂,重要事情有二:第一,连接 QClipboard::dataChanged 信号,与 onDataChanged 方法绑定。第二,在 onDataChanged 方法内,读取剪贴板上的文本数据,组成新的字符串,调用 addItem 方法,把字符串添加到 QListWidget (基类)对象中。

main 函数的代码就那样了,先创建应用程序对象,然后初始化、显示窗口,再进入事件循环。都是老套路了。

#include "CustWindow.h"#include int main(int argc, char** argv) {    QApplication app(argc, argv);    MyWindow win;    win.setWindowTitle("监视粘贴板");    win.resize(350, 320);    win.show();    return app.exec();}

顺便说一下,exec 其实是静态成员,但调用时用变量名或类然都可以。变量名就用成员运算符“.”,类名就用成员运算符“::”。

运行程序后,随便找个地方复制一些文本,然后回到程序窗口,你会有惊喜的。

上图表明,程序已经能监听剪贴板的数据变化了。

------------------------------------------------------------- 量子分隔线 ------------------------------------------------------------

好了,下面开始咱们的主题——拖放。这两个动词言简意赅,包含了两个行为:

a、拖(Drag):数据发送者,发起数据共享操作。此行为一般是鼠标(或笔,或手指,或其他)在某个对象上按下并移动特定距离后触发。

b、放(Drop):把拖动的数据放置到目标对象上,数据接收者提取到数据内容,并结束整个共享操作。一般是松开鼠标按键(或笔,或手指,或其他)时结束拖放操作。

由于拖放操作是由鼠标等指针设备引发的,为了减少误操作,通常会附加两个约束条件:

1、鼠标按下后一段时间,这个时间可以很短。可通过 QApplication::setStartDragTime 方法设置你喜欢的值,单位是毫秒。默认 500 ms。

2、鼠标按下后必须移动一定的距离。这个距离可以从 QApplication::startDragDistance 方法获取,也可以通过 setStartDragDistance 方法修改。这距离指的是“曼哈顿”距离,这个距离是两个点在与X轴和Y轴平行的距离之和,就是正东、正西、正南、正北的方向。总之不是直线距离,这是为了避开大量浮点、开平方等复杂运算,提升速度。具体可以查资料。不懂这个也不影响编程,Qt 的 QPoint 类自带manhattanLength 方法,可以获得两点相减后的曼哈顿距离。

----------------------------------------------------------------------------------------------------

QDrag类

这个类是拖放操作的核心,因为它的 exec 方法会启动一个拖放操作。拖放操作与剪贴板类似,也是使用 QMimeData 类来封送数据的。在调用 QDrag::exec 之前要用 setMimeData 方法设置要传递的数据。

exec 方法返回时,拖放操作已结束。其返回值是 Qt::DropAction 枚举,拖放操作完成时所返回的值可由数据接收者设置。

DropAction枚举

该枚举定义下面几个值:

1、CopyAction:表示拖放操作将复制数据;

2、MoveAction:表示拖放操作会移动数据;

3、LinkAction:仅仅建立从数据源到数据目标的链接;

4、IgnoreAction:无操作(忽略)。

其实,这些 Action 是反馈给用户看的,在数据传递的过程中毫无干扰。也就是说,不管是 Copy 还是 Move,只不过是一种“语义”,具体怎么处理数据,还是 coder 说了算。

DropAction 的不同取值会改变鼠标指针的图标,所以说这些值是给用户看的。详细可粗略看看下面表格,不需要深挖。

复制
移动
链接

“复制”是箭头右下角显示加号(+),“移动”是显示向右的箭头,“链接”是一个“右转”大箭头。如果忽略或禁止拖放,就是大家熟悉的一个圈圈里面一条斜线——。

在调用 QDrag::exec 方法时你可以指定 DropAction 值,通常有两个参数要赋值:

Qt::DropAction exec(Qt::DropActions supportedActions = Qt::MoveAction);Qt::DropAction exec(Qt::DropActions supportedActions, Qt::DropAction defaultAction);
supportedActions 参数表示你规定数据接收者只能使用的 DropAction 值。比如,你在发起拖放时指定 supportedActions = CopyAction | MoveAction,那么,数据接收者在读取数据时,你只能向用户反馈“复制”或“移动”图标,你不能用 LinkAction。而 defaultAction 参数是给数据接收者建议的操作,只能在 supportedActions 的值里面选。比如,supportedAction = Move | Copy | Link,那么,defaultAction = Copy 可以,defaultAction = Move 也可以。拖放操作启动的条件拖放操作是与鼠标有关的,一般会在处理 mousePress、mouseMove 事件时触发。数据接收者接收数据是在 Drop 操作时发生——即东西已顺利拖到目标上,并且放开鼠标按键。数据接收者将通过处理下面几个神秘事件来提取数据的:1、dragEnter:鼠标把某个东西拖进当前对象的边界时发生。假设当前对象是一个窗口,那么,当拖动进入窗口的边沿时就会触发该事件;2、dragMove:东西被拖进来了,过了边界,但鼠标仍在移动。此时会不断触发 dragMove 事件。这事件是连续发生的,除非你鼠标不动。要是一时手抖放开了鼠标左键,那就触发了 drop 事件,拖放操作结束;3、dragLeave:东西拖进来后,没有释放,继续拖,最终离开当前对象的边界——拖出去了(斩了),就会触发 dragLeave 事件;4、drop:释放鼠标,标志拖放操作结束。此时你得读出别人传给你的数据了。dragLeave 事件一般很少处理,干得比较多的是 dragEnter 和 drop。当 dragEnter 发生时,通常要分析一下,拖过来的数据是不是我想要的。别人扔给你的有可能是炸弹,所以要判断一下,不接受的数据直接 ignore(忽略)。如果数据是你想要的,就 accept 它,然后在释放时会发生 drop 事件;在 drop 事件中把你要的数据读出来就完事了。当然了,QMimeData 中的数据你不见得要全读出来,你只取你所要的部分。如果在 dragEnter 事件中拒绝数据,那么释放时是不会发生 drop 事件的。为了让大伙伴们更好地理解,drag 和 drop 两个过程咱们分开说。接下来,我们实现把文本数据从当前窗口拖到其他程序(如记事本)。下面是 CMake 文件。
cmake_minimum_required(VERSION 3.20)project(DragDemo LANGUAGES CXX)set(CMAKE_AUTOMOC ON)find_package(    Qt6    COMPONENTS        Core Gui Widgets    REQUIRED)# 找到项目下所有头文件和源文件file(GLOB_RECURSE SRC_LIST include/*.h src/*.cpp)include_directories(include)add_executable(DragDemo WIN32 ${SRC_LIST})target_link_libraries(    DragDemo    PRIVATE    Qt6::Core    Qt6::Gui    Qt6::Widgets)

代码插件没有 CMake 的,老周用的是 C++ 的插入,因为里面出现了 /*,被识别成了注释,所以上面内容后半部分全绿了。

项目结构是这样的:

下面是头文件。

#pragma once#include #include #include class Demo : public QWidget{    Q_OBJECTpublic:    Demo(QWidget* parent=nullptr);protected:    void paintEvent(QPaintEvent* event) override;    void mousePressEvent(QMouseEvent* event) override;    void mouseMoveEvent(QMouseEvent* event) override;private:    // 这个私有变量用来临时存储鼠标按下的坐标    QPoint m_curpt;};

这个类没什么特别的,就是一个自定义窗口。其中,重写 paintEvent 方法,在窗口上画提示文字。这个只为了好看,你可以省略。

重点是重写 mousePress 和 mouseMove 两个事件,mousePress 时记下鼠标按下的坐标,然后在 mouseMove 中再次获取鼠标的坐标,和按下时的坐标相减,看看它们的曼哈顿距离是否符合启动拖放的条件。

咱们来看实现代码。

Demo::Demo(QWidget *parent){    this->setWindowTitle("拖动示例");    this->resize(258, 240);    this->move(659, 520);}void Demo::paintEvent(QPaintEvent *event){    QRect rect=event->rect();    // 在窗口上绘制文本    QFont font;    font.setFamily("华文仿宋");     //字体名称    font.setPointSize(24);          //字体大小(点)    font.setBold(true);             //加粗    QPainter painter(this);    // 设置字体    painter.setFont(font);    // 计算一下文本所占空间    QString textToDraw = "从此窗口拖动";    QRect textRect = painter.fontMetrics().boundingRect(rect, Qt::AlignCenter, textToDraw);    // 移动文本矩形,让它的中心点和窗口矩形的中心点对齐    textRect.moveCenter(rect.center());    // 设置绘制文本的画笔    QPen pen;    pen.setColor(QColor("red"));    painter.setPen(pen);    // 开始涂鸦    painter.drawText(textRect.toRectF(), textToDraw);    painter.end();}void Demo::mousePressEvent(QMouseEvent *event){    // 获取鼠标按下的坐标点    m_curpt = event->pos();}void Demo::mouseMoveEvent(QMouseEvent *event){    // 获取鼠标现在的位置坐标    QPoint curloc = event->pos();    // 和刚才按下去的坐标比较    if((m_curpt - curloc).manhattanLength() < QApplication::startDragDistance())    {        // 距离不够,不启动拖放        return;    }    // 准备拖放    QString str = "石灰水化死尸可作化肥";  //要传送的数据    QMimeData* mdata = new QMimeData;    // 打包    mdata -> setText(str);    // 发快递    // QDrag(QObject *dragSource)    // dragSource 指的是发起拖放操作的对象    // 这里是当前窗口    QDrag drag(this);    // 设置数据    drag.setMimeData(mdata);    // 出发    auto result = drag.exec(Qt::CopyAction | Qt::LinkAction, Qt::CopyAction);    QString displaymsg = "数据传递完毕,操作结果:";    if(result & Qt::CopyAction)    {        displaymsg += "复制";    }    else if(result & Qt::LinkAction)    {        displaymsg += "链接";    }    else if(result & Qt::IgnoreAction)    {        displaymsg += "忽略";    }    else    {        displaymsg += "未知";    }    QMessageBox::information(this, "提示", displaymsg, QMessageBox::Ok);}

paintEvent 的重写不是重点,不过老周简单说下。

a、创建 QFont 实例,你看名字都知道是什么鬼了,是的,设置字体参数;

b、计算文本”从此窗口拖动“要占多少空间,核心是调用QFontMetrics 类的boundingRect 方法。这里要注意,调用的是这个重载:

QRect QFontMetrics::boundingRect(const QRect &r, int flags, const QString &text, int tabstops = 0, int *tabarray = (int *)nullptr) const

也就是说,不能调用只传文本的重载,那个重载计算出来的 rect 宽度会变小,导致绘制出来的字符串少了一个字符(原因不明)。但,调用上面这个有N多参数的重载是没问题。区别就在于给也一个 r 参数,这个参数提供一个矩形区域作为约束。这里老周用整个窗口的空间作为约束。可能是给的空间足够大,所以计算出来的宽度就足够。于是老周厚着脸皮翻了一下 Qt 的源码,这两重载所使用的处理方法不一样,参数比较多的那个里面调用的是qt_format_text 函数,参数较少的那个里面用的是QStackTextEngine 类。有兴趣的伙伴可以去翻翻。

moveCenter 是使矩形平移,并且中心点对准窗口矩形区域的中心点。这里可以让绘制的文本处在窗口的中央。

接下来说说 mousePress 事件,这里就很简单了,就是直接记录鼠标的位置。不过,有点不严谨,拖放操作没听说过用鼠标右键操作的吧?所以,此处最好判断一下,是不是左键按下。

void Demo::mousePressEvent(QMouseEvent *event){    if(!(event -> buttons() & Qt::LeftButton))    {        return;    }    // 获取鼠标按下的坐标点    m_curpt = event->pos();}

mouseMove 事件也是如此。

void Demo::mouseMoveEvent(QMouseEvent *event){    if(!(event -> buttons() & Qt::LeftButton))    {        return;    }    ……}

QDrag::exec 方法是在 mouseMove 事件中启动的,这个就和剪贴板的操作相似了。先创建 QMimeData,设置文本数据,然后创建 QDrag 实例,设置 MimeData,然后就调用 exec 方法。

最后是整个程序的 main 函数。

int main(int argc, char* argv[]){    QApplication app(argc, argv);    Demo window;    window.show();    return QApplication::exec();}

运行示例后,打开一个文本编辑器(如记事本),在窗口上按下鼠标左键,拖到文本编辑器,文本就发送到目标窗口了。

然后,咱们来看 drop 操作。

要想让某个组件支持放置行为,你必须调用:

setAcceptDrops(true);

默认是不开启的,所以必须调用一次 setAcceptDrops 方法。

当某个组件(可以是窗口,按钮,标签,文本框等)支持放置行为后,把数据拖到该组件上会引发 dragEnter、dragMove 等事件;释放鼠标时会发生 drop 事件,表示整个拖放操作结束。这个上文已讲过,下面重点看几个事件参数。注意了,这几个厮实际上是有继承关系的。

class QDropEvent : public QEventclass QDragMoveEvent : public QDropEventclass QDragEnterEvent : public QDragMoveEvent// 下面这个是特例class QDragLeaveEvent : public QEvent

QDragLeaveEvent 是直接派生自 QEvent 的,因为它是在 dragLeave 事件发生时使用,数据被拖出当前对象,一般不需要额外携带什么参数,所以这个事件类比较特殊。

QDropEvent 类用于 drop 事件,因为这时候你得读取数据了,所以它会夹带私货。这些私货分两类:

1、跟鼠标有关的。比如 buttons 返回鼠标按下了哪个键;modifiers 返回值表示用户是否在拖动的同时按下 Ctrl、Alt、Shift 等按键。position 返回鼠标指针的当前坐标。这些参数咱们通常用不上的。

2、和共享的数据相关的。这个是最需要的。mimeData 返回 QMimeData 对象的指针,然后咱们就能读数据了。source 返回发起拖放操作的对象,一般我们的程序不太关注数据源。

不管读不读取数据,作为数据接收者,我们是文明的,有礼貌的。拖放操作完成时咱们应该响应一下发送者—— QDrag::exec 方法(如果数据是从其他程序拖过来的,那么,拖放的发起者就不一定是调用 exec 方法,毕竟人家不见得是用 Qt 写的,说不定是用 WPF 做的)。

扯远了,回到主题,向数据发送者反馈,还是涉及到了 DropActions 的事。DropEvent 提供了这些成员,可以访问 action。

1、possibleActions 方法,对应的是 exec 方法的 supportedActions 参数;

2、proposedAction 方法,对应 exec 方法的 defaultAction 参数。

还记得前文说过的 exec 方法的两个参数吗?嗯,是滴,possibleActions 就是 supportedActions 参数提供的有效范围,你只能在这些值中选一个。proposedAction 是建议的值,也就是 defaultAction 参数提供的默认值。

所以,如果我们的程序比较在意使用什么 action 的话,你得好好分析一下这两个方法返回的值了。不过,多数时候,我们只关心 mimeData 返回的内容,因为那是要提取的数据。

如果你成功接收了数据,那么要调用acceptProposedAction 方法,表示数据和 defaultAction 你都接受了。

如果你不想用 defaultAction 参数推荐的默认 action,那么,你可以调用 QDropEvent::setDropAction 方法自己设置一个 action,但你设置的 action 必须在 possibleActions 中允许的。如果你调用了 setDropAction 方法,就等于修改了默认 action,所以这时候你只能调用 accept 方法来接受,不能再调用 acceptProposedAction 方法了。不然,acceptProposedAction 方法会还原默认 action 的值。

如果你发现数据不是你想要的,或者数据发送者给的 DropAction 你不接受,那你就调用 ignore 方法忽略,或者你什么都不做也可以(默认会 ignore 掉事件)。

QDragEnterEvent 和 QDragMoveEvent 都是 QDropEvent 的子类,所以成员都是差不多的。就不用老周再废话了。

了解这几个类的关系,你就知道怎么处理接收拖动的过程了。下面我们来个例子,把图片文件拖到咱们的程序,然后会显示该图片。就是拖动打开文件了。

从 QLabel 类派生出一个类,咱们就用它来接收并显示图片。Qt 没有专门显示图片的组件,一般用 QLabel 来显示图片。当然,QPushButton 等按钮组件也可以显示图片,不过通常用作显示小图标。有大伙伴会说,QGraphicsView 什么什么的不用吗?那个就太大动作了,简直是杀小强用牛刀,没有必须,我就想显示个图片而已。

#pragma once#include #include #include #include class MyLabel : public QLabel{    Q_OBJECTpublic:    MyLabel(QWidget* parent=nullptr);protected:    void dragEnterEvent(QDragEnterEvent* event) override;    void dropEvent(QDropEvent* event) override;    void resizeEvent(QResizeEvent* event) override;private:    // 用来缓存图像    QPixmap m_image;};

事件不算多,就重写三个事件。另外,还声明了一个私有成员 m_image 用来存图像资源。你可能会问了,QLabel 不是可以设置和获取 QPixmap 对象吗,为什么要特地用一个私有成员来保存?因为 QLabel 上显示的图像,咱们一般会缩小一下再显示。经过缩小后的 QPixmap 对象,再重新放大就变得很模糊了。所以,QLabel::Pixmap 不保存原图。

在构造函数中,让这个标签组件支持放置。

MyLabel::MyLabel(QWidget *parent)    : QLabel(parent){    this->setAcceptDrops(true);    this->setStyleSheet("background-color: gray");}

setAcceptDrops 开启 drop 支持。还有一个是 setStyleSheet,这里老周是用 QSS 来设置标签的背景颜色为难看的灰色。这是 Qt 搞的装X玩意儿,用起来有点像 HTML 中的 CSS。

又有伙伴问了,QLabel 不是有个带 text 参数的构造函数吗?对,不过这里不需要,咱们这个自定义组件不显示文本。

然后,实现resizeEvent,当大小改变时,咱们也调整一下标签上的图像大小(其实是重新加载缩放过的图像)。

void MyLabel::resizeEvent(QResizeEvent *event){    if(!m_image.isNull())    {        // 获取当前新调整的大小        QSize labelsize = event->size();        // 缩放图像        auto pixmap = m_image.scaled(labelsize, Qt::KeepAspectRatio, Qt::SmoothTransformation);        // 重新设置图像        this->setPixmap(pixmap);    }}

最后就是跟drop 有关的两个事件了。

void MyLabel::dragEnterEvent(QDragEnterEvent *event){    // 检查一下是不是所需要的数据    const QMimeData *data = event->mimeData();    if (data->hasUrls())    {        event->acceptProposedAction();    }}void MyLabel::dropEvent(QDropEvent *event){    // 再次验证一下数据    const QMimeData *data = event->mimeData();    if (data->hasUrls())    {        // 读数据        QList paths = data->urls();        if (paths.size() > 0)        {            QUrl p = paths.at(0);            QString locfile = p.toLocalFile();            m_image.load(locfile);        }        // 缩放一下        auto pix = m_image.scaled(this->size(), Qt::KeepAspectRatio, Qt::SmoothTransformation);        this->setPixmap(pix);        event->acceptProposedAction();    }}

dragEnter 的时候,只是看看有没有想要的数据,不读。读取是在 drop 事件中完成。但是为了防止概率 0.001% 的灵异事件发生,在 drop 事件处理时还要再检验一下数据是不是有效。

文件拖进来,一般是 URL 类型,获取到的对象是 QUrl 类型,它的格式是 file:///xxxxx,这个路径在 load 方法中加载不了,于是得用 toLocalFile 方法,将 URL 转换为本地文件路径,这样就能在 QPixmap::load 方法中加载图像了。

下面,定义一个窗口,实例化两个 MyLabel 组件,放在网格布局中第一行的两个单元格内。

/* 头文件 */#pragma once#include #include "custlabel.h"#include class MyWindow : public QWidget{    Q_OBJECTpublic:    MyWindow(QWidget* parent=nullptr);private:    MyLabel *lbImg1, *lbImg2;    QLabel *lb1, *lb2;    QGridLayout *layout;};

两个 QLabel 组件用来显示普通文本,咱们自己弄的 MyLabel 组件用来显示图片。QGridLayout 是布局用的,以网格形式布局(行、列)。

MyWindow::MyWindow(QWidget *parent)    :QWidget(parent){    setWindowTitle("放置图像");    resize(450, 400);    // 初始化    lb1 = new QLabel("美琪", this);    lb2 = new QLabel("美雪", this);    lb1->setSizePolicy(QSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed));    lb2->setSizePolicy(QSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed));    lbImg1 = new MyLabel(this);    lbImg2 = new MyLabel(this);    layout = new QGridLayout(this);    // 布局    layout->addWidget(lbImg1, 0, 0);    layout->addWidget(lbImg2, 0, 1);    layout->addWidget(lb1, 1, 0);    layout->addWidget(lb2, 1, 1);}

setSizePolicy 那两行是为了让 QLabel 组件的高度固定,因为 QGridLayout 这个王八不能设置固定的行高和列宽,所以只能出此下策了。

写上 main 函数。

int main(int argc, char* argv[]){    QApplication app(argc, argv);    MyWindow win;    win.show();    return QApplication::exec();}

运行程序后,就可以把图片文件拖到两个 MyLabel 上了。注意左边是美琪,右边是美雪,下面的标签是她俩的名字。

标签:

相关新闻

X 关闭

X 关闭

精彩推荐