abchjb

导航

Delphi下的COM编程

Delphi下的COM编程

作者:岑心 03/9(转载需征得作者同意)


Delphi通过向导可以非常迅速和方便的直接建立实现COM对象的代码,但是整个COM实现的过程被完全的封装,甚至没有VCL那么结构清晰可见。

一个没有C++下COM开发经验甚至没有接触过COM开发的Delphi程序员,也能够很容易的按照教程设计一个接口,但是,恐怕深入一想,连生成的

代码代表何种意义,哪些能够定制都不清楚。前几期 “DELPHI下的COM编程技术”一文已经初步介绍了COM的一些基本概念,我则想谈一些个人

的理解,希望能给对Delphi下COM编程有疑惑的朋友带来帮助。

COM (组件对象模型 Component Object Model)是一个很庞大的体系。简单来说,COM定义了一组API与一个二进制的标准,让来自不同平台、不

同开发语言的独立对象之间进行通信。COM对象只有方法和属性,并包含一个或多个接口。这些接口实现了COM对象的功能,通过调用注册的COM

对象的接口,能够在不同平台间传递数据。

COM光标准和细节就可以出几本大书。这里避重就轻,仅仅初步的解释Delphi如何进行COM的封装及实现。对于上述COM技术经验不足的Delphi程

序开发者来说,Delphi通过模版生成的代码就像是给你一幅抽象画照着画一样,画出来了却不一定知道画的究竟是什么,也不知该如何下手画

自己的东西。本文能够帮助你解决这类疑惑。

再次讲解一些概念

“DELPHI下的COM编程技术”一文已经介绍了不少COM的概念,比如GUID、CLSID、IID,引用计数,IUnKnown接口等,下面再补充一些相关内容

COM与DCOM、COM+、OLE、ActiveX的关系

DCOM(分布式COM)提供一种网络上访问其他机器的手段,是COM的网络化扩展,可以远程创建及调用。COM+是Microsoft对COM进行了重要的更

新后推出的技术,但它不简单等于COM的升级,COM+是向后兼容的,但在某些程度上具有和COM不同的特性,比如无状态的、事务控制、安全控

制等等。

以前的OLE是用来描述建立在COM体系结构基础上的一整套技术,现在OLE仅仅是指与对象连接及嵌入有关的技术;ActiveX则用来描述建立在COM

基础上的非COM技术,它的重要内容是自动化(Automation),自动化允许一个应用程序(称为自动化控制器)操纵另一个应用程序或库(称为

自动化服务器)的对象,或者把应用程序元素暴露出来。

由此可见COM与以上的几种技术的关系,并且它们都是为了让对象能够跨开发工具跨平台甚至跨网络的被使用。


Delphi下的接口

Delphi中的接口概念类似C++中的纯虚类,又由于Delphi的类是单继承模式(C++是多继承的),即一个类只能有一个父类。接口在某种程度上

可以实现多继承。接口类的声明与一般类声明的不同是,它可以象多重继承那样,类名 = class (接口类1,接口类2… ),然后被声明的接口

类则重载继承类的虚方法,来实现接口的功能。

以下是IInterface、IUnknown、IDispatch的声明,大家看出这几个重要接口之间是什么样的联系了吗?任何一个COM对象的接口,最终都是从

