Qt Quick应用开发介绍 6-8(JavaScript_视觉化数据_组件和模块)

Chapter6 Using JavaScript 使用JavaScript

在QtQuick中JavaScript可以有很多复杂和强大的用法; 实际上, QtQuick是被实现成一个JavaScript的扩展; JS基本可以在任何地方使用, 只要代码返回的值的类型和预期的一致; 此外, 使用JS是一部分处理应用逻辑和计算的代码的标准形式;

6.1 JavaScript is not JavaScript 

JS是从web开发产生的; 在那段时间内, JS快速成长为许多受欢迎和优秀的扩展, add-ons的开发工具; 为了有更加广泛的支持, JS被标准化, 成为ECMAScript-262标准的开发语言; 要强调ECMAScript-262只覆盖了语言部分, 像获取web页面的对象和库的API这样的附加方法没有涉及; 不管标准化花了多大力气, 许多JS的web开发细节仍旧是浏览器特定的, 甚至近年来更严重; http://en.wikipedia.org/wiki/Javascript 

JS也在web之外被使用, 裁剪成支持某种用例的版本; 用户端的JS用法在web开发中还是占有主导地位的; 所有的书本和多数的web资源实际上都是为web开发存在的; QtQuick属于一个在web之外使用JS的平台; 如果你以后对JS了解更多, 注意其中的区别;

Qt开发团队在尽力提供更多QtQuick中JS的细节, 本文会涉及其中一部分;

6.2 More about JavaScript

本文包含了一份JavaScript基础的附录, 如果你不熟悉JS, 建议先阅读一下;

除了附录, 也可以看看Mozilla的开发者网络资源:

http://developer.mozilla.org/en/JavaScript/About_JavaScript  http://developer.mozilla.org/en/A_re-introduction_to_JavaScript http://developer.mozilla.org/en/JavaScript/Guide 

下面三篇文章解释了JS在QtQuick中的基本要素:

- 整合JS: QtQuick中使用JS的关键点 http://qt-project.org/doc/qt-4.8/qdeclarativejavascript.html  [stateless helper functions, .pragma library, Qt.include("factorial.js"), Component.onCompleted, Global对象是constant的, 目前大多上下文中的this未定义]

Note [There is no way to create a property binding directly from imperative JavaScript code, although it is possible to use the Binding element.]

- ECMAScript参考: 在QtScript/QtQuick中支持的内建类型, 方法和属性的列表; http://qt-project.org/doc/qt-4.8/ecmascript.html 

- QML Scope 解释了JS对象和QtQuick item的可见性 http://qt-project.org/doc/qt-4.8/qdeclarativescope.html 

注意不久以后Qt Doc对JS的使用可能会有重大更新, 随着新的Qt发布更全面的使用覆盖; 

6.3 Adding Logic to Make the Clock Tick 添加逻辑让时钟走动

之前我们已经用了一些JS, 错误处理之类; 这节要使用JS显示时间日期;

我们将从全局对象获取现在的时间日期; 返回值将被格式化, 留下日期和时间信息; http://qt-project.org/doc/qt-4.8/qml-qt.html#formatDateTime-method 

1
2
3
4
function  getFormattedDateTime(format) {
     var  date =  new  Date
     return  Qt.formatDateTime(date, format)
}

Qt.formatDateTime属于QML全局对象, 除了ECMAScript Reference 中定义的标准之外, 其中还提供了很多其他有用的功能; 

getFormattedDateTime()被另一个方法使用, 在Text元素中创建真实的值:

1
2
3
4
5
function  updateTime() {
     root.currentTime =  "<big>"  + getFormattedDateTime(Style.timeFormat) +  "</big>"  +
         (showSeconds ?  "<sup><small> "  + getFormattedDateTime( "ss" ) +  "</small></sup>"  "" );
     root.currentDate = getFormattedDateTime(Style.dateFormat);
}

Note 我们使用多格式文本rich-text格式化text的时间值;

在showSeconds上使用三元运算符conditional operator/ternary operator, 它是一个自定义属性, 表明时间是否要显示秒数; 在QtQuick中使用conditional operator来将属性(或者变量)绑定到一个依赖条件决定的值上面, 是非常方便的; 

