IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> C++知识库 -> Qt Script 模块 for Qt4 -> 正文阅读

[C++知识库]Qt Script 模块 for Qt4

Qt Script

Qt支持使用ECMAScript编写应用程序脚本。在Qt中,Qt提供了Qt Script模块来支持脚本开发。此模块已不再继续开发,Qt5中,提供了QJSEngine以及相关类来替代此模块。
要使用此模块需要在pro文件中添加:

QT += script

基本用法

要计算脚本代码,请创建QScriptEngine并调用其evaluate()函数,将要计算的脚本代码(文本)作为参数传递。

  QScriptEngine engine;
  qDebug() << "the magic number is:" << engine.evaluate("1 + 2").toNumber();

engine.evaluate()的返回值将表示为QScriptValue;这可以转换成标准的C++和QT类型。

通过向QScriptEngine 注册自定义属性,脚本可以使用自定义属性。其中globalObject 是 QScriptEngine 的全局对象。

 engine.globalObject().setProperty("foo", 123);
  qDebug() << "foo times two is:" << engine.evaluate("foo * 2").toNumber();

上述会将属性foo放置在脚本环境中,从而使它们可用于脚本代码。

在脚本引擎中使用QObject类

任何基于QObject的实例都可以与脚本一起使用。
将QObject传递给QScript Engine::newQObject()函数时,将创建一个QScriptValue,该对象可用于使QObject的信号、插槽、属性和子对象可供脚本使用。
下面是一个让QObject子类的实例可用于名为“myObject”的脚本代码的示例:

  QScriptEngine engine;
  QObject *someObject = new MyObject;
  QScriptValue objectValue = engine.newQObject(someObject);
  engine.globalObject().setProperty("myObject", objectValue);

这将在脚本环境中创建一个名为’myObject’全局变量。变量用作底层C++对象的代理。请注意,脚本变量的名称可以是任何名称;即setProperty的第一个参数由你自己选择;不依赖于QObject::objectName()。

engine.globalObject().setProperty("yourself", objectValue);

newQObject()函数接受两个额外的可选参数:一个是所有权模式,另一个是选项集合,允许您控制包装QObject的QScriptValue的某些方面的行为。稍后我们将回到这些参数的用法。
原型为:

QScriptValue QScriptEngine::newQObject(QObject *object, QScriptEngine::ValueOwnership ownership = QtOwnership, const QScriptEngine::QObjectWrapOptions &options = QObjectWrapOptions())

如何使用信号槽

Qt Script模块适配Qt的信号和槽功能用于脚本编写。在Qt脚本中使用信号和插槽有三种主要方法:

  • 混合C++/Script:C++应用程序代码将信号连接到脚本函数。例如,脚本函数可以是用户键入的函数,也可以是从文件中读取的函数。如果您有一个QObject,但不想将对象本身暴露在脚本环境中,这种方法很有用;您只需要一个脚本能够定义信号应该如何反应,并将其保留到应用程序的C++侧来建立连接。
  • 混合Script/C++:在c++ QObject 中提供了信号和槽对象,在脚本定义中来动态连接连接信号和插槽,何时连接,在脚本中动态定义。
  • 纯Script定义:脚本既可以定义信号处理程序函数(实际上是“用Qt脚本编写的插槽”),也可以设置使用这些处理程序的连接。例如,脚本可以定义一个函数来处理QLineEdit::returnPressed()信号,然后将该信号连接到脚本函数。

使用 qScriptConnect() 可以将C++信号连接到脚本函数。在以下示例中,定义了一个脚本信号处理程序,该处理程序将处理QLineEdit::textChanged() 信号:

  QScriptEngine eng;
  QLineEdit *edit = new QLineEdit(...);
  QScriptValue handler = eng.evaluate("(function(text) { print('text was changed to', text); })");
  qScriptConnect(edit, SIGNAL(textChanged(const QString &)), QScriptValue(), handler);

