首 页 网络编程
网页制作 图形图象 操作系统 冲浪宝典
软件教学 认证考试

网络安全 网络办公 行业资讯 评测对比
您当前位置:站长天空 -> 网络编程-> ASP教程
多层数据库开发十一:tclientdataset_delphi教程
作者:网友供稿 点击:0
推荐
西部数码-全国虚拟主机10强!20余项虚拟主机管理功能,全国领先!第6代双线路虚拟主机,南北访问畅通无阻!可在线rar解压,自动数据恢复设置虚拟目录等.免费赠送访问统计,企业邮局.Cn域名注册10元/年,自助建站480元起,免费试用7天,满意再付款!P4主机租用799元/月.月付免压金
站内搜索
文章页数:[1] 
   第十一章 TClientDataSet
  与TTable、TQuery一样,TClientDataSet也是从TDataSet继承下来的,它通常用于多层体系结构的客户端。TClientDataSet最大的特点是它不依赖于BDE(Borland Database Engine),但它需要一个动态链接库的支持,这个动态链接库叫DBCLIENT.DLL。在客户端,也不需要用TDatabase构件,因为客户端并不直接连接数据库。
  由于TClientDataSet是从TDataSet继承下来的,所以,它支持诸如编辑、搜索、浏览、纠错、过滤等功能。由于TClientDataSet在内存中建立了数据的本地副本,上述操作的执行速度很快。也正是由于TClientDataSet并不直接连接数据库,因此,客户程序必须提供获取数据的机制。在Delphi 4中,TClientDataSet有三种途径获取数据:
.从文件中存取数据。
.从本地的另一个数据集中获取数据。
.通过IProvider接口从远程数据库服务器获取数据。
  在一个客户程序中,可以同时运用上述三种机制获取数据。
11.1 浏览和编辑数据
  和其他数据集构件一样,可以用标准的数据控件显示由TClientDataSet引入的数据集,当然,这需要借助于TDataSource构件。
  由于TClientDataSet是从TDataSet继承下来的,所以,凡是其他数据集构件支持的功能,TClientDataSet构件也大致具备。不同的是,TClientDataSet能够在内存中建立数据的副本,因此,TClientDataSet比其他数据集构件增加了一些特殊的功能。
11.1.1 浏览数据
  可以用标准的数据控件显示由TClientDataSet引入的数据集。在运行期,可以调用诸如First、GotoKey、Last、Next和Prior等函数来浏览数据。
  TClientDataSet也支持书签功能,可以用书签来标记某条记录,以后就可以方便地找到这条记录。
  对于TTable、TQuery等数据集构件来说,只能读RecNo属性来判断当前记录的序号。对于TClientDataSet构件来说,还可以写RecNo属性,使某一序号的记录成为当前记录。
11.1.2 CanModify属性
  TDataSet的CanModify属性用于判断数据集中的数据是否可以修改。CanModify属性本身是只读的,也就是说,数据是否能够修改不取决于应用程序。
  不过,TClientDataSet构件有其特殊性,因为TClientDataSet已经把数据在内存中建立了副本,因此,应用程序可以决定是否允许修改数据。如果不允许用户修改数据,只要把ReadOnly属性设为True,此时,CanModify属性肯定返回False。
  与其他数据集构件不同,修改TClientDataSet构件的ReadOnly属性时,不需要事先把Active属性设为True。
11.1.3 取消修改
  TClientDataSet传输数据的基本单位称为数据包,当前的数据包可以由Data属性来访问。不过,用户对数据的修改并不直接反映到Data属性中,而是临时写到一个日志即Delta属性中,这样做的好处是以后随时可以取消修改。
  不过,这里要说明一点,尽管用户的修改并没有反映到Data,当用户在数据控件中看到的却是最新修改的数据。如果一条记录被反复修改了多次,用户看到的只是最新的数据,但日志中却记载了多次。
  要取消上一次的修改,调用UndoLastChange函数。UndoLastChange需要传递一个布尔类型的参数叫FollowChange,如果FollowChange参数设为True,光标就移到被恢复的记录上,如果FollowChange参数设为False,光标仍然在当前记录上。
  ChangeCount属性返回日志中记载的修改次数。如果一条记录被反复修改了多次,每调用一次UndoLastChange能够逐级取消上一次的修改。
  UndoLastChange只能取消上一次的修改,如果想一下子取消所有的修改,首先要选择一个记录,然后调用RevertRecord。RevertRecord将从日志中取消所有对当前记录的修改。
  TClientDataSet还有一个SavePoint属性,它能把当前的编辑状态保存起来,以后随时可以返回当时的状态。例如,可以这样保存当前的状态:
  BeforeChanges := ClientDataSet1.SavePoint;
  以后,可以这样来恢复当时的状态:
  ClientDataSet1.SavePoint := BeforeChanges;
  应用程序可以保存多处状态,可以恢复其中一个状态,不过,一旦某个状态被恢复,在其之后的状态就无效。
  如果要一下子取消日志中记载的所有修改,可以调用CancelUpdates函数。CancelUpdates将把日志清空,取消所有的修改。
  如果LogChanges属性设为False,用户对数据的修改就会直接反映到Data属性中。