updateTime()会触发currentTime和currentDate不断更新; 使用Timer元素:

1
2
3
4
5
6
7
8
9
10
11
12
Timer {
     id: updateTimer
     running: Qt.application.active && visible ==  true
     repeat:  true
     triggeredOnStart:  true
     onTriggered: {
         updateTime()
         // refresh the interval to update the time each second or minute.
         // consider the delta in order to update on a full minute
         interval = 1000*(showSeconds? 1 : (60 - getFormattedDateTime( "ss" )))
     }
}

实现中有些有趣部分: 为了优化耗电, 把timer的running属性绑定到2个其他属性上, 从而减轻CPU负荷; 当clock元素不可见(在使用其他应用时)或者程序不再处于活动状态(在后台运行或最小化iconified)

我们在没有启动但是timer触发的时候也给interval属性分配了值; 这是为了在秒数没有被使用的时候来抓取增量时间的, 以保证时间的更新对应分钟;

代码: NightClock/NightClock.qml

6.4 Importing导入JavaScript文件 

如果你的程序有很多JS代码, 考虑将它们移到一个单独的文件中; 你可以import这些文件就像importQtQuick模块; 由于JS在QtQuick中的特殊角色, 你必须为这些文件的内容定义namespace; e.g. 例子中的Logic; 你的代码会像这样使用: Logic.Foo(), 而不是直接 Foo(); 导入语句看起来是这样的:

1
2
import QtQuick 1.1
import  "logic.js"  as Logic

Note 如果应用逻辑很复杂, 考虑将它们在C++实现, 然后导入QtQuick: http://qt-project.org/doc/qt-4.8/qml-extending.html 

Note that signals with the same name but different parameters cannot be distinguished.

当你导入一个JS文件, 用起来就像库一样, 范围限于导入它的QML文件; 一些情况下, 你需要一个stateless的库或者一组由多个QML文件共享的全局变量; [就像static的, 普通的JS对于每个QML都有一份对象] 你可以使用 .pragma library 声明;  http://qt-project.org/doc/qt-4.8/qdeclarativejavascript.html 

这里将clock的JS方法搬到logic.js, 导入名为Logic; 还把所有style属性搬到style.js, 导入名Style; 这样相当程度上简化了代码, 而且其他组件也可以共享样式style;

代码: NightClock.qml

更多JS的高级用法

Qt Quick Application Developer Guide for Desktop  http://qt-project.org/wiki/Developer-Guides/ 

---6---


Chapter7 Acquire and Visualize Data 获取以及视觉化数据

这章开始天气预报应用, 主要关注数据处理; 前一个代码中数据都在属性和JS变量中; 对于简小的程序或许足够, 但很快你会需要处理大量的数据;

QtQuick应用了现有的model-view结构, 提供了一组方便的APIs; model用来保存或者获取数据; View元素读取model项. 把每个model根据delegate用特定的方式渲染出来; e.g. 一个grid或一个list;

7.1 Models 模型

QtQuick模型model非常简单, 基于列表list的概念; 用的最多的三种model:

- 一个int值(用来显示多次)

- 一个JavaScript对象的数组array

- 列表list model, e.g. ListModel, XmlListModel元素 

看一下Models and Data Handling部分: http://qt-project.org/doc/qt-4.8/qdeclarativeelements.html#models-and-data-handling [Binding],了解model相关列表; 还有一些高级方法在QML Data Models中提到; http://qt-project.org/doc/qt-4.8/qdeclarativemodels.html [in delegate: ListView.view.model] 

我们将使用XmlListModel, 还要看几个使用int和array作为model的例子;

我们的天气预报程序使用Google weather API来获取数据;  注意, Google weather API还没有作为常规的互联网服务;

通过这些API, 你可以在网上创建一个请求query, 然后接收XML格式的天气数据的response; 作为一个常规的数据储备, QtQuick提供了一个专门的model: XmlListModel;