qScriptConnect() 的前两个参数与传递给 QObject::connect() 方法相同,以建立正常的C++连接。第三个参数是脚本对象,当调用信号处理程序时,它将充当this对象;在上面的例子中,我们传递了一个无效的脚本值,所以这个对象将是全局对象。第四个参数是脚本函数(“slot”)本身。以下示例显示了如何使用此参数:

  QLineEdit *edit1 = new QLineEdit(...);
  QLineEdit *edit2 = new QLineEdit(...);

  QScriptValue handler = eng.evaluate("(function() { print('I am', this.name); })");
  QScriptValue obj1 = eng.newObject();
  obj1.setProperty("name", "the walrus");
  QScriptValue obj2 = eng.newObject();
  obj2.setProperty("name", "Sam");

  qScriptConnect(edit1, SIGNAL(returnPressed()), obj1, handler);
  qScriptConnect(edit2, SIGNAL(returnPressed()), obj2, handler);

上面创建了两个QLineEdit对象,并定义了一个信号处理函数。这些连接使用相同的处理程序函数,但根据触发了哪个对象的信号,将使用不同的this对象调用该函数,因此每个对象的print()语句的输出将不同。
在脚本代码中, Qt Script使用不同的语法来连接和断开信号,而不是熟悉的C++语法;i、 例如,QObject::connect()。要连接到信号,请将相关信号作为发送方对象的属性引用,并调用其connect()函数。connect()有三个重载,每个重载对应一个disconnect()重载。以下小节描述了这三种形式。

1. Signal to Function Connections

connect(function)
在这种连接形式中,connect()的参数是连接信号的函数。

function myInterestingScriptFunction() {
      // ...
  }
  // ...
myQObject.somethingChanged.connect(myInterestingScriptFunction);

connect的参数可以是Qt Script function,如上例所示,也可以是QObject的槽函数,如以下示例所示:

myQObject.somethingChanged.connect(myOtherQObject.doSomething);

当参数是QObject槽函数时,信号和槽的参数类型不一定要兼容;如有必要,Qt Script 将执行信号参数的转换,以匹配插槽的参数类型。
要断开与信号的连接,可以调用信号的disconnect()函数,将函数作为参数传递给disconnect:

myQObject.somethingChanged.disconnect(myInterestingFunction);
myQObject.somethingChanged.disconnect(myOtherQObject.doSomething);

当响应信号调用脚本函数时,this对象将是全局对象。

2. Signal to Member Function Connections

语法:connect(thisObject, function)
在这种形式的connect()函数中,第一个参数是在调用使用第二个参数指定的函数时绑定到变量的对象。
如果窗口中有一个按钮,通常需要对该按钮的点击信号进行一些操作;在这种情况下,将窗口作为this对象传递是有意义的。

 var obj = { x: 123 };
 var fun = function() { print(this.x); };
 myQObject.somethingChanged.connect(obj, fun);

要断开与信号的连接,请将相同的参数传递给disconnect():

myQObject.somethingChanged.disconnect(obj, fun);

3. Signal to Named Member Function Connections

语法:connect(thisObject, functionName)

在这种形式的connect()函数中,第一个参数是将绑定到变量的对象,这是在响应信号调用函数时发生的。第二个参数指定连接到信号的函数的名称,这是指作为第一个参数传递的对象的成员函数(在上述方案中为thisObject)。
请注意,该功能是在建立连接时解析的,而不是在发出信号时解析的。

var obj = { x: 123, fun: function() { print(this.x); } };
myQObject.somethingChanged.connect(obj, "fun");

要断开与信号的连接,请将相同的参数传递给disconnect():

  myQObject.somethingChanged.disconnect(obj, "fun");

4. 信号槽连接信号处理

当connect()或disconnect()成功时,函数将返回undefined;否则,它将抛出脚本异常。可以从生成的错误对象获取错误消息。例子:

try {
      myQObject.somethingChanged.connect(myQObject, "slotThatDoesntExist");
  } catch (e) {
      print(e);
  }

5. 在脚本中发射信号