IUnknown继承的,而Automation对象,则还要包含IDispatch,后面DCOM部分我们会看到它的作用。

  IInterface = interface

    [''''{00000000-0000-0000-C000-000000000046}'''']

    function QueryInterface(const IID: TGUID; out Obj): HResult; stdcall;

    function _AddRef: Integer; stdcall;

    function _Release: Integer; stdcall;

  end;

  IUnknown = IInterface;

  IDispatch = interface(IUnknown)

    [''''{00020400-0000-0000-C000-000000000046}'''']

    function GetTypeInfoCount(out Count: Integer): HResult; stdcall;

    function GetTypeInfo(Index, LocaleID: Integer; out TypeInfo): HResult; stdcall;

    function GetIDsOfNames(const IID: TGUID; Names: Pointer;

      NameCount, LocaleID: Integer; DispIDs: Pointer): HResult; stdcall;

    function Invoke(DispID: Integer; const IID: TGUID; LocaleID: Integer;

      Flags: Word; var Params; VarResult, ExcepInfo, ArgErr: Pointer): HResult; stdcall;

  end;

对照“DELPHI下的COM编程技术”一文,可以明白IInterface中的定义,即接口查询及引用记数,这也是访问和调用一个接口所必须的。

QueryInterface可以得到接口句柄,而AddRef与Release则负责登记调用次数。

COM和接口的关系又是什么呢?COM通过接口进行组件、应用程序、客户和服务器之间的通信。COM对象需要注册,而一个GUID则是作为识别接口

的唯一名字。

假如你创建了一个COM对象,它的声明类似 Txxxx= class(TComObject, Ixxxx),前面是COM对象的基类,后面这个接口的声明则是:Ixxxx =

interface(IUnknown)。所以说IUnknown是Delphi中COM对象接口类的祖先。到这一步,我想大家对接口类的来历已经有初步了解了。


聚合

接口是COM实现的基础,接口也是可继承的,但是接口并没有实现自己,仅仅只有声明。那么怎么使COM对象对接口的实现得到重用呢?答案就

是聚合。聚合就是一个包含对象(外部对象)创建一个被包含对象(内部对象),这样内部对象的接口就暴露给外部对象。

简单来说,COM对象被注册后,可以找到并调用接口。但接口不是仅仅有个定义吗,它必然通过某种方式找到这个定义的实现,即接口的“实现

类”的方法,这样才最终通过外部的接口转入进行具体的操作,并通过接口返回执行结果。


进程内与进程外(In-Process, Out-Process)

进程内的接口的实现基础是一个DLL,进程外的接口则是建立在应用程序(EXE)上的。通常我们建立进程外接口的目的主要是为了方便调试(

跟踪DLL是件很麻烦的事),然后在将代码改为进程内发布。因为进程内比进程外的执行效率会高一些。

COM对象创建在服务器的进程空间。如果是EXE型服务器,那么服务器和客户端不在同一进程;如果是DLL型服务器,则服务器和客户端就是一个

进程。所以进程内还能节省内存空间,并且减少创建实例的时间。

StdCall与SafeCall

Delphi生成的COM接口默认的方法函数调用方式是stdcall而不是缺省的Register。这是为了保证不同语言编译器的接口兼容。

双重接口(在后面讲解自动化时会提到双重接口)中则默认的是SafeCall。它的意义除了按SafeCall约定方式调用外,还将封装方法以便向调

用者返回HResult值。SafeCall的好处是能够捕获所有异常,即使是方法中未被代码处理的异常,也可以被外套处理并通过HResult返回给调用

者。


WideString等一些有差异的类型

接口定义中缺省的字符参数或返回值将不再是String而是WideString。WideString 是Delphi中符合OLE 32-bit版本的Unicode类型,当是字符

时,WideString与String几乎等同,当处理Unicode字符时,则会有很大差别。联想到COM本身是为了跨平台使用,可以很容易的理解为什么数

据通信时需要使用WideString类型。

同样的道理,integer类型将变成SYSINT或者Int64、SmallInt或者Shortint,这些细微的变化都是为了符合规范。

通过向导生成基础代码


打开创建新工程向导(菜单“File-New-Other”或“New Items按钮”),选择ActiveX页。先建立一个ActiveX Library。编译后即是个DLL文

件(进程内)。然后在同样的页面再建立一个COM Object。


实例模式与线程模式

接着你将看到如下向导,除了填写类名外(接口名会自动根据类名填充),还有实例创建方式(Instancing)和线程模式(Threading Model)

的选项。


实例模式决定客户端请求后,COM对象如何创建实例:

Internal:供COM对象内部使用,不会响应客户端请求,只能通过COM对象内部的其他

方法来建立;

Single Instance:不论当前系统内部是否存在相同COM对象,都会建立一个新的程序

及独立的对象实例;

Mulitple Instance:如果有多个相同的COM对象,只会建立一个程序,多个COM对象

的实例共享公共代码,并拥有自己的数据空间。

Single/ Mulitple Instance有各自的优点,Mulitple虽然节省了内存但更加费时。即Single模式需要更多的内存资源,而Mulitple模式需要更

多的CPU资源,且Single的实例响应请求的负荷较为平均。该参数应根据服务器的实际需求来考虑。


线程模式有五种:

Single:仅单线程,处理简单,吞吐量最低;

Apartment:COM程序多线程,COM对象处理请求单线程;

Free:一个COM对象的多个实例可以同时运行。吞吐量提高的同时,也要求对COM对象

进行必要的保护,以避免多个实例冲突;

Both:同时支持Aartment和Free两种线程模式。

Neutral:只能在COM+下使用。

虽然Free和Both的效率得到提高,但是要求较高的技巧以避免冲突(这是很不容易调试的),所以一般建议使用Delphi的缺省方式。


类型库编辑器(Type Library)

假设我们建立一个叫做TSample的类和ISample的接口(如图),然后使用类型库编辑器创建一个方法GetCOMInfo(在右边树部分点击右键弹出

菜单选择New-Method或者点击上方按钮),并于左边Parameters页面建立两个参数(ValInt : Integer , ValStr : String),返回值为BSTR

。如图:


可以看到,除了常用类型外,参数和返回值还可以支持很多指针、OLE对象、接口类型。建立普通的COM对象,其Returen Type是可以任意的,

这是和DCOM的一个区别。

双击Modifier列弹出窗口,可以选择参数的方式:in、out分别对应const、out定义,选择Has Default Value可设置参数缺省值。


Delphi生成代码详解


点击刷新按钮刷新后,上面类型库编辑器对应的Delphi自动生成的代码如下:

unit uCOM;

{$WARN SYMBOL_PLATFORM OFF}

 

interface

 

uses

  Windows, ActiveX, Classes, ComObj, pCOM_TLB, StdVcl;

 

type

  TSample = class(TTypedComObject, ISample)

  protected

    function GetCOMInfo(ValInt: SYSINT; const ValStr: WideString): WideString;

      stdcall;

  end;

 

implementation

 

uses ComServ;

 

function TSample.GetCOMInfo(ValInt: SYSINT;const ValStr: WideString): WideString;

begin

end;

 

initialization

  TTypedComObjectFactory.Create(ComServer, TSample, Class_Sample,

    ciMultiInstance, tmApartment);

end.

 

引用单元

有三个特殊的单元被引用:ComObj,ComServ和pCOM_TLB。ComObj里定义了COM接口类的父类TTypedComObject和类工厂类

TTypedComObjectFactory(分别从TComObject和TComObjectFactory继承,早期版本如Delphi4建立的COM,就直接从TcomObject继承和使用

TComObjectFactory了); ComServ单元里面定义了全局变量ComServer: TComServer,它是从TComServerObject继承的,关于这个变量的作用

,后面将会提到。

这几个类都是delphi实现COM对象的比较基础的类,TComObject(COM对象类)和TComObjectFactory(COM对象类工厂类)本身就是IUnknown的

两个实现类,包含了一个COM对象的建立、查询、登记、注册等方面的代码。TComServerObject则用来注册一个COM对象的服务信息。

 

 

接口定义说明

再看接口类定义TSample = class(TTypedComObject, ISample)。到这里,已经可以通过涉及的父类的作用大致猜测到TSample是如何创建并注

册为一个标准的COM对象的了。那么接口ISample又是怎么来的呢?pCOM_TLB单元是系统自动建立的,其名称加上了_TLB,它里面包含了ISample

= interface(IUnknown)的接口定义。前面提到过,所有COM接口都是从IUnknown继承的。

在这个单元里我们还可以看到三种ID(类型库ID、IID及COM注册所必须的CLSID)的定义:LIBID_pCOM,IID_ISample和CLASS_Sample。关键是

这时接口本身仅仅只有定义代码而没有任何的实现代码,那接口创建又是在何处执行的?_TLB单元里还有这样的代码:

CoSample = class

  class function Create: ISample;

  class function CreateRemote(const MachineName: string): ISample;

end;

 

class function CoSample.Create: ISample;

begin

  Result := CreateComObject(CLASS_Sample) as ISample;

end;

 

class function CoSample.CreateRemote(const MachineName: string): ISample;

begin

  Result := CreateRemoteComObject(MachineName, CLASS_Sample) as ISample;

end;

由Delphi的向导和类型编辑器帮助生成的接口定义代码,都会绑定一个“Co+类名”的类,它实现了创建接口实例的代码。CreateComObject和

CreateRemoteComObject函数在ComObj单元定义,它们就是使用CLSID创建COM/DCOM对象的函数!

 

初始化:注册COM对象的类工厂

类工厂负责接口类的统一管理——实际上是由支持IClassFactory接口的对象来管理的。类工厂类的继承关系如下:

IClassFactory = interface(IUnknown)

TComObjectFactory=class(TObject,IUnknown,IClassFactory,IClassFactory2) TTypedComObjectFactory = class(TComObjectFactory)

我们知道了接口ISample是怎样被创建的,接口实现类TSample又是如何被定义为COM对象的实现类。现在解释它是怎么被注册,以及何时创建的

。这一切的小把戏都在最后initialization的部分,这里有一条类工厂建立的语句。

Initialization是Delphi用于初始化的特殊部分,此部分的代码将在整个程序启动的时候首先执行。回顾前面的内容并观察一下

TTypedComObjectFactory的参数:ComServer是用于注册/撤消注册COM服务的对象,TSample是接口实现类,Class_Sample是接口唯一对应的

GUID,ciMultiInstance是实例模式,tmApartment是线程模式。一个COM对象应该具备的特征和要素都包含在了里面!

那么COM对象的管理又是怎么实现的呢?在ComObj单元里面可以见到一条定义function ComClassManager: TComClassManager;

这里TComClassManager顾名思义就是COM对象的管理类。任何一个祖先类为TComObjectFactory的对象被建立时,其Create里面会执行这样一句

ComClassManager.AddObjectFactory(Self);

AddObjectFactory方法的原形为procedure TComClassManager.AddObjectFactory(Factory: TComObjectFactory);相对应的还有

RemoveObjectFactory方法。具体的代码我就不贴出来了,相信大家已经猜测到了它的作用——将当前对象(self)加入到ComClassManager管

理的对象链(FFactoryList)中。

 

封装的秘密

读者应该还有最后一个疑问:假如服务器通过类工厂的注册以及GUID确定一个COM对象,那当客户端调用的时候,服务器是如何启动包含COM对

象的程序的呢?

当你建立ActiveX Library的工程的时候,将发现一个和普通DLL模版不同的地方——它定义了四个输出例程:

exports

  DllGetClassObject,

  DllCanUnloadNow,

  DllRegisterServer,

  DllUnregisterServer;

这四个例程并不是我们编写的,它们都在ComServ单元例实现。单元还定义了类TComServer,并且在初始化部分创建了类的实例,即前面提到过

的全局变量ComServer。

例程DllGetClassObject通过CLSID得到支持IClassFactory接口的对象;例程DllCanUnloadNow判断DLL是否可从内存卸载;DllRegisterServer

和DllUnregisterServer负责DLL的注册和解除注册,其具体的功能由ComServer实现。

 

接口类的具体实现

好了,现在自动生成代码的来龙去脉已经解释清楚了,下一步就是由我们来添加接口方法的实现代码。在function TSample.GetCOMInfo的部分

添加如下代码。我写的例子很简单,仅仅是根据传递的参数组织一条字符串并返回。以此证明接口正确调用并执行了该代码:

function TSample.GetCOMInfo(ValInt: SYSINT;const ValStr: WideString): WideString;

const

  Server1 = 1;  Server2 = 2;  Server3 = 3;

var

  s : string;

begin

  s := ''''This is COM server : '''';

  case ValInt of

    Server1: s := s + ''''Server1'''';

    Server2: s := s + ''''Server2'''';

    Server3: s := s + ''''Server3'''';

  end;

  s := s + #13 + #10 + ''''Execute client is '''' + ValStr;

  Result := s;

end;

 

注册、创建COM对象及调用接口

随便建立一个Application用于测试上面的COM。必要的代码很少,创建一个接口的实例然后执行它的方法。当然我们得先行注册COM,否则调用

根据CLSID找不接口的话,将报告“无法向注册表写入项”。如果接口定义不一致,则会报告“Interface not supported”。

编译上面的这个COM工程,然后选择菜单“Run – Register ActiveX Server”,或者通过Windows下system/system32目录中的regsvr32.exe程

序注册编译好的DLL文件。regsvr32的具体参数可以通过regsvr32/?来获得。对于进程外(EXE型)的COM对象,执行一次应用程序就注册了。

提示DLL注册成功后,就应该可以正确执行下列客户端程序了:

uses ComObj, pCOM_TLB;

 

procedure Ttest.Button1Click(Sender: TObject);

var

  COMSvr : ISample;

  retStr : string;

begin

  COMSvr := CreateComObject(CLASS_Sample) as ISample;

  if COMSvr <> nil then begin

    retStr := COMSvr.GetCOMInfo(2,''''client 2'''');

    showmessage(retStr);

    COMSvr := nil;

  end

  else showmessage(''''接口创建不成功'''');

end;

最终值是从当前程序外的一个“接口”返回的,我们甚至可以不知道这个接口的实现!第一次接触COM的人,成功执行此程序并弹出对话框后,

也许会体会到一种技术如斯奇妙的感觉,因为你仅仅调用了“接口”,就可以完成你猜测中的东西。

 

 

创建一个分布式DCOM(自动化接口)

 

IDispatch

在delphi6之前的版本中,所有接口的祖先都是IUnknown,后来为了避免跨平台操作中接口概念的模糊,又引入了IInterface接口。

使用向导生成DCOM的步骤和COM几乎一致。而生成的代码仅将接口类的父类换为TAutoObject,类工厂类换为TAutoObjectFactory。这其实没有

太大的不同,因为TAutoObject等于是一个标准COM外加IDispatch接口,而TAutoObjectFactory是从TTypedComObjectFactory直接继承的:

TAutoObject = class(TTypedComObject, IDispatch)

TAutoObjectFactory = class(TTypedComObjectFactory)

自动化服务器支持双重接口,而且必须实现IDispatch。因讨论范畴限制,本文只能简单提出,IDispatch是DCOM和COM技术实现上的一个重要区

别。打开_TLB.pas单元,可以找到Ixxx = interface(IDispatch)和Ixxx = dispinterface的定义,这在前面COM的例子里面是没有的。

 

创建过程中的差异

 

使用类型库编辑器的时候,有两处和COM不同的地方。首先Return Type必须选择HRESULT,否则会提示错误,这是为了满足双重接口的需要。当

Return Type选择HRESULT后,你会发现方法定义将变成procedure(过程)而不是预想中的function(函数)。

怎么才能让方法有返回值呢?还需要在Parameters最后多添加一个参数,然后将该参数改名与方法名一致,设置参数类型为指针(如果找不到

某种类型的指针类型,可以直接在类型后面加*,如图,BSTR*是BSTR的指针类型)。最后在Modifier列设置Parameter Flags为RetVal,同时

Out将被自动选中,而In将被取消。

 

刷新后,得到下列代码。添加方法的具体实现,大功告成:

  TSampleAuto = class(TAutoObject, ISampleAuto)

  protected

    function GetAutoSerInfo(ValInt: SYSINT;const ValStr: WideString): WideString; safecall;

  end;

 

远程接口调用

远程接口的调用需要使用CreateRemoteComObject函数,其它如接口的声明等等与COM接口调用相同。CreateRemoteComObject函数比

CreateComObject 多了一个参数,即服务器的计算机名称,这样就比COM多出了远程调用的查询能力。前面“接口定义说明”一节的代码可以对

照CreateComObject、CreateRemoteComObject的区别。

 

 

自定义COM的对象

 

接口一个重要的好处是:发布一个接口,可以不断更新其功能而不用升级客户端。因为不论应用升级还是业务改变,客户端的调用方式都是一

致的。

既然我们已经弄清楚Delphi是怎样实现一个接口的,那能否不使用向导,自己定义接口呢?这样做可以用一个接口继承出不同的接口实现类,

来完成不同的功能。同时也方便了小组开发、客户端开发、进程内/外同步编译以及调试。

 

接口单元:xxx_TLB.pas

前面略讲了接口的定义需要注意的方面。接口除了没有实例化外,它与普通类还有以下区别:接口中不能定义字段,所有属性的读写必须由方

法实现;接口没有构造和析构函数,所有成员都是public;接口内的方法不能定义为virtual,dynamic,abstract,override。

首先我们要建立一个接口。前面讲过接口的定义只存在于一个地方,即xxx_TLB.pas单元里面。使用类型库编辑器可以产生这样一个单元。还是

在新建项目的ActiveX页,选择最后一个图标(Type Library)打开类型库编辑器,按F12键就可以看到TLB文件(保存为.tlb)了。没有定义任

何接口的时候,TLB文件里除了一大段注释外只定义了LIBID(类型库的GUID)。假如关闭了类型库编辑器也没有关系,可以随时通过菜单View

– Type Library打开它。

先建立一个新接口(使用向导的话这步已经自动完成了),然后如前面操作一样建立方法、属性…生成的TLB文件内容与向导生成_TLB单元大致

相同,但仅有定义,缺乏“co+类名”之类的接口创建代码。

再观察代码,将发现接口是从IDispatch继承的,必须将这里的IDispatch改为IUnknown。保存将会得到.tlb文件,而我们想要的是一个单元

(.pas)文件,仅仅为了声明接口,所以把代码拷贝复制并保存到一个新的Unit。

 

自定义CLSID

从注册和调用部分可以看出CLSID的重要作用。CLSID是一个GUID(全局唯一接口表示符),用来标识对象。GUID是一个16个字节长的128位二进

制数据。Delphi声明一个GUID常量的语法是:

  Class_XXXXX : TGUID = ''''{xxxxxxxx-xxxxx-xxxxx-xxxxx-xxxxxxxx}'''';

在Delphi的编辑界面按Ctrl+Shift+G键可以自动生成等号后的数据串。GUID的声明并不一定在_TLB单元里面,任何地方都可以声明并引用它。

 

接口类声明与实现

新建一个ActiveX Library工程,加入刚才定义的TLB单元,再新建一个Unit。我的TLB单元取名为MyDef_TLB.pas,定义了一个接口

IMyInterface = interface(IUnknown),以及一个方法function SampleMethod(val: Smallint): SYSINT; safecall;现在让我们看看全部接口

类声明及实现的代码:

unit uMyDefCOM;

 

interface

 

uses

  ComObj, Comserv, ActiveX, MyDef_TLB;

 

const

  Class_MySvr : TGUID = ''''{1C0E5D5A-B824-44A4-AF6C-478363581D43}'''';

 

type

 

  TMyIClass = class(TComObject, IMyInterface)

    procedure Initialize; override;

    destructor Destroy; override;

  private

    FInitVal : word;

  public

    function  SampleMethod(val: Smallint): SYSINT; safecall;

  end;

 

  TMySvrFactory = class(TComObjectFactory)

    procedure UpdateRegistry(Register:Boolean);override;

  end;

 

implementation

 

{ TMyIClass }

 

procedure TMyIClass.Initialize;

begin

  inherited;

  FInitVal := 100;

end;

 

destructor TMyIClass.Destroy;

begin

  inherited;

end;

 

function TMyIClass.SampleMethod(val: Smallint): SYSINT;

begin

  Result := val + FInitVal;

end;

 

{ TMySvrFactory }

 

procedure TMySvrFactory.UpdateRegistry(Register: Boolean);

begin

  inherited;

 

  if Register then begin

    CreateRegKey(''''MyApp\''''+ClassName, ''''GUID'''', GUIDToString(Class_MySvr));

  end else begin

    DeleteRegKey(''''MyApp\''''+ClassName);

  end;

end;

 

initialization

 

  TMySvrFactory.Create(ComServer, TMyIClass, Class_MySvr,

      ''''MySvr'''', '''''''', ciMultiInstance, tmApartment);

 

end.

Class_MySvr是自定义的CLSID,TMyIClass是接口实现类,TMySvrFactory是类工厂类。

 

COM对象的初始化

procedure Initialize是接口的初始化过程,而不是常见的Create方法。当客户端创建接口后,将首先执行里面的代码,与Create的作用一样

。一个COM对象的生存周期内,难免需要初始化类成员或者设置变量的初值,所以经常需要重载这个过程。

相对应的,destructor Destroy则和类的标准析构过程一样,作用也相同。

 

类工厂注册

在代码的最后部分,假如使用TComObjectFactory来注册,就和前面所讲的完全一样了。我在这里刻意用类TMySvrFactory继承了一次,并且重

载了UpdateRegistry 方法,以便向注册表中写入额外的内容。这是种小技巧,希望大家根据本文的思路,摸清COM/DCOM对象的Delphi实现结构

后,可以举一反三。毕竟随心所欲的控制COM对象,能提供的功能远不如此。

 

(本文所有代码在Delphi6、Delphi7下编译执行通过)

全文完。

发表于 2005年05月27日 4:26 PM

posted on 2006-07-28 02:48  忘我  阅读(2150)  评论(1编辑  收藏  举报