11.1.4 合并修改
  要把日志中记载的修改合并到Data属性中,有两种方式,具体使用哪一种方式,取决于应用程序获取数据的机制。不过,不管是哪种机制,合并后,日志自动被清空。
  对于一个从文件中获取数据的程序来说,只要调用MergeChangeLog函数,就把日志中记载的修改合并到Data属性中。不用担心其他用户同时修改了数据。
  对于一个从应用服务器获取数据的程序来说,就不能调用MergeChangeLog来合并数据,而要调用ApplyUpdates函数,ApplyUpdates会把日志中记载的修改传递给应用服务器,待应用服务器成功地把数据更新了数据库服务器后,才会合并到Data属性中。
11.1.5 纠错
  TClientDataSet支持纠错功能。一般情况下,需要自己建立纠错规则,以便对用户输入的数据进行纠错。
  此外,如果获得了IProvider接口的话,还可以从远程服务器引入纠错规则。
  有时候,客户端可能需要暂时禁止纠错,因为客户端从应用服务器检索数据是分阶段进行的,在所有的数据检索完毕之前,有些纠错规则很可能会报错。
要暂时禁止纠错,可以调用DisableConstraints,要重新允许纠错,可以调用EnableConstraints函数。DisableConstraints和EnableConstraints实际上都是作用于一个内部的计数。
11.2 索 引
  使用索引有这么几个好处:
.在数据集中定位记录比较快。
.能够在两个数据集之间建立Lookup或Master/Detail关系。
.可以对记录排序。
  在多层体系结构中,当客户程序从应用服务器检索数据时,它同时获得了默认的索引。默认的索引叫DEFAULT_ORDER,可以使用这个索引排序,但不能修改或删除这个索引。
  除了默认的索引外,TClientDataSet还对日志中记载的记录自动建立了一个副索引叫CHANGEINDEX。与DEFAULT_ORDER一样,不能修改或删除这个副索引。
  另外,还可以使用数据集中已建立的其他索引,或者自己建立索引。
11.2.1 创建一个新的索引
  要创建一个新的索引,可以调用AddIndex。AddIndex需要传递若干个参数:
  一是Name参数,用于指定索引名。在运行期切换索引时需要用到索引的名称。
  二是Fields参数,它是一个字符串,用于指定索引中的字段名,彼此之间用分号隔开。
  三是Options参数,用于设置索引的选项,包含ixDescending元素表示按降序排列,包含ixCaseInsensitive元素表示大小写不敏感。
  四是DescFields参数,它也是一个字符串,用于指定若干个字段名,这些字段将按照降序排列。
  五是CaseInsFields参数,它的作用与DescFields参数类似,包含在CaseInsFields参数中的字段将对大小写不敏感。
  六是GroupingLevel参数,用于指定分组级别,其值不能超过索引中的字段数。
  下面的代码创建了一个索引:
If Edit1.Text <> and ClientDataSet1.Fields.FindField(Edit1.Text) then
Begin
ClientDataSet1.AddIndex(Edit1.Text+Index,Edit1.Text,  
  [ixCaseInsensitive],,,0);
ClientDataSet1.IndexName := Edit1.Text + Index;
End;
为了避免创建一个索引,可以临时用IndexFieldNames属性来指定若干个字段,让数据集按这些字段排序。
11.2.2 删除和切换索引
  要删除一个先前创建的索引,可以调用DeleteIndex并指定要删除的索引名称。注意:DEFAULT_ORDER和CHANGEINDEX不能删除。
  如果建立了多个索引,可以任意选择其中的一个索引,这就要用到IndexName属性。
