zoukankan      html  css  js  c++  java
  • delphi组件读写机制

    一、流式对象(Stream)和读写对象(Filer)的介绍
     在面向对象程序设计中,对象式数据管理占有很重要的地位。在Delphi中,对对象式数据管
    理的支持方式是其一大特色。
     Delphi是一个面向对象的可视化设计与面向对象的语言相结合的集成开发环境。Delphi的核
    心是组件。组件是对象的一种。Delphi应用程序完全是由组件来构造的,因此开发高性能的
    Delphi应用程序必然会涉及对象式数据管理技术。

     对象式数据管理包括两方面的内容:
    ● 用对象来管理数据
    ● 对各类数据对象(包括对象和组件)的管理

     Delphi将对象式数据管理类归结为Stream对象(Stream)和Filer对象(Filer),并将它们应用
    于可视组件类库(VCL)的方方面面。它们提供了丰富的在内存、外存和Windows资源中管理
    对象的功能,
     Stream对象,又称流式对象,是TStream、THandleStream、TFileStream、TMemoryStream、
    TResourceStream和TBlobStream等的统称。它们分别代表了在各种媒介上存储数据的能力,
    它们将各种数据类型(包括对象和组件) 在内存、外存和数据库字段中的管理操作抽象为对象
    方法,并且充分利用了面向对象技术的优点,应用程序可以相当容易地在各种Stream对象中
    拷贝数据。
     读写对象(Filer)包括TFiler对象、TReader对象和TWriter对象。TFiler对象是文件读写
    的基础对象,在应用程序中使用的主要是TReader和TWriter。TReader和TWriter对象都直接
    从TFiler对象继承。TFiler对象定义了Filer对象的基本属性和方法。

      Filer对象主要完成两大功能:
    ● 存取窗体文件和窗体文件中的组件
    ● 提供数据缓冲,加快数据读写操作

     为了对流式对象和读写对象有一个感性的认识,先来看一个例子。
    a)写文件
    procedure TFomr1.WriteData (Sender: TObject); r;
    Var
      FileStream:TFilestream;
      Mywriter:TWriter;
      i: integer
    Begin
      FileStream:=TFilestream.create(‘c:Test.txt’,fmopenwrite);//创建文件流对象
      Mywriter:=TWriter.create(FileStream,1024); //把Mywriter和FileStream联系起来
      Mywriter. writelistbegin;  //写入列表开始标志
      For i:=0 to Memo1.lines.count-1 do   
        Mywriter.writestring(memo1.lines[i]); //保存Memo组件中文本信息到文件中
      Mywriter.writelistend;          //写入列表结束标志
      FileStream.seek(0,sofrombeginning); //文件流对象指针移到流起始位置
      Mywriter.free; //释放Mywriter对象
      FileStream.free; //释放FileStream对象
    End;
     
    b)读文件
    procedure TForm1.ReadData(Sender: TObject);
    Var
      FileStream:TFilestream;
      Myreader:TReader;
    Begin
      FileStream:=TFilestream.create(‘c:Test.txt’,fmopenread);
      Myreader:=TRreader.create(FileStream,1024); //把Myreader和FileStream联系起来
      Myreader.readlistbegin;  //把写入的列表开始标志读出来
      Memo1.lines.clear;  //清除Memo1组件的文本内容
      While not myreader.endoflist do //注意TReader的一个方法:endoflist
      Begin
        Memo1.lines.add(myreader.readstring); //把读出的字符串加到Memo1组件中
      End;
      Myreader.readlistend; //把写入的列表结束标志读出来
      Myreader.free;  //释放Myreader对象
      FileStream.free; //释放FileStream对象
    End;
     上面两个过程,一个为写过程,另一个为读过程。写过程通过TWriter,利用TFilestream把
    一个Memo中的内容(文本信息)存为一个保存在磁盘上的二进制文件。读过程刚好和写过程
    相反,通过TReader,利用TFilestream把二进制文件中的内容转换为文本信息并显示在Memo
    中。运行程序可以看到,读过程忠实的把写过程所保存的信息进行了还原。
     下图描述了数据对象(包括对象和组件)、流式对象和读写对象之间的关系。
     
     

     值得注意的是,读写对象如TFiler对象、TReader对象和TWriter对象等很少由应用程序编写
    者进行直接的调用,它通常用来读写组件的状态,它在读写组件机制中扮演着非常重要的角
    色。
    对于流式对象Stream,很多参考资料上都有很详细的介绍,而TFiler对象、TReader对象和T
    Writer对象特别是组件读写机制的参考资料则很少见,本文将通过对VCL原代码的跟踪而对组
    件读写机制进行剖析。

    二、读写对象(Filer)与组件读写机制
     Filer对象主要用于存取Delphi的窗体文件和窗体文件中的组件,所以要清楚地理解Filer对
    象就要清楚Delphi 窗体文件(DFM文件)的结构。
      DFM文件是用于Delphi存储窗体的。窗体是Delphi可视化程序设计的核心。窗体对应Del
    phi应用程序中的窗口,窗体中的可视组件对应窗口中的界面元素,非可视组件如TTimer和T
    OpenDialog,对应Delphi应用程序的某项功能。Delphi应用程序的设计实际上是以窗体的设
    计为中心。因此,DFM文件在Delphi应用设计中也占很重要的位置。窗体中的所有元素包括窗
    体自身的属性都包含在DFM文件中。
      在Delphi应用程序窗口中,界面元素是按拥有关系相互联系的,因此树状结构是最自然
    的表达形式;相应地,窗体中的组件也是按树状结构组织;对应在DFM文件中,也要表达这种
    关系。DFM文件在物理上,是以文本方式存储的(在Delphi2.0版本以前是存储为二进制文件
    的),在逻辑上则是以树状结构安排各组件的关系。从该文本中可以看清窗体的树状结构。
    下面是DFM文件的内容:
    object Form1: TForm1
      Left = 197
      Top = 124
      ……
      PixelsPerInch = 96
      TextHeight = 13
      object Button1: TButton
        Left = 272
        ……
        Caption = 'Button1'
        TabOrder = 0
      end
      object Panel1: TPanel
        Left = 120
        ……
        Caption = 'Panel1'
        TabOrder = 1
        object CheckBox1: TCheckBox
          Left = 104
          ……
       Caption = 'CheckBox1'
          TabOrder = 0
        end
      end
    end
     这个DFM文件就是TWriter通过流式对象Stream来生成的,当然这里还有一个二进制文件到文
    本信息文件的转换过程,这个转换过程不是本文要研究的对象,所以忽略这样的一个过程。
     在程序开始运行的时候,TReader通过流式对象Stream来读取窗体及组件,因为Delphi在编
    译程序的时候,利用编译指令{$R *.dfm}已经把DFM文件信息编译到可执行文件中,因此TRe
    ader读取的内容实际上是被编译到可执行文件中的有关窗体和组件的信息。
     TReader和TWriter不仅能够读取和写入Object Pascal中绝大部分标准数据类型,而且能够
    读写List、Variant等高级类型,甚至能够读写Perperties和Component。不过,TReader、T
    Writer自身实际上提供的功能很有限,大部分实际的工作是由TStream这个非常强大的类来完
    成的。也就是说TReader、TWriter实际上只是一个工具,它只是负责怎么去读写组件,至于
    具体的读写操作是由TStream来完成的。
     由于TFiler是TReader和TWriter的公共祖先类,因为要了解TReader和TWriter,还是先从T
    Filer开始。


    TFiler

           先来看一下TFiler类的定义:

      TFiler = class(TObject)
      private
        FStream: TStream;
        FBuffer: Pointer;
        FBufSize: Integer;
        FBufPos: Integer;
        FBufEnd: Integer;
        FRoot: TComponent;
        FLookupRoot: TComponent;
        FAncestor: TPersistent;
        FIgnoreChildren: Boolean;
      protected
        procedure SetRoot(Value: TComponent); virtual;
      public
        constructor Create(Stream: TStream; BufSize: Integer);
        destructor Destroy; override;
        procedure DefineProperty(const Name: string;
          ReadData: TReaderProc; WriteData: TWriterProc;
          HasData: Boolean); virtual; abstract;
        procedure DefineBinaryProperty(const Name: string;
          ReadData, WriteData: TStreamProc;
          HasData: Boolean); virtual; abstract;
        procedure FlushBuffer; virtual; abstract;
        property Root: TComponent read FRoot write SetRoot;
        property LookupRoot: TComponent read FLookupRoot;
        property Ancestor: TPersistent read FAncestor write FAncestor;
        property IgnoreChildren: Boolean read FIgnoreChildren write FIgnoreChildren;
      end;

           TFiler对象是TReader和TWriter的抽象类,定义了用于组件存储的基本属性和方法。
    它定义了Root属性,Root指明了所读或写的组件的根对象,它的Create方法将Stream对象作
    为传入参数以建立与Stream对象的联系, Filer对象的具体读写操作都是由Stream对象完成
    。因此,只要是Stream对象所能访问的媒介都能由Filer对象存取组件。

           TFiler 对象还提供了两个定义属性的public方法:DefineProperty和DefineBinary
    Property,这两个方法使对象能读写不在组件published部分定义的属性。下面重点介绍一下
    这两个方法。

           Defineproperty()方法用于使标准数据类型持久化,诸如字符串、整数、布尔、字
    符、浮点和枚举。

           在Defineproperty方法中。Name参数用于指定应写入DFM文件的属性的名称,该属性
    不在类的published部分定义。

           ReadData和WriteData参数指定在存取对象时读和写所需数据的方法。ReadData参数
    和WriteData参数的类型分别是TReaderProc和TWriterProc。这两个类型是这样声明的:

      TReaderProc = procedure(Reader: TReader) of object;

      TWriterProc = procedure(Writer: TWriter) of object;

           HasData参数在运行时决定了属性是否有数据要存储。

           DefineBinaryProperty方法和Defineproperty有很多的相同之处,它用来存储二进制
    数据,如声音和图象等。

           下面来说明一下这两个方法的用途。

           我们在窗体上放一个非可视化组件如TTimer,重新打开窗体时我们发现TTimer还是在
    原来的地方,但TTimer没有Left和Top属性啊,那么它的位置信息保存在哪里呢?

           打开该窗体的DFM文件,可以看到有类似如下的几行内容:

      object Timer1: TTimer
        Left = 184
        Top = 149
      end

    Delphi的流系统只能保存published数据,但TTimer并没有published的Left和Top属性,那么
    这些数据是怎么被保存下来的呢?

    TTimer是TComponent的派生类,在TComponent类中我们发现有这样的一个函数:

    procedure TComponent.DefineProperties(Filer: TFiler);
    var
      Ancestor: TComponent;
      Info: Longint;
    begin
      Info := 0;
      Ancestor := TComponent(Filer.Ancestor);
      if Ancestor <> nil then Info := Ancestor.FDesignInfo;
      Filer.DefineProperty('Left', ReadLeft, WriteLeft,
        LongRec(FDesignInfo).Lo <> LongRec(Info).Lo);
      Filer.DefineProperty('Top', ReadTop, WriteTop,
        LongRec(FDesignInfo).Hi <> LongRec(Info).Hi);
    end;
           TComponent的DefineProperties是覆盖了它的祖先类TPersistent的方法,在TPersi
    stent类中该方法为空的虚方法。

           在DefineProperties方法中,我们可以看出,有一个Filer对象作为它的参数,当定
    义属性时,它引用了Ancestor属性,如果该属性非空,对象应当只读写与从Ancestor继承的
    不同的属性的值。它调用TFiler的DefineProperty方法,并定义了ReadLeft,WriteLeft,R
    eadTop,WriteTop方法来读写Left和Top属性。

           因此,凡是从TComponent派生的组件,即使它没有Left和Top属性,在流化到DFM文件
    中,都会存在这样的两个属性。

           在查找资料的过程中,发现很少有资料涉及到组件读写机制的。由于组件的写过程是
    在设计阶段由Delphi的IDE来完成的,因此无法跟踪它的运行过程。所以笔者是通过在程序运
    行过程中跟踪VCL原代码来了解组件的读机制的,又通过读机制和TWriter来分析组件的写机
    制。所以下文将按照这一思维过程来讲述组件读写机制,先讲TReader,而后是TWriter。


    TReader

           先来看Delphi的工程文件,会发现类似这样的几行代码:

    begin
      Application.Initialize;
      Application.CreateForm(TForm1, Form1);
      Application.Run;
    end.

           这是Delphi程序的入口。简单的说一下这几行代码的意义:Application.Initializ
    e对开始运行的应用程序进行一些必要的初始化工作,Application.CreateForm(TForm1, Fo
    rm1)创建必要的窗体,Application.Run程序开始运行,进入消息循环。

           现在我们最关心的是创建窗体这一句。窗体以及窗体上的组件是怎么创建出来的呢?
    在前面已经提到过:窗体中的所有组件包括窗体自身的属性都包含在DFM文件中,而Delphi在
    编译程序的时候,利用编译指令{$R *.dfm}已经把DFM文件信息编译到可执行文件中。因此,
    可以断定创建窗体的时候需要去读取DFM信息,用什么去读呢,当然是TReader了!

           通过对程序的一步步的跟踪,可以发现程序在创建窗体的过程中调用了TReader的Re
    adRootComponent方法。该方法的作用是读出根组件及其所拥有的全部组件。来看一下该方法
    的实现:

    function TReader.ReadRootComponent(Root: TComponent): TComponent;

    ……

    begin
      ReadSignature;
      Result := nil;
      GlobalNameSpace.BeginWrite;  // Loading from stream adds to name space
      try
        try
          ReadPrefix(Flags, I);
          if Root = nil then
          begin
            Result := TComponentClass(FindClass(ReadStr)).Create(nil);
            Result.Name := ReadStr;
          end else
          begin
            Result := Root;
            ReadStr; { Ignore class name }
            if csDesigning in Result.ComponentState then
              ReadStr else
            begin
              Include(Result.FComponentState, csLoading);
              Include(Result.FComponentState, csReading);
              Result.Name := FindUniqueName(ReadStr);
            end;
          end;
          FRoot := Result;
          FFinder := TClassFinder.Create(TPersistentClass(Result.ClassType), True);
          try
            FLookupRoot := Result;
            G := GlobalLoaded;
            if G <> nil then
              FLoaded := G else
              FLoaded := TList.Create;
            try
              if FLoaded.IndexOf(FRoot) < 0 then
                FLoaded.Add(FRoot);
              FOwner := FRoot;
              Include(FRoot.FComponentState, csLoading);
              Include(FRoot.FComponentState, csReading);
              FRoot.ReadState(Self);
              Exclude(FRoot.FComponentState, csReading);
              if G = nil then
                for I := 0 to FLoaded.Count - 1 do TComponent(FLoaded[I]).Loaded;
            finally
              if G = nil then FLoaded.Free;
              FLoaded := nil;
            end;
          finally
            FFinder.Free;
          end;

         ……

      finally
        GlobalNameSpace.EndWrite;
      end;
    end;

           ReadRootComponent首先调用ReadSignature读取Filer对象标签(’TPF0’)。载入
    对象之前检测标签,能防止疏忽大意,导致读取无效或过时的数据。

           再看一下ReadPrefix(Flags, I)这一句,ReadPrefix方法的功能与ReadSignature的
    很相象,只不过它是读取流中组件前面的标志(PreFix)。当一个Write对象将组件写入流中时
    ,它在组件前面预写了两个值,第一个值是指明组件是否是从祖先窗体中继承的窗体和它在
    窗体中的位置是否重要的标志;第二个值指明它在祖先窗体创建次序。

           然后,如果Root参数为nil,则用ReadStr读出的类名创建新组件,并从流中读出组件
    的Name属性;否则,忽略类名,并判断Name属性的唯一性。

              FRoot.ReadState(Self);

           这是很关键的一句,ReadState方法读取根组件的属性和其拥有的组件。这个ReadSt
    ate方法虽然是TComponent的方法,但进一步的跟踪就可以发现,它实际上最终还是定位到了
    TReader的ReadDataInner方法,该方法的实现如下:

    procedure TReader.ReadDataInner(Instance: TComponent);
    var
      OldParent, OldOwner: TComponent;
    begin
      while not EndOfList do ReadProperty(Instance);
      ReadListEnd;
      OldParent := Parent;
      OldOwner := Owner;
      Parent := Instance.GetChildParent;
      try
        Owner := Instance.GetChildOwner;
        if not Assigned(Owner) then Owner := Root;
        while not EndOfList do ReadComponent(nil);
        ReadListEnd;
      finally
        Parent := OldParent;
        Owner := OldOwner;
      end;
    end;
           其中有这样的这一行代码:

      while not EndOfList do ReadProperty(Instance);

           这是用来读取根组件的属性的,对于属性,前面提到过,既有组件本身的published
    属性,也有非published属性,例如TTimer的Left和Top。对于这两种不同的属性,应该有两
    种不同的读方法,为了验证这个想法,我们来看一下ReadProperty方法的实现。

    procedure TReader.ReadProperty(AInstance: TPersistent);

    ……

    begin

           ……

          PropInfo := GetPropInfo(Instance.ClassInfo, FPropName);
          if PropInfo <> nil then ReadPropValue(Instance, PropInfo) else
          begin
            { Cannot reliably recover from an error in a defined property }
            FCanHandleExcepts := False;
            Instance.DefineProperties(Self);
            FCanHandleExcepts := True;
            if FPropName <> '' then
              PropertyError(FPropName);
          end;

           ……

    end;

           为了节省篇幅,省略了一些代码,这里说明一下:FPropName是从文件读取到的属性名。

          PropInfo := GetPropInfo(Instance.ClassInfo, FPropName);

           这一句代码是获得published属性FPropName的信息。从接下来的代码中可以看到,如
    果属性信息不为空,就通过ReadPropValue方法读取属性值,而ReadPropValue方法是通过RT
    TI函数来读取属性值的,这里不再详细介绍。如果属性信息为空,说明属性FPropName为非p
    ublished的,它就必须通过另外一种机制去读取。这就是前面提到的DefineProperties方法
    ,如下:

           Instance.DefineProperties(Self);

           该方法实际上调用的是TReader的DefineProperty方法:

    procedure TReader.DefineProperty(const Name: string;
      ReadData: TReaderProc; WriteData: TWriterProc; HasData: Boolean);
    begin
      if SameText(Name, FPropName) and Assigned(ReadData) then
      begin
        ReadData(Self);
        FPropName := '';
      end;
    end;

           它先去比较读取的属性名是否和预设的属性名相同,如果相同并且读方法ReadData不
    为空时就调用ReadData方法读取属性值。

           好了,根组件已经读上来了,接下来应该是读该根组件所拥有的组件了。再来看方法:

    procedure TReader.ReadDataInner(Instance: TComponent);

           该方法后面有一句这样的代码:

        while not EndOfList do ReadComponent(nil);

           这正是用来读取子组件的。子组件的读取机制是和上面所介绍的根组件的读取一样的
    ,这是一个树的深度遍历。

           到这里为止,组件的读机制已经介绍完了。

           再来看组件的写机制。当我们在窗体上添加一个组件时,它的相关的属性就会保存在
    DFM文件中,这个过程就是由TWriter来完成的。

    ?        TWriter

           TWriter 对象是可实例化的往流中写数据的Filer对象。TWriter对象直接从TFiler继
    承而来,除了覆盖从TFiler继承的方法外,还增加了大量的关于写各种数据类型(如Integer
    、String和Component等)的方法。

           TWriter对象提供了许多往流中写各种类型数据的方法, TWrite对象往流中写数据是
    依据不同的数据采取不同的格式的。 因此要掌握TWriter对象的实现和应用方法,必须了解
    Writer对象存储数据的格式。

      首先要说明的是,每个Filer对象的流中都包含有Filer对象标签。该标签占四个字节其
    值为“TPF0”。Filer对象为WriteSignature和ReadSignature方法存取该标签。该标签主要
    用于Reader对象读数据(组件等)时,指导读操作。

      其次,Writer对象在存储数据前都要留一个字节的标志位,以指出后面存放的是什么类
    型的数据。该字节为TValueType类型的值。TValueType是枚举类型,占一个字节空间,其定
    义如下:

     

      TValueType = (VaNull, VaList, VaInt8, VaInt16, VaInt32, VaEntended, VaString, VaIdent,

    VaFalse, VaTrue, VaBinary, VaSet, VaLString, VaNil, VaCollection);

     

           因此,对Writer对象的每一个写数据方法,在实现上,都要先写标志位再写相应的数
    据;而Reader对象的每一个读数据方法都要先读标志位进行判断,如果符合就读数据,否则产
    生一个读数据无效的异常事件。VaList标志有着特殊的用途,它是用来标识后面将有一连串
    类型相同的项目,而标识连续项目结束的标志是VaNull。因此,在Writer对象写连续若干个
    相同项目时,先用WriteListBegin写入VaList标志,写完数据项目后,再写出VaNull标志;
    而读这些数据时,以ReadListBegin开始,ReadListEnd结束,中间用EndofList函数判断是否
    有VaNull标志。

           来看一下TWriter的一个非常重要的方法WriteData:

    procedure TWriter.WriteData(Instance: TComponent);

    ……

    begin

      ……

      WritePrefix(Flags, FChildPos);
      if UseQualifiedNames then
        WriteStr(GetTypeData(PTypeInfo(Instance.ClassType.ClassInfo)).UnitName + '.' + Instance.ClassName)
      else
        WriteStr(Instance.ClassName);
      WriteStr(Instance.Name);
      PropertiesPosition := Position;
      if (FAncestorList <> nil) and (FAncestorPos < FAncestorList.Count) then
      begin
        if Ancestor <> nil then Inc(FAncestorPos);
        Inc(FChildPos);
      end;
      WriteProperties(Instance);
      WriteListEnd;
     
      ……

    end;

           从WriteData方法中我们可以看出生成DFM文件信息的概貌。先写入组件前面的标志(
    PreFix),然后写入类名、实例名。紧接着有这样的一条语句:

      WriteProperties(Instance);

           这是用来写组件的属性的。前面提到过,在DFM文件中,既有published属性,又有非
    published属性,这两种属性的写入方法应该是不一样的。来看WriteProperties的实现:

    procedure TWriter.WriteProperties(Instance: TPersistent);

    ……

    begin
      Count := GetTypeData(Instance.ClassInfo)^.PropCount;
      if Count > 0 then
      begin
        GetMem(PropList, Count * SizeOf(Pointer));
        try
          GetPropInfos(Instance.ClassInfo, PropList);
          for I := 0 to Count - 1 do
          begin
            PropInfo := PropList^[I];
            if PropInfo = nil then
              Break;
            if IsStoredProp(Instance, PropInfo) then
              WriteProperty(Instance, PropInfo);
          end;
        finally
          FreeMem(PropList, Count * SizeOf(Pointer));
        end;
      end;
      Instance.DefineProperties(Self);
    end;

           请看下面的代码:

            if IsStoredProp(Instance, PropInfo) then

              WriteProperty(Instance, PropInfo);

           函数IsStoredProp通过存储限定符来判断该属性是否需要保存,如需保存,就调用W
    riteProperty来保存属性,而WriteProperty是通过一系列的RTTI函数来实现的。

           Published属性保存完后就要保存非published属性了,这是通过这句代码完成的:

      Instance.DefineProperties(Self);

           DefineProperties的实现前面已经讲过了,TTimer的Left、Top属性就是通过它来保存的。

           好,到目前为止还存在这样的一个疑问:根组件所拥有的子组件是怎么保存的?再来
    看WriteData方法(该方法在前面提到过):

    procedure TWriter.WriteData(Instance: TComponent);

    ……

    begin

      ……

        if not IgnoreChildren then
          try
            if (FAncestor <> nil) and (FAncestor is TComponent) then
            begin
              if (FAncestor is TComponent) and (csInline in TComponent(FAncestor).ComponentState) then
                FRootAncestor := TComponent(FAncestor);
              FAncestorList := TList.Create;
              TComponent(FAncestor).GetChildren(AddAncestor, FRootAncestor);
            end;
            if csInline in Instance.ComponentState then
              FRoot := Instance;
            Instance.GetChildren(WriteComponent, FRoot);
          finally
            FAncestorList.Free;
          end;
    end;

           IgnoreChildren属性使一个Writer对象存储组件时可以不存储该组件拥有的子组件。
    如果IgnoreChildren属性为True,则Writer对象存储组件时不存它拥有的子组件。否则就要
    存储子组件。

            Instance.GetChildren(WriteComponent, FRoot);

           这是写子组件的最关键的一句,它把WriteComponent方法作为回调函数,按照深度优
    先遍历树的原则,如果根组件FRoot存在子组件,则用WriteComponent来保存它的子组件。这
    样我们在DFM文件中看到的是树状的组件结构。

  • 相关阅读:
    关于datatable的一些操作以及使用adapter对数据的操作
    P1083 借教室
    P2264 情书
    P1772 [ZJOI2006]物流运输
    P1353 [USACO08JAN]跑步Running
    P2903 [USACO08MAR]麻烦的干草打包机The Loathesome Hay Baler
    P2895 [USACO08FEB]流星雨Meteor Shower
    P2665 [USACO08FEB]连线游戏Game of Lines
    P2896 [USACO08FEB]一起吃饭Eating Together
    P2925 [USACO08DEC]干草出售Hay For Sale
  • 原文地址:https://www.cnblogs.com/spiritofcloud/p/3898383.html
Copyright © 2011-2022 走看看