XmlListModel使用XPath和XQuery(http://en.wikipedia.org/wiki/XPath)来读取XML数据; 使用XmlRole来创造model items对应被选中的XML树节点;

请求URL类似这样 http://www.google.com/ig/api?weather=[LOCATION]&hl=[LANGUAGE], 返回最新天气情况和后几天的预报; e.g. http://www.google.com/ig/api?weather=Munich&hl=en 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<? xml  version = "1.0"  ?>
< xml_api_reply  version = "1" >
< weather  module_id = "0"  tab_id = "0"  mobile_row = "0"  mobile_zipped = "1"  row = "0"  section = "0" >
< forecast_information >
     < city  data = "Munich, Bavaria"  />
     < postal_code  data = "Munich"  />
     < latitude_e6  data = ""  />
     < longitude_e6  data = ""  />
     < forecast_date  data = "2012-02-22"  />
     < current_date_time  data = "1970-01-01 00:00:00 +0000"  />
     < unit_system  data = "US"  />
</ forecast_information >
< current_conditions >
     < condition  data = "Clear"  />
     < temp_f  data = "39"  />
     < temp_c  data = "4"  />
     < humidity  data = "Humidity: 56%"  />
     < icon  data = "/ig/images/weather/sunny.gif"  />
     < wind_condition  data = "Wind: E at 8 mph"  />
</ current_conditions >
 
< forecast_conditions >
     < day_of_week  data = "Fri"  />
     < low  data = "36"  />
     < high  data = "54"  />
     < icon  data = "/ig/images/weather/sunny.gif"  />
     < condition  data = "Clear"  />
</ forecast_conditions >
< forecast_conditions >
     < day_of_week  data = "Sat"  />
     < low  data = "34"  />
     < high  data = "48"  />
     < icon  data = "/ig/images/weather/chance_of_rain.gif"  />
     < condition  data = "Chance of Rain"  />
</ forecast_conditions >
</ weather >
</ xml_api_reply >

进行请求和处理的数据的model:

1
2
3
4
5
6
7
8
XmlListModel {
     id: weatherModelCurrent
     source: baseURL + dataURL + location +  "&hl="  + language
     query:  "/xml_api_reply/weather/current_conditions"
     XmlRole { name:  "condition" ; query:  "condition/@data/string()"  }
     XmlRole { name:  "temp_f" ; query:  "temp_f/@data/string()"  }
//...
}

仔细看XmlRole元素, 你会发现它基本上是按照属性-值组合对来创建model的, 通过query开始处定义的节点, 把它们map成特定的XML树的节点; 比如Image, Font, XmlListModel都提供了statusprogress属性, 用来跟踪读取的过程, 捕获错误; 另外, 还有一个reload()方法会强行让model请求一次URL和加载数据; 我们会用它来让天气预报保持更新;

7.2 Repeater和View

现在将model中收集的天气数据可视化; QtQuick有很多方法, 可视化的大多数元素是继承自Flickable的: ListView, GridView, PathView; 

这些元素作为view port, 使用delegate元素来画出每个model item; View通过height和width设定一个固定的大小; 里面的内容在特定区域画出来, 而且是flicked的(默认可以up/down): 

1
2
3
4
5
ListView {
     width: 150; height: 50
     model: [ "one" "two" "three" "four" "five" // or just a number, e.g 10
     delegate: Text { text:  "Index: "  + model.index +  ", Data: "  + model.modelData }
}

[model.index/modelData, delegate中使用model的内置属性 - QAbstractItemDelegate]

view的最佳使用是对于一个有巨大数目的model items要被显示出来的时候; view提供内建的scroll或flick功能, 对于大数据集合支持人体工程学表现; 由于performance的原因, view只是部分地加载可见的的item, 而不是整个集合;

使用视图view的好处

view提供了多样的功能, 可以创建漂亮又精巧的UI; http://qt-project.org/wiki/Qt_Quick_Carousel 

如果你有小量的model item要被一个接一个地按次序放置, 那么使用Repeater会比较合适;  Repeat按model中的item来创建特定元素; 这些元素必须用positioner来放置在屏幕上, e.g. Column, Grid之类; 上面的例子可以改成Repeater:

1
2
3
4
5
6
Column {
     Repeater {
         model: [ "one" "two" "three" "four" "five" // or just a number, e.g 10
         Text { text:  "Index: "  + model.index +  ", Data: "  + model.modelData }
     }
}

注意所有的item现在都是可见的, 即便Column的size没有设定; Repeater会计算元素的size, 将Column调整到合适大小; http://qt-project.org/doc/qt-4.8/qml-views.html [ListView-section]

再添加两个可视化的element来完善程序, 每个都有自己的delegate; 我们要把delegate分成最近的天气情况和天气预报, 它们有不同的结构, 用不同方式来展示;

该使用哪种element呢? view和Repeater都可以么?  weatherModelForecast可以显示成一个GridView, 可以是多列的; 如果用Repeater看起来就会像一列;

weatherModelCurrent只有一个item, 因此Repeater足够显示; 

Weather/weather.qml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
Column {
      id: allWeather
      anchors.centerIn: parent
      anchors.margins: 10
      spacing: 10
 
      Repeater {
          id: currentReportList
          model: weatherModelCurrent
          delegate: currentConditionDelegate
      }
 
      /* we can use a GridView...*/
      GridView {
          id: forecastReportList
          width: 220
          height: 220
          cellWidth: 110; cellHeight: 110
          model: weatherModelForecast
          delegate: forecastConditionDelegate
      }
      /**/
 
      /* ..a Repeater
      Repeater {
          id: forecastReportList
          model: weatherModelForecast
          delegate: forecastConditionDelegate
      }
      */
  }

FolderListModel - Qt4.8可以使用plug-in来实现, Qt5已经加入QML element;

http://qt-project.org/doc/qt-4.8/src-imports-folderlistmodel.html 

下一步

下一章把clock和weather forecast放入一个程序中; 

---7---


Chapter8 Comoments and Modules 组件和模块

下一步的版本: 将开发的天气预报和时钟应用组合起来; 我们不用再次实现这些特性或者拷贝代码; 只要稍稍改动一下程序, 重用组件即可; 

下一章我们会学习添加更多组件来强化程序的功能;

8.1 Creating Components and Collecting Modules 创建组件以及搜集模块

component在QtQuick中的概念很简单: 任何由其他elements组合的item或由自己组合起来的component. component是一些用来创建更大应用程序的建筑块; 使用module的时候, 可以创建component集合(libraries)

为了创建component, 你要先创建一个文件: <NameOfComponent>.qml; 文件中有一个root元素(和普通的QtQuick程序一样); Note 文件名必须首字母大写; 

现在开始, 新的名为<NameOfComponent>的component可以在任何其他同一文件夹下的QtQuick程序中使用; 一般来说, 文件有qml后缀的就是QML文件 http://qt-project.org/doc/qt-4.8/qdeclarativedocuments.html  

工作中, 你可能会在程序文件夹有许多文件, 甚至要管理不同版本的component; QtQuick module这时可以帮到你; 1) 把所有属于一种功能/模块组的component(基本上就是文件)搬到新的文件夹中; 2) 然后你需要创建一个qmldir 文件包含文件夹中这些component的meta-information; 3) 这样这个文件夹就变成一个模块, 可以import到你的应用中, 和标准QtQuick元素一样:

1
2
import QtQuick 1.1
import  "components"  1.0

Define New Components http://qt-project.org/doc/qt-4.8/qmlreusablecomponents.html 

如果你把文件夹和模块移动了位置, 必须在QML文件中更新路径; 你可以在全局中的任何程序中使用这些预设的模块; http://qt-project.org/doc/qt-4.8/qdeclarativemodules.html 

Note Component也可以作为C++plug-ins, http://qt-project.org/doc/qt-4.8/qml-extending.html 

有些情况下, 你必须定义in-line的component, e.g. 在同一个QML文件中, 当你把一个component的引用传递给了一个元素; 这种情况常见在view的delegate; 

在import模块或component的时候如果你遇到问题, 设置环境变量: QML_IMPORT_TRACE: http://qt-project.org/doc/qt-4.8/qdeclarativedebugging.html 

实践一下:

把NightClock.qml移到一个叫components的文件夹下面, 包含两个component: Weather和WeatherModelItem; 就像前面提到的, 添加一个qmldir文件, 用来描述新的moudle:

components/qmldir

1
2
3
4
Configure 1.0 Configure.qml
NightClock 1.0 NightClock.qml
WeatherModelItem 1.0 WeatherModelItem.qml
Weather 1.0 Weather.qml

Weather和WeatherModelItem包含了前面章节的代码;