11.2.3 用索引把数据分组
  选择了一个索引后,数据集将自动按其中的字段进行排序。这样,临近的记录往往在关键字段上含有相同的值。例如,假设有一个表是这样的:
SalesRep Customer OrderNo Amount
1      1     5    100
1      1     2    50
1      2     3    200
1       2     6    75
2      1     1    10
2      3     4    200
  可以看出,SalesRep字段的值有重复的。对于SalesRep字段的值为1的来说,Customer字段的值也有重复的。这就是说,可以按SalesRep字段分组,进而再按Customer字段分组。显然,这里的分组级别是不同的,按SalesRep字段建立的分组属于第一级,按Customer字段建立的分组属于第二级。实际上,分组级别取决于字段在索引中的顺序。
  TClientDataSet可以决定是否按照分组级别来显示记录的值。例如,也许想以下面这种形式显示数据:
SalesRep Customer OrderNo Amount
1      1    5    100
           2    50
       2    3    200
           6    75
2      1    1    10
2      3    4    200
  要判断当前记录某一级的什么位置,可以调用GetGroupState函数。GetGroupState函数需要传递一个参数,用于指定分组级别。
11.3 计 算 字 段
  与其他数据集一样,也可以在TClientDataSet建立的数据集中增加计算字段。计算字段的值是基于同一个记录中的其他字段计算出来的。
  在其他数据集中,只要用户修改了数据或当前记录发生改变,就会触发OnCalcFields事件,换句话说,计算字段的值就被计算一次。
  TClientDataSet引入了“内部计算字段”的概念。与一般的计算字段不同的是,内部计算字段的值将随其他字段的值一起存取,这样,只有当用户修改了数据才会触发OnCalcFields事件,如果仅仅改变了当前记录,不会触发OnCalcFields事件。也就是说,内部计算字段的值需要重新计算的机会大大减少。
  在处理OnCalcFields事件的句柄中,首先要判断State属性。如果State属性返回dsInternalCalc,此时需要计算内部计算字段的值。如果State属性返回dsCalcFields,此时需要计算一般的计算字段的值。
11.4 统 计 值
  TClientDataSet增加了统计的功能,它可以基于分组自动计算总和、平均、计数、最大、最小值。当用户编辑数据时,这些统计值会自动跟着变化。
11.4.1 指定统计方式
  要指定怎样进行统计,就要用到Aggregates属性。这个属性是一个TAggregates对象,它用于管理一组TAggregate对象。
  在设计期,可以单击Aggregates属性边上的省略号按钮打开如图11.1所示
的编辑器。
  图11.1 管理一组TAggregate对象
  单击按钮可以增加一个TAggregate对象,单击按钮可以删减一个TAggregate对象,单击按钮可以把TAggregate对象前移,单击按钮可以把TAggregate对象后移。
  可以用字段编辑器专门创建一个用于表达统计值的字段,该字段的类型必须是“Aggregate”。Delphi 4会自动创建一个TAggregate对象,并加到Aggregates属性中。选择一个TAggregate对象,Object Inpector将显示该对象的属性。
  其中,Expression属性用于指定统计表达式,例如:
Sum(Field1)
  也可以是比较复杂的表达式:
Sum(Qty * Price) - Sum(AmountPaid)
  在表达式中,可以使用下列统计运算符:
.Sum计算一组数据的总和。
.Avg计算一组数据的平均值。
.Count计算一组数据中的非空值的个数。
.Min计算一组数据的最小值。
.Max计算一组数据的最大值。
  除了上述几个统计运算符外,还可以使用过滤条件中所能使用的运算符,但不能嵌套。在一个表达式中,可以混合出现几个统计值或常量,但不能混合出现统计值和字段。
  Sum(Qty * Price){合法}
  Max(Field1) - Max(Field2){合法}
  Avg(DiscountRate) * 100{合法}
  Min(Sum(Field1)){非法,不能嵌套}
  Count(Field1) - Field2{非法,统计值和字段不能混合出现在一个表达式中}
11.4.2 指定分组
  默认情况下,统计值是基于数据集中所有的记录计算出来的。不过,也可以针对一部分记录计算统计值,这就需要事先建立分组。
  前面在介绍索引时已经提到分组的概念。可以通过IndexName属性和GroupingLevel属性来选择使用哪个索引以及最大的分组级别。
  例如,假设有一个表是这样的:
SalesRep Customer OrderNo Amount
1      1     5    100
1      1     2    50
1      2     3    200
1       2     6    75
2      1     1    10
2      3     4    200
  如果要按SalesRep字段分组,并且指定其中的第一级,程序代码应当这样写:
Agg.Expression := Sum(Amount);
Agg.IndexName := SalesCust;
Agg.GroupingLevel := 1;
Agg.AggregateName := Total for Rep;
11.4.3 怎样获取统计值
  要获取统计值,可以调用TAggregate对象的Value函数。如果统计值是基于数据集中所有的记录计算出来的,随时可以调用Value函数。如果统计值是基于分组计算出来的,必须保证当前记录正好位于该分组内。因此,在调用Value之前,最好先调用GetGroupState函数看看当前记录是否位于该分组内。
  要在数据控件中显示统计值,必须事先在字段编辑器中创建一个永久字段对象,该字段的类型必须是Aggregate。
11.5 数 据 包
  通过Data属性可以访问客户程序从应用服务器检索到的数据。程序示例如下:
Procedure TForm1.Button1Click(Sender: TObject);
Begin
ClientDataSet1.Data := ClientDataSet1.Provider.DataRequest(FilterEdit.Text);
End;
11.5.1 直接对Data属性赋值
  前面讲过,客户程序既可以通过IProvider接口获取数据,也可以从另一个数据集获取数据,后者就是通过Data属性赋值的。程序示例如下:
  ClientDataSet1.Data := ClientDataSet2.Data;
  一旦Data被赋值,就可以用标准的数据控件显示这些数据。
  注意:当从另一个数据集获取数据时,另一个数据集的日志也将被复制过来,但不包括原来的范围和过滤条件。
  如果要从另一个基于BDE的数据集中获取数据,可以通过数据集构件的Provider属性,程序示例如下:
  ClientDataSet1.Data := Table1.Provider.Data;
  如果要从一个自定义的数据集获取数据,首先要创建一个临时的TProvider构件,然后设置其DataSet属性指定这个自定义的数据集。程序示例如下:
TempProvider := TDataSetProvider.Create(Form1);
TempProvider.DataSet := SourceDataSet;
ClientDataSet1.Data := TempProvider.Data;
TempProvider.Free;
11.5.2 在数据包中加入自定义的信息
  可以把自定义的信息加到数据包中。当把数据保存到文件或流中时,这些自定义的信息也将保存到文件或流中。如果把数据包直接赋值给另一个数据集的话,这些自定义的信息也将被复制。
  要把自定义的信息加到数据包中,可以调用SetOptionalParam函数。要从数据包中检索自定义的信息,可以调用GetOptionalParam。程序示例如下:
Procedure TAppServer.Provider1UpdateData(Sender: TObject; DataSet: TClientDataSet);
var
WhenProvided: TDateTime;
Begin
WhenProvided := DataSet.GetOptionalParam(TimeProvided);
...
End;
11.5.3 克隆另一个数据集
  调用TClientDataSet的CloneCursor函数可以获得一个数据集的完全相同的副本。它与直接通过Data属性赋值是有区别的。
  区别之一:数据在两个数据集之间是共享的,修改其中一个将同时修改另一个。
  区别之二:除了数据外,CloneCursor函数还复制了一些属性和事件,这取决于Reset和KeepSettings参数怎样设置。
   CloneCursor函数需要传递三个参数,其中,Source参数指定源数据集,Reset参数和KeepSettings参数用于设置除了数据外是否还要复制下列属性和事件:Filter、Filtered、FilterOptions、OnFilterRecord、IndexName、MasterSource、MasterFields、ReadOnly、RemoteServer、ProviderName、Provider。
  如果Reset和KeepSettings参数都设为False,源数据集的上述属性和事件都将被复制给目标数据集。如果Reset参数设为True,目标数据集的上述属性和事件都将被清空。如果Reset参数设为False,而KeepSettings参数设为True,目标数据集的上述属性和事件不变,不过,必须保证这些属性和事件与克隆后的数据相容。