要从脚本代码发出信号,只需调用signal函数,并传递相关参数:

  myQObject.somethingChanged("hello");

目前无法在脚本中定义新信号;也就是说,所有的信号必须由C++类定义。

6. 信号、槽的重载

当信号或槽重载时,Qt脚本将根据函数调用中涉及的QScript值参数的实际类型,尝试选择正确的重载。例如,如果您的类具有插槽myOverloadedSlot(int)和myOverloadedSlot(QString),则以下脚本代码的行为将合理:

 myQObject.myOverloadedSlot(10);   // will call the int overload
 myQObject.myOverloadedSlot("10"); // will call the QString overload

可以使用数组样式属性访问指定特定的重载,其中C++函数的规范化签名作为属性名称:

 myQObject['myOverloadedSlot(int)']("10");   // call int overload; the argument is converted to an int
 myQObject['myOverloadedSlot(QString)'](10); // call QString overload; the argument is converted to a string

如果重载有不同数量的参数,Qt Script 将选择参数计数与传递到槽的实际参数数最匹配的重载。
对于重载信号,如果试图通过名称连接到信号,Qt Script 将抛出错误;您必须引用具有要连接的特定重载的完整规范化签名的信号。

属性访问

QObject的属性可以作为相应Qt脚本对象的属性使用。当您在脚本代码中操作属性时,将自动调用该属性的C++ GET/SET方法。例如,如果C++类具有如下声明的属性:

Q_PROPERTY(bool enabled READ enabled WRITE setEnabled)

脚本代码如下:

  myQObject.enabled = true;

  // ...

  myQObject.enabled = !myQObject.enabled;

访问子对象

QObject的每个命名子对象(即QObject::objectName()不是空字符串的子对象)默认情况下都可以作为Qt Script 对象的属性使用。例如,如果您有一个QDialog,其中有一个子小部件,其objectName属性为“okButton”,则可以通过表达式在脚本代码中访问该对象

myDialog.okButton

由于objectName本身是一个Q_PROPERTY属性,因此可以在脚本代码中使用该名称,例如重命名对象:

myDialog.okButton.objectName = "cancelButton";

还可以使用函数findChild() 和findChildren() 查找子对象。这两个函数的行为分别与QObject::findChild() 和QObject::findChildren() )相同。
例如,我们可以使用这些函数来查找使用字符串和正则表达式的对象:

 var okButton = myDialog.findChild("okButton");
  if (okButton != null) {
     // do something with the OK button
  }

  var buttons = myDialog.findChildren(RegExp("button[0-9]+"));
  for (var i = 0; i < buttons.length; ++i) {
     // do something with buttons[i]
  }

在处理使用嵌套布局的窗口时,通常需要使用findChild() ;这样,脚本就与小部件所在的特定布局的细节隔离开来。

QObject 所属权问题

当脚本对象不再使用时,Qt-Script使用垃圾收集来回收脚本对象内存;当对象不再在脚本环境中的任何位置被引用时,可以自动回收对象的内存。Q tScript允许在包装对象被回收时控制底层C++对象的发生(即是否删除QObjt对象);通过将所有权模式作为第二个参数传递给QScript Engine::newQObject()来创建对象时,可以执行此操作。
了解Qt脚本如何处理所有权是很重要的,因为它可以帮助您避免在它应该是(导致内存泄漏)时删除C++对象的情况,或者当它不应该被删除时C++对象被删除的情况(如果C++代码后来尝试访问该对象,通常会导致崩溃)。

Qt 所属权

默认情况下,脚本引擎不拥有传递给QScriptEngine::newQObject() 的QObject的所有权。例如,当您正在包装C++应用程序的一部分时,该模式是适当的,这是应用程序核心的一部分;也就是说,无论脚本环境中发生了什么,它们都应该保持不变。另一种说明这一点的方法是C++对象应该比脚本引擎生命周期时间长。

Script 所属权