8.2 Defining Interfaces and Default Behavior 定义接口和默认行为

把代码移到单独的文件中只是创建component的第一步; 你必须定义一个新的component将怎样使用, i.e. 哪些接口用来改变行为和外观; 如果你使用Qt/C++, 应当记住在QtQuick中使用component和在C++中使用class和library不同; 

QtQuick和JavaScript差不多; 如果在你的Item中使用一个外部component, 它加载后就好像是被内联定义的一样, 有property, handler, signal等; 你可以把现存的property绑定到另一个值上, 使用已有的signal和handler; 你也可以扩展component, 声明其他的property, 新的signal, handler, JavaScript function; 虽然这些步骤都是可选的, 一个component加载的时候必须有一个默认的外观和行为; e.g.

1
2
3
4
5
6
7
8
import QtQuick 1.1
import  "components"  1.0
Item {
     id: root
     NightClock {
         id: clock
     }
}

这个和独立执行NightClock的QtQuick的程序一样; 

我们尝试再创建个新程序clock-n-weather, 使用3个component: NightClock--数字钟, WeatherModelItem--结合了天气预报与当前天气的model, Weather--绘制天气数据的delegate;

代码稍有改动:

1
2
3
4
5
6
7
8
NightClock {
     id: clock
     height: 80
     width: 160
     showDate: root.showDate
     showSeconds: root.showSeconds
     textColor: Style.onlineClockTextColor
}

showDate和showSeconds属性是配置参数, 在root元素中作为属性的值; 

8.3 Handling Scope 处理作用域 

WeatherModelItem的作用和之前的component有些不同, 合理的做法是将forecast model和current condition model组合成一个component, 这样我们就可以把它们作为一个天气model使用:

components/WeatherModelItem.qml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
Item {
     id: root
     property alias forecastModel: forecast
     property alias currentModel: current
//...
 
     XmlListModel {
         id: forecast
         source: root.source
         query:  "/xml_api_reply/weather/forecast_conditions"
 
         XmlRole { name:  "day_of_week" ; query:  "day_of_week/@data/string()"  }
         XmlRole { name:  "low" ; query:  "low/@data/string()"  }
  //...
     }
 
     XmlListModel {
         id: current
         source: root.source
         query:  "/xml_api_reply/weather/current_conditions"
 
         XmlRole { name:  "condition" ; query:  "condition/@data/string()"  }
         XmlRole { name:  "temp_c" ; query:  "temp_c/@data/string()"  }
         onStatusChanged: {
//  ...
     }
 
     Timer {
         // note that this interval is not accurate to a second on a full minute
         // since we omit adjustment on seconds like in the clock interval
         // to simplify the code
         interval: root.interval*60000
         running: Qt.application.active && !root.forceOffline
         repeat:  true
         onTriggered: {
             current.reload()
             forecast.reload()
         }
     }
}

上述代码中, 一个Item中有2个model; 之后我们可以单独访问其中的一个, 在view中显示; WeatherModelItem的id是weatherModelItem, 你可能觉得可以使用weatherModelItem.forecast 和 weatherModelItem.current来分别访问它们; 但是不行; 

Note 问题在于一个imported的component的child item默认是不可见的; 一种解决方案是使用alias property来导出id;

1
2
property alias forecastModel: forecast
property alias currentModel: current

item的Scope和visibility, property和JavaScript object是QtQuick中的重要部分; --QML Scope http://qt-project.org/doc/qt-4.8/qdeclarativescope.html 

上面的文章解释了QtQuick的scope机制对名字冲突的解析; 记住这些规则很重要; 日常工作中应当注意对绑定的property的适当描述, 防止side effect, 使得代码容易理解; e.g. 使用这样的代码

1
2
3
4
5
Item {
id: myItem
...
enable: otherItem.visible
}

而不要:

1
2
3
4
5
Item {
id: myItem
...
enable: visible
}

8.4 Integrated Application 集成整个程序

之前代码中有些改进部分值得注意; 最重要的一个是timer, 触发了两个model的加载:

1
2
3
4
5
6
7
8
9
10
11
12
Timer {
     // note that this interval is not accurate to a second on a full minute
     // since we omit adjustment on seconds like in the clock interval
     // to simplify the code
     interval: root.interval*60000
     running: Qt.application.active && !root.forceOffline
     repeat:  true
     onTriggered: {
         current.reload()
         forecast.reload()
     }
}

这个timer和前面更新天气数据的时钟应用类似; root.interval是一个可配置的参数, 定义为一个属性并且绑定了相应的值;

我们也更新了delegate来绘制天气情况; 使用本地的icon来代替网络上的; 这样做有很多好处, 比如节省带宽(如果是移动设备), 或者这个外观更符合预期而且也不用依赖于外部网络情况; KED http://www.kde.org/ 上有不错的icon; 将它们重命名以符合天气情况的描述; 写几句JavaScript就能加载它们:

1
2
3
4
5
6
7
8
9
10
11
12
13
Image {
     id: icon
     anchors.fill: parent
     smooth:  true
     fillMode: Image.PreserveAspectCrop
     source:  "../content/resources/weather_icons/"  + conditionText.toLowerCase().split( ' ' ).join( '_' ) +  ".png"
     onStatusChanged:  if  (status == Image.Error) {
                          // we set the icon to an empty image if we failed to find one
                          source =  ""
                          console.log( "no icon found for the weather condition: \""
                                      + conditionText +  "\"" )
     }
}

注意Weather组件可以独立运行; 它使用了默认属性值; 这样做有利于在各种情况下进行测试; 

主要的Item使用了各种component: clock-n-weather/ClockAndWeather.qml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
import  "../components"  1.0
import  "../js/logic.js"  as Logic
import  "../js/style.js"  as Style
 
Rectangle {
     id: root
//...
     Image {
         id: background
         source:  "../content/resources/background.png"
         fillMode:  "Tile"
         anchors.fill: parent
         onStatusChanged:  if  (background.status == Image.Error)
                              console.log( "Background image \""  +
                                          source +
                                          "\" cannot be loaded" )
     }
 
     WeatherModelItem {
         id: weatherModelItem
         location: root.defaultLocation
         interval: root.defaultInterval
     }
 
     Component {
         id: weatherCurrentDelegate
         Weather {
             id: currentWeatherItem
             labelText: root.defaultLocation
             conditionText: model.condition
             tempText: model.temp_c +  "C°"
         }
     }
 
     Component {
         id: weatherForecastDelegate
         Weather {
             id: forecastWeatherItem
             labelText: model.day_of_week
             conditionText: model.condition
             tempText: Logic.f2C (model.high) +
                       "C° / "  +
                       Logic.f2C (model.low) +
                       "C°"
         }
     }
 
     Column {
         id: clockAndWeatherScreen
         anchors.centerIn: root
 
         NightClock {
  //...
         }
 
         Repeater {
             id: currentWeatherView
             model: weatherModelItem.currentModel
             delegate: weatherCurrentDelegate
         }
 
         GridView {
             id: forecastWeatherView
             width: 300
             height: 300
             cellWidth: 150; cellHeight: 150
             model: weatherModelItem.forecastModel
             delegate: weatherForecastDelegate
         }
     }
//...
}

WeatherModelItem作为weatherModelItem加载进来, 随后定义了了2个基于Weather的delegate; Column放置NightClock, Repeater放置当前天气, GridView放置了预报; 

8.5 Further Readings 扩展阅读

UI elements on Symbian http://doc.qt.digia.com/qtquick-components-symbian-1.1/index.html

开发的UI Component http://qt-project.org/wiki/Qt_Quick_Components 

桌面: https://qt.gitorious.org/qt-components/desktop   

下一步 集中在用户交互上; 

---8---

--YCR---

 


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值