11.6 与应用服务器通讯
  在多层体系结构中,客户程序通过IProvider接口与应用服务器交换数据。这一章介绍怎样在客户端获得IProvider接口、怎样向应用服务器传递参数、怎样向应用服务器请求数据、怎样把用户对数据的修改写到数据库中。
11.6.1 怎样在客户端获得IProvider接口
  在单层应用程序以及工作在“公文包”模式下的多层应用程序中,不需要用到IProvider接口。而在多层体系结构中,客户程序要与应用服务器交换数据,首先必须获得IProvider接口,这就要用到RemoteServer属性和ProviderName属性。
  RemoteServer属性用于指定客户端的MIDAS连接构件。MIDAS连接构件又称Data Broker,用于建立和维护与应用服务器的连接。
  在设计期,正确设置了RemoteServer属性后,就可以在对象观察器中为ProviderName属性选择一个值,实际上就是选择应用服务器上的一个TProvider构件。
11.6.2 向应用服务器传递参数
  客户程序可以向应用服务器传递参数,这些参数实际上是传递给应用服务器上的TQuery构件或TStoredProc构件。既可以在设计期也可以在运行期设置参数。
  在设计期,可以单击Params属性边上的省略号按钮,打开一个如图11.2所示的编辑器。
  图11.2 设置参数
  单击按钮可以增加一个参数,单击按钮可以删减一个参数,单击按钮可以把一个参数前移,单击按钮可以把一个参数后移。
  选择一个参数,对象观察器将显示该参数(TParam对象)的属性。
  在运行期可以调用TParams的CreateParam函数来创建一个参数。例如,下面的代码创建了一个参数叫CustNo,它的使用类型是ptInput,数据类型是ftInteger,它的值设为605。
With ClientDataSet1.Params.CreateParam(ftInteger, CustNo, ptInput) Do
AsInteger := 605;
  设置好参数以后,如果TClientDataset的Active属性是False,只要把Active属性设为True,这些参数将被自动传递给应用服务器。如果Active属性已经为True,就要调用SendParams函数把参数传递给应用服务器。
  注意:传递给应用服务器的参数必须与TQuery构件或TStoredProc构件的参数匹配,包括名称、数据类型和参数类型。
11.6.3 怎样向应用服务器请求数据
  TClientDataSet提供了两个属性和三个方法,用于怎样向应用服务器请求数据:
  一是FetchOnDemand属性。如果这个属性设为True,TClientDataSet会根据需要自动检索附加的数据包,例如BLOB字段的值或者嵌套表的内容。如果这个属性设为False,程序需要显式地调用GetNextPacket才能获得这些附加的数据包。
  二是PacketRecords属性,用于设置一个数据包中最多可容纳的记录数,设为-1表示一个数据包可以容纳数据集的所有记录。
  三是GetNextPacket函数,用于向应用服务器检索下一个数据包,并把检索到的数据包添加到前一次检索到的数据包的后面。这个函数返回实际检索到的记录数。
  四是FetchBlobs过程,用于从应用服务器检索BLOB字段的值。如果FetchOnDemand属性设为True,就没必要调用FetchBlobs函数。
  五是FetchDetails过程,用于检索嵌套表中的数据。如果FetchOnDemand属性设为True,就没必要调用FetchDetails函数。
11.6.4 更新数据库
  在多层体系结构中,用户在客户端修改了数据后,需要把最新的数据写到数据库中,这就要调用TClientDataSet的ApplyUpdates函数。
  ApplyUpdates只需要传递一个参数叫MaxErrors,用于指定一个整数,当遇到无法更新的记录超过这个数时,此次更新就中止。如果MaxErrors参数设为0,表示只要遇到一个错误更新就中止,客户端的日志保持不变。如果MaxErrors参数设为-1,当应用服务器发现有错误的记录,就尝试更新下一个记录,等所有的记录都尝试过以后才返回。
  ApplyUpdates会自动调用Reconcile函数,进而调用应用服务器上的TProvider构件的ApplyUpdates函数去更新远程的数据库服务器。没有被DBMS服务器认可的记录通过Reconcile返回给客户端,此时将在客户端触发OnReconcileError事件让您更正错误。最后,ApplyUpdates函数返回仍然没有被认可的记录数。