将 QScriptEngine::ScriptOwnership 指定为所有权模式将导致脚本引擎获得QObject的完全所有权,并确定这样做是安全的(即,脚本代码中不再有对它的引用)时将其删除。如果QObject没有父对象,并且/或者QObject是在脚本引擎的上下文中创建的,并且不打算比脚本引擎更长寿,那么这种所有权模式是合适的。
例如,QObject的构造函数只用于脚本环境,是一个很好的选择:

 QScriptValue myQObjectConstructor(QScriptContext *context, QScriptEngine *engine)
  {
    // let the engine manage the new object's lifetime.
    return engine->newQObject(new MyQObject(), QScriptEngine::ScriptOwnership);
  }

自动所有权

对于QScript Engine::AutoOwnership,所有权基于QObject是否有父对象。如果Qt脚本垃圾收集器发现QObject不再在脚本环境中被引用,则只有当QObject没有父对象时,才会将其删除。

当其他人删除QObject时会发生什么?

有可能在Qt脚本的控制之外删除了一个包装好的QObject; 例如,不考虑指定的所有权模式。在这种情况下,包装对象仍然是一个对象(不同于它包装的C++指针,脚本对象不会变成空)。但是,任何访问脚本对象属性的尝试都会导致引发脚本异常。
请注意,对于已删除的QObject,QScriptValue::isQObject() 仍将返回true,因为它测试脚本对象的类型,而不是内部指针是否为非null。换句话说,如果QScriptValue::isQObject()返回true,但QScriptValue::toQObject()返回空指针,则表明QObject已在Qt脚本之外删除(可能是意外删除)。

如何设计和实现Qt 对象,以便可以用于脚本?

在脚本中使用C++类成员函数

元对象系统还使有关信号和插槽的信息在运行时动态可用。默认情况下,对于QObject子类,只有信号和插槽会自动提供给脚本。这是非常方便的,因为在实践中,我们通常只想让脚本编写人员可以使用特殊选择的函数。创建QObject子类时,请确保要向Qt脚本公开的函数是public槽函数
例如,以下类定义仅允许为某些函数编写脚本:

  class MyObject : public QObject
  {
      Q_OBJECT
  public:
      MyObject( ... );
      void aNonScriptableFunction();
  public slots: // 这些槽函数可用于脚本
      void calculate( ... );
      void setEnabled( bool enabled );
      bool isEnabled() const;
  private:
     ....
  };

在上面的示例中,aNonScriptableFunction()未声明为槽,因此它在Qt脚本中不可用。其他三个函数将在Qt脚本中自动可用,因为它们在类定义的public槽函数。

通过在声明函数时指定Q_INVOKABLE修饰符,可以使任何函数脚本可调用:

class MyObject : public QObject
  {
      Q_OBJECT

      public:
      Q_INVOKABLE void thisMethodIsInvokableInQtScript();
      void thisMethodIsNotInvokableInQtScript();
      ...
  };

一旦用Q_INVOKABLE声明,就可以从Qt脚本代码调用该方法,就像它是一个槽函数一样。虽然这样的方法不是slot,但仍然可以在脚本代码中调用connect()时将其指定为目标函数;
如果您的函数采用了Qt脚本无法处理转换的参数,则需要提供转换函数。这是使用qScriptRegisterMetaType()函数完成的。

在脚本中使用c++ 成员属性

在上一个示例中,如果我们想使用Qt Scirpt 获取或设置属性,我们必须编写如下代码:

  var obj = new MyObject;
  obj.setEnabled( true );
  print( "obj is enabled: " + obj.isEnabled() );

脚本语言通常提供属性语法来修改和检索对象的属性(在本例中为启用状态)。许多脚本程序员都希望这样编写上述代码:

  var obj = new MyObject;
  obj.enabled = true;
  print( "obj is enabled: " + obj.enabled );

为了使这成为可能,必须定义C++ QObject 子类中的属性。例如,下面的MyObject类声明声明了一个名为enabled的布尔属性,该属性使用函数setEnabled(bool)作为其setter函数,使用isEnabled()作为其getter函数:

class MyObject : public QObject
  {
      Q_OBJECT
      // define the enabled property
      Q_PROPERTY( bool enabled WRITE setEnabled READ isEnabled )

  public:
      MyObject( ... );

      void aNonScriptableFunction();

  public slots: // available in Qt Script
      void calculate( ... );
      void setEnabled( bool enabled );
      bool isEnabled() const;

  private:
     ....

  };

与原始代码的唯一区别是使用了宏Q_PROPERTY属性,它将属性的类型和名称,以及setter和getter函数的名称作为参数。
如果不希望类的属性在Qt脚本中可访问,则在声明属性时将SCRIPTABLE属性设置为false;默认情况下,SCRIPTABLE属性为true。例如:

Q_PROPERTY(int nonScriptableProperty READ foo WRITE bar SCRIPTABLE false)

在脚本中处理C++信号

在Qt对象模型中,信号被用作QObjects 对象之间的通讯机制。这意味着一个对象可以将一个信号连接到另一个对象的插槽,并且每次发出信号时,该插槽都被调用。此连接是使用QObject::connect()函数建立的。
Qt Script 程序员也可以使用信号和插槽机制。在C++中声明信号的代码是相同的,不管信号是否连接到C++中的槽或QT脚本中。

class MyObject : public QObject
  {
      Q_OBJECT
      // define the enabled property
      Q_PROPERTY( bool enabled WRITE setEnabled READ isEnabled )

  public:
      MyObject( ... );

      void aNonScriptableFunction();

  public slots: // these functions (slots) will be available in Qt Script
      void calculate( ... );
      void setEnabled( bool enabled );
      bool isEnabled() const;

  signals: // the signals
      void enabledChanged( bool newState );

  private:
     ....

  };

我们对上一节中的代码所做的唯一更改是使用相关信号声明信号部分。现在,脚本编写器可以定义一个函数并连接到对象,如下所示:

function enabledChangedHandler( b )
  {
      print( "state changed to: " + b );
  }

  function init()
  {
      var obj = new MyObject();
      // connect a script function to the signal
      obj["enabledChanged(bool)"].connect(enabledChangedHandler);
      obj.enabled = true;
      print( "obj is enabled: " + obj.enabled );
  }

在脚本中获得Qt 对象的指针

如果有一个槽返回QObject指针,那么应该注意,默认情况下,Qt脚本只处理QObject和QWidget类型的转换。这意味着,如果用“MyObjectgetMyObject()”这样的签名声明插槽,Qt脚本不会自动知道MyObject应以与QObject和QWidget相同的方式处理。解决这个问题的最简单方法是在脚本接口的方法签名中只使用QObject和QWidget
或者,可以使用QScript RegisterMetaType函数为自定义类型注册转换函数。这样,您就可以保留C++声明中的精确类型,同时仍然允许指针到自定义对象在C++和脚本之间切换。例子:

  class MyObject : public QObject
  {
      Q_OBJECT
      ...
  };

  Q_DECLARE_METATYPE(MyObject*)

  QScriptValue myObjectToScriptValue(QScriptEngine *engine, MyObject* const &in)
  { return engine->newQObject(in); }

  void myObjectFromScriptValue(const QScriptValue &object, MyObject* &out)
  { out = qobject_cast<MyObject*>(object.toQObject()); }

  ...

  qScriptRegisterMetaType(&engine, myObjectToScriptValue, myObjectFromScriptValue);

在C++中如何调用脚本函数

从C++中调用Qt脚本函数是用QScriptValue::call(),典型的情况是,以下脚本定义了一个具有toKelvin()function 的Qt Script object :

  ({ unitName: "Celsius",
     toKelvin: function(x) { return x + 273; }
   })

函数的作用是:以开尔文表示的温度作为参数,并返回转换为摄氏度的温度。下面的代码片段显示了如何从C++获取和调用toKelvin()函数:

QScriptValue object = engine.evaluate("({ unitName: 'Celsius', toKelvin: function(x) { return x + 273; } })");
  QScriptValue toKelvin = object.property("toKelvin");
  QScriptValue result = toKelvin.call(object, QScriptValueList() << 100);
  qDebug() << result.toNumber(); // 373

如果脚本定义了全局函数,则可以将该函数作为QScriptEngine::globalObject() 的属性访问。例如,以下脚本定义了一个全局函数add():

 function add(a, b) {
      return a + b;
  }

C++代码可以调用Add() 函数如下:

  QScriptValue add = engine.globalObject().property("add");
  qDebug() << add.call(QScriptValue(), QScriptValueList() << 1 << 2).toNumber(); // 3

如前所述,函数只是Qt脚本中的值;函数本身并不“绑定”特定对象。这就是为什么必须指定一个This对象(QScriptValue::call()的第一个参数),该函数应该应用于该对象。
如果函数它只能应用于某类对象,则由函数本身检查是否使用兼容的this对象调用它。
将无效的QScriptValue作为this参数传递给QScriptValue::call(),表示应该将全局对象用作this对象;换句话说,函数应该作为全局函数调用。

this 对象

从script 调用脚本函数时,调用该函数的方式决定了函数体的执行环境,如以下脚本示例所示:

  var getProperty = function(name) { return this[name]; };
  name = "Global Object"; // creates a global variable
  print(getProperty("name")); // "Global Object"

  var myObject = { name: 'My Object' };
  print(getProperty.call(myObject, "name")); // "My Object"

  myObject.getProperty = getProperty;
  print(myObject.getProperty("name")); // "My Object"

  getProperty.name = "The getProperty() function";
  getProperty.getProperty = getProperty;
  getProperty.getProperty("name"); // "The getProperty() function"

需要注意的是,在Qt脚本中,与C++和java不同,这个对象不是执行范围的一部分。这意味着成员函数(即对其进行操作的函数)必须始终使用This关键字来访问对象的属性。例如,以下脚本可能不符合您的要求:

  var o = { a: 1, b: 2, sum: function() { return a + b; } };
  print(o.sum()); // reference error, or sum of global variables a and b!!

上例中,a +b 中的 a、b由可能是全局变量中的a和b。
你会得到一个参考错误,表示“a未定义”,或者更糟的是,如果存在两个完全不相关的全局变量a和b,则将使用它们来执行计算。相反,脚本应该如下所示:

 var o = { a: 1, b: 2, sum: function() { return this.a + this.b; } };
 print(o.sum()); // 3

对于C++和java的范围规则,程序员不小心省略这个关键字是一个典型的错误源。

其他概念

可查看qt的帮助手册
类相关:

类名解释
QScriptClass脚本对象接口
QScriptClassPropertyIterator脚本对象的迭代器接口
QScriptContext脚本上下文
QScriptContextInfo脚本上下文信息
QScriptEngine评估Qt脚本代码的环境
QScriptEngineAgent脚本引擎的代理
QScriptEngineDebugger脚本调试器
QScriptProgram封装一个Qt脚本程序
QScriptString在QScript引擎中充当“暂存”字符串的句柄
QScriptSyntaxCheckResult脚本语法检查的结果
QScriptValue数据类型的容器
QScriptValueIteratorQScriptValue 的Java风格迭代器
QScriptable从Qt C++成员函数访问Qt脚本环境
  C++知识库 最新文章
【C++】友元、嵌套类、异常、RTTI、类型转换
通讯录的思路与实现(C语言)
C++PrimerPlus 第七章 函数-C++的编程模块(
Problem C: 算法9-9~9-12:平衡二叉树的基本
MSVC C++ UTF-8编程
C++进阶 多态原理
简单string类c++实现
我的年度总结
【C语言】以深厚地基筑伟岸高楼-基础篇(六
c语言常见错误合集
上一篇文章      下一篇文章      查看所有文章
加:2022-04-06 15:59:31  更:2022-04-06 16:00:23 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/24 0:11:42-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码