11.7 在文件中存取数据
  要从文件中读取数据,可以调用LoadFromFile函数。LoadFromFile函数需要传递一个参数,用于指定文件名。文件名应包含完整的路径。如果客户程序总是从一个固定的文件中读取数据,可以设置FileName属性指定一个文件名,以后,当TClientDataSet引入的数据集打开时,就自动从这个文件中读取数据,不需要调用LoadFromFile。
  要从流中读取数据,可以调用LoadFromStream。LoadFromStream需要传递一个参数,用于指定一个流对象。
  注意:LoadFromFile(LoadFromStream)只能从先前用SaveToFile(SaveToStream)保存的文件中读取数据。
  要把数据保存到文件中,可以调用SaveToFile函数。SaveToFile需要传递一个参数,用于指定文件名。如果指定的文件已存在,文件中的数据将被覆盖。如果客户程序总是把数据保存到一个固定的文件中,可以设置FileName属性指定一个文件名,当TClientDataSet引入的数据集关闭时,就自动把数据保存到这个文件中,不需要调用SaveToFile。
  要把数据保存到流中,可以调用SaveToStream。SaveToStream需要传递一个参数,指定一个流对象。
  注意:当把数据保存到文件或流中时,日志中记载的修改仍然保留。这样,当下次调用LoadFromFile或LoadFromStream读取数据时,仍然可以恢复原来的数据。

文章整理:站长天空 网址:http://www.z6688.com/
以上信息与文章正文是不可分割的一部分,如果您要转载本文章,请保留以上信息,谢谢!

文章页数:[1] 


放大字体显示 缩小字体显示 打印文章 推荐给朋友
热门文章
·在ASP.NET中使用Office Web Components (OWC)创建统计图-.NET教程,数据库应用
·windows系统下jsp+mysql+tomcat的配置-JSP教程,Application/Applet
·把aspx文件编译成DLL文件-.NET教程,Asp.Net开发
·Haneng.com的简单留言板制作源程序例子-ASP教程,ASP应用
·使用VB实现邮箱自动注册(一):表单自动提交-.NET教程,VB.Net语言
·VS.NET安装指南(To菜鸟)-.NET教程,Asp.Net开发
·web下水晶报表的使用!-.NET教程,Web Service开发
·C# 静态成员和方法的学习小结-.NET教程,数据库应用
·IIS的使用-ASP教程,ASP基础
·asp讲座之二:读取通过表单发送的数据
最新文章
·photoshop鼠绘实例:浪漫夏夜壁纸_photoshop教程
·买卖中小网站交易的一些细节问题_站长心得
·七招打造最安全的windows xp操作系统_windows xp
·做google adsense最佳和最重要的要诀_网赚技巧
·上下文关联广告清单(内文广告)推荐_网赚技巧
·广告联盟,痛定思痛_网赚技巧
·insenz首批广告费发放给站长_网赚技巧
·李彦宏:三分之一时间用在寻找人才_站长访谈
·中国汽车资源网杨锁民:网络寒冬时下海_站长访谈
·做it新闻资讯网站应先学新浪_站长心得
相关主题
  • 多层数据库开发十四:剖析几个midas示范程序_delphi教程
  • 多层数据库开发十三:剖析几个数据库应用程序_delphi教程
  • 多层数据库开发十二:使用数据控件_delphi教程
  • 多层数据库开发九:查询数据库_delphi教程
  • 多层数据库开发六:什么是数据集_delphi教程
  • 西部数码虚拟主机

    友情链接
    CNNIC 西部数码
    万网 自助建站
    虚拟主机 asp空间
    域名注册 域名
    域名申请 主页空间
    论坛空间 网站空间
    国际域名 虚拟空间
    空间租用 DDOS防火墙
    成都主机托管 四川主机托管
    主机租用 服务器租用
    网站目录 自助建站
    虚拟主机 网址大全
    软件下载
    自助链接
    虚拟主机资讯 特价虚拟主机
    版权申明:本站文章均来自网络,如有侵权,请联系我们,我们收到后立即删除,谢谢!
    关于我们:站长天空:专业提供最新的站长资讯、在线教程、虚拟主机权威评测、虚拟主机性能对比、网站制作教程,开发教程,站长工具。包括网页制作教程、冲浪宝典、编程参考、操作系统、软件教学、行业动态等。
    特别注意:本站所有转载文章言论不代表本站观点,本站所提供的摄影照片,插画,设计作品,如需使用,请与原作者联系,版权归原作者所有。
    发表评论 打印  刷新     关闭