zoukankan      html  css  js  c++  java
  • 开发新的VCL 组件 -3

    开发新的VCL 组件 -3 

    16.10.3 增加图形能力
    声明了图形组件并发布了所有需要访问的继承属性之后,就可以给组件增加图形功能。另外,在
    本例中,还需要增加一些能使应用程序员在设计时定制Shape 外观的属性。
    1.决定画的内容
    图形组件可以根据动态条件(包括用户输入)改变它的外观,如果只需要静态图形,可以直接导
    出图形,而不需要使用控件。
    通常,图形控件的外观取决于它的一些属性的结合,例如Gauge 属性控制具有决定其形状、方向
    和是否使用图形显示它的进度的能力。同样,Shape 控件也应有决定绘制哪种形状的属性,在Shape
    控件中增加一个名为Shape 的属性。
    (1)声明属性类型
    当声明一个用户自定义类型的属性时,必须在类包含该属性之前声明类型。最常见的用于属性的
    自定义类型是枚举类型。
    对Shape 控件来说,需要声明一个该控件能绘制的形状的枚举类型。下面是枚举类型的声明:
    type
    TSampleShapeType = (sstRectangle, sstSquare, sstRoundRect, sstRoundSquare,
    sstEllipse, sstCircle);
    TSampleShape = class(TGraphicControl) { this is already there }
    现在可以用该类型在类中来声明新属性。
    (2)声明属性
    当声明一个属性时,通常需要声明一个private 域来保存属性值,然后定义读写属性值的方法。通
    常可以不用声明读方法,只是指定读取存储的数据。
    本例将声明一个域保存当前形状,然后声明一个通过方法调用来读写该域值的属性。
    在TSampleShape 中加入以下声明:
    type
    TSampleShape = class(TGraphicControl)
    private
    FShape: TSampleShapeType; { field to hold property value }
    procedure SetShape(Value: TSampleShapeType);
    published
    property Shape: TSampleShapeType read FShape write SetShape;
    end;
    现在,只需要加入SetShape 的实现部分了。
    (3)编写实现方法
    SetShape 方法的实现代码如下:
    procedure TSampleShape.SetShape(Value: TSampleShapeType);
    begin

    ·464·
    if FShape <> Value then {如果不变则忽略}
    begin
    FShape := Value; {存储新值}
    Invalidate; {强制重新绘制形状}
    end;
    end;
    2.重载构造和析构方法
    为了改变默认属性值和初始化组件拥有的类,需要重载继承的构造和析构方法。在新的构造或析
    构方法中必须调用继承的构造或析构方法。
    (1)改变默认属性值
    图形控件的默认尺寸非常小,因此需要在构造方法中改变高度和宽度。本例中设置Shape 控件的
    默认宽和高都为65 像素。
    *在组件类的声明中加入重载的构造方法,实现代码如下:
    type
    TSampleShape = class(TGraphicControl)
    public {构造方法必须是public}
    constructor Create(AOwner: TComponent); override {记住override 指令}
    end;
    *用新的默认值重新声明属性Height 和Width,实现代码如下:
    type
    TSampleShape = class(TGraphicControl)
    ...
    published
    property Height default 65;
    property Width default 65;
    end;
    *在单元的实现部分编写新的构造方法,实现代码如下:
    constructor TSampleShape.Create(AOwner: TComponent);
    begin
    inherited Create(AOwner); {首先必须调用继承的构造方法}
    Width := 65;
    Height := 65;
    end;
    (2)发布Pen 和Brush
    在默认情况下,一个Canvas 具有一个细黑笔和实心白刷。为了使应用程序员在使用Shape 控件时
    能改变Pen 和Brush,必须在设计时提供这些类,然后在绘制时将这些对象拷贝到Canvas 中。像这样
    附属的Pen 或Brush 类被称为拥有的对象(Owned Classes),组件有责任创建和销毁它们。
    ① 声明Owned 对象域
    组件拥有的每一个对象必须有对象域的声明,该域保证组件肯定存在指向拥有对象的指针,以便
    组件在销毁自己之前销毁拥有对象。通常组件在构造方法中中创建它,在析构方法中撤消它。
    Owned 对象的域总是声明为private 的,如果要使应用程序(或其他组件)访问拥有的对象,通常
    要提供published 或public 属性。下面的代码声明了Pen 和Brush 的对象域:
    type
    TSampleShape = class(TGraphicControl)
    第16 章 开发新的VCL 组件
    ·465·
    private {域基本上都是private }
    FPen: TPen; {pen 对象域}
    FBrush: TBrush; {brush 对象域}
    ...
    end;
    ② 声明访问属性
    可以通过声明与拥有对象相同类型的属性来提供对拥有对象的访问能力,这给应用程序员提供在
    设计时或运行时访问对象的途径。通常属性的读部分只是对象域的引用,但是写部分必须调用方法以
    使组件对拥有对象的变化作出反应。
    对于Shape 控件,需要加入属性提供对Pen 和Brush 的访问,同时需要声明对Pen 或Brush 变化
    作出反应的方法。代码如下:
    type
    TSampleShape = class(TGraphicControl)
    ...
    private {这些方法应该是private }
    procedure SetBrush(Value: TBrush);
    procedure SetPen(Value: TPen);
    published {使属性在设计时可用}
    property Brush: TBrush read FBrush write SetBrush;
    property Pen: TPen read FPen write SetPen;
    end;
    然后在单元的实现部分编写SetBrush 和SetPen 方法,代码如下
    procedure TSampleShape.SetBrush(Value: TBrush);
    begin
    FBrush.Assign(Value); {使用参数替换现存的Brush}
    end;
    procedure TSampleShape.SetPen(Value: TPen);
    begin
    FPen.Assign(Value); {使用参数替换现存的Pen}
    end;
    如果使用下面代码直接指定FBrush 的值:
    FBrush := Value;
    ...
    将会重写FBrush 的内部指针,导致内存泄漏。
    ③ 初始化拥有的对象
    组件中增加了的新对象,必须在组件构造方法中初始化,这样用户才能在运行时与对象交互。相
    应地,组件的析构方法必须在销毁自身之前销毁拥有的对象。
    因为Shape 控件中加入了Pen 和Brush 对象,所以要在构造方法中要对它们进行初始化,并在析
    构方法中撤消它们。
    *在Shape 控件的构造方法中创建Pen 和Brush,代码如下:
    constructor TSampleShape.Create(AOwner: TComponent);
    begin
    inherited Create(AOwner); {必须首先调用继承的构造方法}
    Width := 65;
    Height := 65;

    ·466·
    FPen := TPen.Create; {构造Pen}
    FBrush := TBrush.Create; {构造Brush}
    end;
    *在组件类声明中加入重载的析构方法,代码如下:
    type
    TSampleShape = class(TGraphicControl)
    public {析构方法必须是public}
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override; {记住override 指令}
    end;
    *在单元的实现部分编写新的析构方法,代码如下:
    destructor TSampleShape.Destroy;
    begin
    FPen.Free; {销毁Pen 对象}
    FBrush.Free; {销毁Brush 对象}
    inherited Destroy; {必须调用继承的析构方法}
    end;
    ④ 设置拥有对象的属性
    处理Pen 和Brush 对象的最后一步是处理Pen 和Brush 发生改变时对Shape 控件的重画问题。Pen
    和Brush 对象都有OnChange 事件,因此能够在Shape 控件中创建一个方法,使OnChange 事件指向该
    方法。下面代码给Shape 控件增加了该方法,并更新了组件的构造方法以使Pen 和Brush 事件指向新
    方法:
    type
    TSampleShape = class(TGraphicControl)
    published
    procedure StyleChanged(Sender: TObject);
    end;
    ...
    implementation
    ...
    constructor TSampleShape.Create(AOwner: TComponent);
    begin
    inherited Create(AOwner); {必须调用继承的构造方法}
    Width := 65;
    Height := 65;
    FPen := TPen.Create; {构造Pen}
    FPen.OnChange := StyleChanged; {指定OnChange 事件的方法}
    FBrush := TBrush.Create; {构造Brush}
    FBrush.OnChange := StyleChanged; {指定OnChange 事件的方法}
    end;
    procedure TSampleShape.StyleChanged(Sender: TObject);
    begin
    Invalidate; {删除并重画组件}
    end;
    经过这些工作,组件将会重画以响应Pen 或Brush 的改变。
    第16 章 开发新的VCL 组件
    ·467·
    3.绘制组件图形
    图形控件的基本要素是在屏幕上绘制图形的方法。抽象类TGraphicControl 定义了名为Paint 的虚
    方法,可以在组件中重载该方法来绘制需要的图形。
    Shape 控件的Paint 方法需要完成以下功能:
    *使用用户选择的Pen 和Brush。
    *使用所选的形状。
    *调整坐标以便方形和圆可以使用相同的Width 和Height。
    重载Paint 方法需要如下两个步骤:
    *在组件声明中增加Paint 方法的声明。
    *在单元的implementation 部分实现Paint 方法。
    本例中Paint 方法的声明如下:
    type
    TSampleShape = class(TGraphicControl)
    ...
    protected
    procedure Paint; override;
    ...
    end;
    然后实现Paint 方法,代码如下:
    procedure TSampleShape.Paint;
    begin
    with Canvas do
    begin
    Pen := FPen; {拷贝组件的Pen }
    Brush := FBrush; {拷贝组件的Brush }
    case FShape of
    sstRectangle, sstSquare:
    Rectangle(0, 0, Width, Height);
    sstRoundRect, sstRoundSquare:
    RoundRect(0, 0, Width, Height, Width div 4, Height div 4);
    sstCircle, sstEllipse:
    Ellipse(0, 0, Width, Height);
    end;
    end;
    end;
    只要控件需要更新图形,就需要调用Paint。当控件第1 次出现或当控件前面的窗口消失时,
    Windows 会通知控件绘制自己,也可以通过调用Invalidate 方法强制重画,与StyleChanged 方法相同。
    4.修改Shape 的图形绘制
    标准的Shape 控制比现在的SampleShape 功能强大的一点在于,它可以处理正方形和圆形以及矩
    形和椭圆。为实现这一点,需要编写代码查找最短的边以使图形处于中心。
    这是修改过的调整矩形和椭圆的Paint 方法,代码如下:
    procedure TSampleShape.Paint;
    var
    X, Y, W, H, S: Integer;

    ·468·
    begin with Canvas do
    begin
    Pen := FPen; {拷贝组件的Pen }
    Brush := FBrush; {拷贝组件的Brush }
    W := Width; {使用组件宽度}
    H := Height; {使用组件高度}
    if W < H then S := W else S := H; {保存最短的边}
    case FShape of {调整高度、宽度和位置}
    sstRectangle, sstRoundRect, sstEllipse:
    begin
    X := 0; {原始位置是图形左上}
    Y := 0;
    end;
    sstSquare, sstRoundSquare, sstCircle:
    begin
    X := (W - S) div 2; {水平方向处于中心}
    Y := (H - S) div 2; {然后是垂直方向}
    W := S; {宽度使用最小值}
    H := S; {高度也是}
    end;
    end;
    case FShape of
    sstRectangle, sstSquare:
    Rectangle(X, Y, X + W, Y + H);
    sstRoundRect, sstRoundSquare:
    RoundRect(X, Y, X + W, Y + H, S div 4, S div 4);
    sstCircle, sstEllipse:
    Ellipse(X, Y, X + W, Y + H);
    end;
    end;
    end;
    16.11 开发窗口控件:制作Calendar组件
    组件库提供了一个可以作为派生组件基类的抽象类。其中最重要的是Grid 和ListBox。下面介绍
    一个从Grid 的基类TCustomGrid 开发一个日历的例子。Calender 组件提供了一个月历,用户可以使用
    键盘或鼠标在月历中任意设置日期。
    在VCL 应用程序中,最后生成的组件与组件面板Samples 页中的TCalendar 组件类似。
    16.11.1 创建并注册组件
    下面是本例的创建和注册组件的步骤。
    *将组件单元保存为CalSamp。
    *派生新组件类型,叫作TSampleCalendar。
    *在组件面板Samples 页中(CLX 的其他页)注册TSampleCalendar。
    按照上述步骤操作之后,从TCustomGrid 派生的结果单元的代码如下:
    第16 章 开发新的VCL 组件
    ·469·
    unit CalSamp;
    interface
    uses
    Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, Grids;
    type
    TSampleCalendar = class(TCustomGrid)
    end;
    procedure Register;
    implementation
    procedure Register;
    begin
    RegisterComponents(’Samples’, [TSampleCalendar]);
    end;
    end.
    如果现在安装日历组件,那它将会显示在Samples 页。可用的属性都是最基本的控件属性。下一
    步是使日历的用户可以有更多的可用属性。
    注意:如果可以安装刚才编译的日历组件,不要将它放到一个表单中。TCustomGrid 有一个
    抽象的DrawCell 方法,在创建实例之前必须重新声明。
    16.11.2 发布继承属性
    抽象的表格组件TCustomGrid 提供了大量的protected 属性。可以选择发布需要提供给组件用户的
    属性。为使组件用户可以使用继承的protected 属性,只需要在组件声明的published 部分重新声明属
    性即可。
    在本例中发布下列属性和事件:
    type
    TSampleCalendar = class(TCustomGrid)
    published
    property Align; {发布属性}
    property BorderStyle;
    property Color;
    property Font;
    property GridLineWidth;
    property ParentColor;
    property ParentFont;
    property OnClick; {发布事件}
    property OnDblClick;
    property OnDragDrop;
    property OnDragOver;
    property OnEndDrag;
    property OnKeyDown;
    property OnKeyPress;
    property OnKeyUp;
    end;
    还有很多属性可以发布,例如Options 属性可以让组件用户选择绘制哪一条表格线,但它们不用

    ·470·
    于日历。
    如果现在将修改后的日历组件安装到组件面板,并在应用程序中使用,就会发现日历中有很多属
    性和事件可以用,并且都有完全的功能。
    现在可以加入自己设计的功能。
    16.11.3 改变初始值
    日历基本上是一个行列数目固定的表格(当然并不是每一行都包括日期),因此不需要发布表格
    的ColCount 属性和RowCount 属性(因为组件用户不太可能除了用它显示一周7 天之外还显示其他内
    容)。然而必须设置属性的初始值,因为每周必然是7 天。
    要改变组件属性的初值,可以重载构造方法来设置需要的值。构造方法必须是虚方法。
    记住需要在组件对象声明的public 部分加入构造方法的声明,然后在组件单元的实现部分编写新
    的构造方法。新构造方法的第1 条语句一定是调用继承的构造方法。然后在uses 语句中加入StdCtrls
    单元。代码如下:
    type
    TSampleCalendar = class(TCustomGrid
    public
    constructor Create(AOwner: TComponent); override;
    ...
    end;
    ...
    constructor TSampleCalendar.Create(AOwner: TComponent);
    begin
    inherited Create(AOwner); {调用继承的构造方法}
    ColCount := 7; {每周一定是7 天}
    RowCount := 7; {加上开头,一定是6 行}
    FixedCols := 0; {没有行标签}
    FixedRows := 1; {日期名一行}
    ScrollBars := ssNone; {不需要滚动条}
    Options := Options - [goRangeSelect] + [goDrawFocusSelected]; {不能行选择}
    end;
    现在日历有6 行7 列,并且首行是固定的。
    16.11.4 重定义单元格大小
    当用户或应用程序改变窗口或控件大小时,Windows 发送一个WM_SIZE 消息给受影响的窗口和
    控件,以便根据需要调整设置。VCL 组件可以根据这个消息作出反应,改变单元格的大小,以便它们
    适应控件的内部边界。为了对WM_SIZE 消息作出反应,需要给组件增加一个消息处理过程。
    在本例中,日历控件需要对WM_SIZE 作出反应,因此给控件加入一个protected 方法WMSize,
    然后编写方法计算单元格的大小,以允许所有的单元格能在新的尺寸下显示。代码如下:
    type
    TSampleCalendar = class(TCustomGrid)
    protected
    procedure WMSize(var Message: TWMSize); message WM_SIZE;
    ...
    end;
    第16 章 开发新的VCL 组件
    ·471·
    ...
    procedure TSampleCalendar.WMSize(var Message: TWMSize);
    var
    GridLines: Integer; {临时局部变量}
    begin
    GridLines := 6 * GridLineWidth; {计算所有行的大小}
    DefaultColWidth := (Message.Width - GridLines) div 7; {设置新的默认cell 宽度}
    DefaultRowHeight := (Message.Height - GridLines) div 7; {设置新的默认cell 高度}
    end;
    当重新日历设置大小时,它在适合控件大小的最大尺寸内显示所有单元(Cell)。
    注意: 在CLX 应用程序中, 改变一个窗口或控件的大小通过调用protected 方法
    BoundsChanged 自动通知,CLX 组件可以通过改变单元格的大小适应新的控件大小来
    对这个通知作出反应。
    在本例中,日历控件需要重载BoundsChanged 方法来计算适当的单元格大小以允许所有单元格在
    新尺寸下都能显示。代码如下:
    type
    TSampleCalendar = class(TCustomGrid)
    protected
    procedure BoundsChanged; override;
    ...
    end;
    ...
    procedure TSampleCalendar.BoundsChanged;
    var
    GridLines: Integer; {临时局部变量}
    begin
    GridLines := 6 * GridLineWidth; {计算所有行的宽度}
    DefaultColWidth := (Width - GridLines) div 7; {设置新的默认cell 宽度}
    DefaultRowHeight := (Height - GridLines) div 7; {设置新的默认cell 高度}
    inherited; {now call the inherited method }
    end;
    16.11.5 填充单元格
    表格控件是将逐个地填充单元格的内容。在本例中,就是要计算每一天属于哪一个单元格。表格
    单元格的默认绘制方法是一个叫作DrawCell 的虚方法。因此需要重载DrawCell 方法以填充表格单元
    格。
    填充表格最容易的是填充固定行的单元格。运行时库包含了一个具有短日期名的数组,日历控件
    只需要在每一列使用一个合适的名字。代码如下:
    type
    TSampleCalendar = class(TCustomGrid)
    protected
    procedure DrawCell(ACol, ARow: Longint; ARect: TRect; AState: TGridDrawState);
    override;
    end;

    ·472·
    ...
    procedure TSampleCalendar.DrawCell(ACol, ARow: Longint; ARect: TRect;
    AState: TGridDrawState);
    begin
    if ARow = 0 then
    Canvas.TextOut(ARect.Left, ARect.Top, ShortDayNames[ACol + 1]); {使用RTL 字符串}
    end;
    16.11.6 跟踪日期
    为了使日历控件有用,必须给组件用户和应用程序提供一个设置年月日的机制。Delphi 使用
    TDateTime 类型存储日期和时间。TDateTime 是日期和时间的一个编码数值表示,它适合于程序操作,
    但人使用不方便。因此,可以将编码的格式作为运行时的内部访问格式,但是同时提供Day、Month、
    Year 属性给日历组件的使用者,以便用户在设计时设置时间。
    1.存储内部日期
    要在日历中存储日期,需要一个private 域保存日期以及一个运行时的属性提供对日期的访问。向
    日历加入内部日期需要如下3 个步骤。
    (1)声明保存日期的private 域
    type
    TSampleCalendar = class(TCustomGrid)
    private
    FDate: TDateTime;
    ...
    (2)在构造方法中初始化日期域
    constructor TSampleCalendar.Create(AOwner: TComponent);
    begin
    inherited Create(AOwner); {这是已经存在的语句}
    ... {其他初始化过程}
    FDate := Date; {从RTL 得到当前日期}
    end;
    (3)声明允许访问编码格式的日期的运行时属性
    应该还需要一个方法设置日期,因为设定日期需要更新控件的屏幕图像。
    type
    TSampleCalendar = class(TCustomGrid)
    private
    procedure SetCalendarDate(Value: TDateTime);
    public
    property CalendarDate: TDateTime read FDate write SetCalendarDate;
    ...
    procedure TSampleCalendar.SetCalendarDate(Value: TDateTime);
    begin
    FDate := Value; {设置新的日期值}
    Refresh; {更新屏幕显示}
    end;
    第16 章 开发新的VCL 组件
    ·473·
    2.访问年月日
    一个编码的数值型日期对程序来说是适用的,但是对于人来说使用年月日更方便一些。因此可以
    通过创建属性提供一个将存储的、编码的日期转换为年月日的方法。
    因为日期的每个元素(年月日)都是整数,并且因为它们中的每一个都需要对日期进行“解码”,
    所以可以通过共享所有3 个属性的实现方法来避免重复书写这些代码。也就是说,可以写两个方法,
    即读一个元素和写一个元素,并且利用这些方法设置所有的3 个属性。
    为提供设计时对年月日的访问,需要如下步骤。
    (1)声明3 个属性,给出每个惟一的索引值,代码如下:
    type
    TSampleCalendar = class(TCustomGrid)
    public
    property Day: Integer index 3 read GetDateElement write SetDateElement;
    property Month: Integer index 2 read GetDateElement write SetDateElement;
    property Year: Integer index 1 read GetDateElement write SetDateElement;
    ...
    (2)声明并且编写实现方法,为每个索引值设置不同的元素,代码如下:
    type
    TSampleCalendar = class(TCustomGrid)
    private
    function GetDateElement(Index: Integer): Integer; {注意Index 参数}
    procedure SetDateElement(Index: Integer; Value: Integer);
    ...
    function TSampleCalendar.GetDateElement(Index: Integer): Integer;
    var
    AYear, AMonth, ADay: Word;
    begin
    DecodeDate(FDate, AYear, AMonth, ADay); {将编码日期分解为元素}
    case Index of
    1: Result := AYear;
    2: Result := AMonth;
    3: Result := ADay;
    else Result := -1;
    end;
    end;
    procedure TSampleCalendar.SetDateElement(Index: Integer; Value: Integer);
    var
    AYear, AMonth, ADay: Word;
    begin
    if Value > 0 then {所有元素必须是正数}
    begin
    DecodeDate(FDate, AYear, AMonth, ADay); {获得当前日期元素}
    case Index of {根据索引设置新的元素}
    1: AYear := Value;
    2: AMonth := Value;
    3: ADay := Value;

    ·474·
    else Exit;
    end;
    FDate := EncodeDate(AYear, AMonth, ADay); {编码修改的日期}
    Refresh; {更新显示的日历}
    end;
    end;
    现在可以在设计时使用对象编辑器或运行时使用代码设置日历的年月日。当然,现在还没有加入
    在单元格中绘制日期的代码,但是现在可以得到需要的数据。
    3.产生日期数字
    将数字填充到日历需要考虑几个问题。每月有多少天,依赖于它是哪一个月以及当年是否为闰年。
    另外每月在每周不同天的开始,并且依赖于年和月。使用IsLeapYear 函数可以决定是否是闰年,使用
    SysUtils 单元中的MonthDay 数组可以得到每月的天数。一旦有了是否是闰年和每月天数的信息,就可
    以计算每一天所在表格的位置。计算是基于每月开始那一天是星期几的。
    计算填充的每个单元格的当月的第一周的偏移天数(即每月的第一天是星期几),最好的办法是
    当改变年或月时计算一次,然后每次引用就可以了。可以将值存储在类的域中,然后在日期变更时更
    新这个域。
    填写每个单元格的日期的步骤如下。
    (1)在对象中加入一个月的偏移天数域以及一个更新域值的方法,代码如下:
    type
    TSampleCalendar = class(TCustomGrid)
    private
    FMonthOffset: Integer; {存储月的偏移}
    ...
    protected
    procedure UpdateCalendar; virtual; {设置偏移的方法}
    end;
    ...
    procedure TSampleCalendar.UpdateCalendar;
    var
    AYear, AMonth, ADay: Word;
    FirstDate: TDateTime; {月的第一天的日期}
    begin
    if FDate <> 0 then {如果日期有效只计算偏移}
    begin
    DecodeDate(FDate, AYear, AMonth, ADay); {得到日期元素}
    FirstDate := EncodeDate(AYear, AMonth, 1); {第一天的日期}
    FMonthOffset := 2 - DayOfWeek(FirstDate); {在表格中产生偏移}
    end;
    Refresh; {重画控件}
    end;
    (2)在构造方法和SetCalendarDate、SetDateElement 方法中加入调用新的更新方法的语句,代码
    如下:
    constructor TSampleCalendar.Create(AOwner: TComponent);
    begin
    第16 章 开发新的VCL 组件
    ·475·
    inherited Create(AOwner); {已经存在}
    ... {其他初始化}
    UpdateCalendar; {设置正确的偏移}
    end;
    procedure TSampleCalendar.SetCalendarDate(Value: TDateTime);
    begin
    FDate := Value; {这已经存在}
    UpdateCalendar; {这原来是调用Refresh}
    end;
    procedure TSampleCalendar.SetDateElement(Index: Integer; Value: Integer);
    begin
    ...
    FDate := EncodeDate(AYear, AMonth, ADay); {解码修改的日期}
    UpdateCalendar; {这原来是调用Refresh}
    end;
    end;
    (3)在日历中加入当根据单元格的行列坐标返回日期的函数,代码如下:
    function TSampleCalendar.DayNum(ACol, ARow: Integer): Integer;
    begin
    Result := FMonthOffset + ACol + (ARow - 1) * 7; {计算这个cell 的日期}
    if (Result < 1) or (Result > MonthDays[IsLeapYear(Year), Month]) then
    Result := -1; {如果无效返回-1}
    end;
    记住在组件的类型声明中加入DayNum 的声明。
    (4)更新DrawCell 以填充日期,代码如下:
    procedure TCalendar.DrawCell(ACol, ARow: Longint; ARect: TRect; AState: TGridDrawState);
    var
    TheText: string;
    TempDay: Integer;
    begin
    if ARow = 0 then {如果这是表头}
    TheText := ShortDayNames[ACol + 1] {只是用日期的名称}
    else begin
    TheText := ’’; {默认为空}
    TempDay := DayNum(ACol, ARow); {得到这个cell 的数值}
    if TempDay <> -1 then TheText := IntToStr(TempDay); {如果有效则填充}
    end;
    with ARect, Canvas do
    TextRect(ARect, Left + (Right - Left - TextWidth(TheText)) div 2,
    Top + (Bottom - Top - TextHeight(TheText)) div 2, TheText);
    end;
    如果现在重新安装日历组件并将它放在一个表单中,就可以看到当前月的信息。
    4.选择当前日期
    既然在日历单元格中已经有了数值,那就应该可以移动选择的单元格设定当前日期。默认的选择

    ·476·
    从左上单元格开始,因此在构造日历和当日期变化时需要设置Row 和Column 属性。
    要设置当前日期的选择,可以改变UpdateCalendar,以在调用Refresh 之前设置Row 和Column。
    代码如下:
    procedure TSampleCalendar.UpdateCalendar;
    begin
    if FDate <> 0 then
    begin
    ... {已经存在的设置FMonthOffset 的语句}
    Row := (ADay - FMonthOffset) div 7 + 1;
    Col := (ADay - FMonthOffset) mod 7;
    end;
    Refresh; {已经存在}
    end;
    16.11.7 导航月和年
    对组件进行操作时属性是非常有用的,特别是设计时。但是程序中经常涉及到对多个属性进行操
    作,这时最好提供一个方法来完成操作。将常用的操作封装为一个方法的缺点是方法只在运行时可用。
    然而,对多个属性进行操作通常只在重复执行时出现,设计时很少会有这样的要求。下面介绍一个对
    多个属性进行操作的示例,即在日历中查找“下个月”。
    处理月的循环以及年的递增或递减其实比较简单,对日历来说,可以加入下面4 个查找前后年月
    的方法。每个方法都通过使用IncMonth 函数增加或减少CalendarDate,只是参数有一点不同。代码如
    下:
    procedure TCalendar.NextMonth;
    begin
    CalendarDate := IncMonth(CalendarDate, 1);
    end;
    procedure TCalendar.PrevMonth;
    begin
    CalendarDate := IncMonth(CalendarDate, -1);
    end;
    procedure TCalendar.NextYear;
    begin
    CalendarDate := IncMonth(CalendarDate, 12);
    end;
    procedure TCalendar.PrevYear;
    begin
    CalendarDate := DecodeDate(IncMonth(CalendarDate, -12);
    end;
    必须保证在类声明中加入新方法的声明。
    现在当使用日历组件创建应用程序时,可以很容易地切换年和月。
    16.11.8 导航日期
    在一个给定的月中,在日期之间导航有两个明显的方法,一个是使用箭头键,另一个是响应鼠标
    单击,标准表格组件对两种方法都进行处理。使用箭头键移动就相当于点击临近的单元格。
    第16 章 开发新的VCL 组件
    ·477·
    1.切换当前单元格
    一个表格继承的行为对使用箭头或鼠标切换当前单元格都进行处理,但是如果想改变选择的日
    期,就必须修改默认的行为。
    要处理在日历内部切换需要重载表格的Click 方法。当重载一个类似Click 的与用户交互相关的方
    法时,基本上都必须包括对继承方法的调用,以免丢失标准行为。
    下面是重载的日历表格的Click 方法,确保在TSampleCalendar 中加入了Click 的声明,并且在声
    明后面包括override 指令。代码如下:
    procedure TSampleCalendar.Click;
    var
    TempDay: Integer;
    begin
    inherited Click; {记住调用继承方法!}
    TempDay := DayNum(Col, Row); {获得被单击cell 的日期数字}
    if TempDay <> -1 then Day := TempDay; {如果有效,改变日期}
    end;
    2.提供OnChange 事件
    现在组件用户可以在日历内部改变日期,它应该允许应用程序对这些变化作出反应。
    向TSampleCalendar 加入一个OnChange 事件的步骤如下:
    (1)声明事件、存储事件的域以及调用事件的动态方法,代码如下:
    type
    TSampleCalendar = class(TCustomGrid)
    private
    FOnChange: TNotifyEvent;
    protected
    procedure Change; dynamic;
    ...
    published
    property OnChange: TNotifyEvent read FOnChange write FOnChange;
    ...
    (2)编写Change 方法,代码如下:
    procedure TSampleCalendar.Change;
    begin
    if Assigned(FOnChange) then FOnChange(Self);
    end;
    (3)在SetCalendarDate 和SetDateElement 的结尾加入调用Change 的语句,代码如下:
    procedure TSampleCalendar.SetCalendarDate(Value: TDateTime);
    begin
    FDate := Value;
    UpdateCalendar;
    Change; {这是惟一的新语句}
    end;
    procedure TSampleCalendar.SetDateElement(Index: Integer; Value: Integer);
    begin
    ... {许多设置元素值的语句}

    ·478·
    FDate := EncodeDate(AYear, AMonth, ADay);
    UpdateCalendar;
    Change; {这是新语句}
    end;
    end;
    通过绑定Onchange 事件处理代码,使用日历组件的应用程序现在可以对组件中日期的变化作出
    反应。
    3.排除空的单元格
    使用日历组件时,可以选择一个空的单元格,日期不变。但是比较自然的还是不允许选择空的单
    元格。
    为控制一个给定的单元格是否可选,可以重载表格的SelectCell 方法。SelectCell 是一个使用行和
    列作为参数的函数,返回一个布尔值指明指定的单元格是否可选。可以重载SelectCell,当一个单元格
    不包含有效日期时返回False。
    function TSampleCalendar.SelectCell(ACol, ARow: Longint): Boolean;
    begin
    if DayNum(ACol, ARow) = -1 then Result := False {-1 指明值无效}
    else Result := inherited SelectCell(ACol, ARow); {否则使用继承的值}
    end;
    现在如果使用者单击或试图使用箭头键移到空的单元格,日历将仍停留在当前选择的单元格中。
    16.12 开发数据感知控件:制作数据感知的Calender组件
    当处理数据库连接时,数据感知控件是非常方便的。就是说,应用程序可以在控件和数据库的某
    部分建立链接。Delphi 包含了数据感知的标签、编辑框、列表框、下拉列表框、查找控件和表格。也
    可以开发自己的数据感知控件。
    数据感知有若干等级。最简单的是只读数据感知或数据浏览,以及反映数据库当前状态的能力。
    比较复杂的是可编辑的数据感知,即用户可以在控件上操作数据库中的数据。同时包含的数据库的等
    级也是变化的,从最简单的一个连接绑定一个字段,到复杂的例子,例如多记录控件。
    本节首先将示例最简单的情况,即开发连接数据库的单个字段的只读控件。本例中将使用实例三
    中创建的TSampleCalendar 组件作为基类,当然也可以使用ComponentPalette 的Samples 页中的标准
    TCalendar 组件。
    然后本节将继续解释如何使一个新的数据浏览控件变为可以编辑数据的控件。
    本节中完成的组件除了完成月历功能之外,还可以将月历的日期绑定到数据库中的日期型字段
    中。数据库中的日期改变,则月历显示的日期也改变;用户改变月历中的日期,则绑定的数据库中的
    字段值也随之改变。
    16.12.1 制作一个数据浏览控件
    1.创建和注册组件
    按照以下步骤创建和注册组件。
    *将组件单元命名为DBCal。
    *从TSampleCalendar 派生一个新组件,命名为TDBCalendar。
    *在组件面板的Samples 页(CLX 是其他页)中注册TDBCalendar。
    在VCL 中创建的示例代码如下:
    unit CalSamp;
    interface
    第16 章 开发新的VCL 组件
    ·479·
    uses
    Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, Grids;
    type
    TSampleCalendar = class(TCustomGrid)
    end;
    procedure Register;
    implementation
    procedure Register;
    begin
    RegisterComponents(’Samples’, [TSampleCalendar]);
    end;
    end.
    如果现在安装日历组件,它会出现在Samples 页中。所有的属性都是从基类派生的。
    注意:当安装刚才编译的日历组件后,不要将它放置到一个表单中。TCustomGrid 组件中有
    一个抽象的DrawCell 在创建对象实例之前必须重载。
    2.使控件只读
    因为这个日历以只读方式响应数据,所以用户不能在控件中改变数据并保存到数据库中。
    注意:如果是从Delphi 的Samples 页的TCalendar 组件派生,则它已经有一个ReadOnly 属
    性,可以忽略这一步。
    (1)增加ReadOnly 属性
    通过增加ReaOnly 属性,可以提供使组件在设计时只读的方法。当属性值被设为True,将使控件
    中所有元素都不可被选。
    *增加属性声明和保存值的private 域。
    type
    TDBCalendar = class(TSampleCalendar)
    private
    FReadOnly: Boolean; {内部存储的域}
    public
    constructor Create(AOwner: TComponent); override; {必须重载以设置默认值}
    published
    property ReadOnly: Boolean read FReadOnly write FReadOnly default True;
    end;
    ...
    constructor TDBCalendar.Create(AOwner: TComponent);
    begin
    inherited Create(AOwner); {必须调用继承的构造方法!}
    FReadOnly := True; {设置默认值}
    end;
    *重载SelectCell 方法,使得当控件是只读时,不允许选择。
    function TDBCalendar.SelectCell(ACol, ARow: Longint): Boolean;
    begin
    if FReadOnly then Result := False {如果只读则不允许选择}
    else Result := inherited SelectCell(ACol, ARow); {否则使用继承的方法}

    ·480·
    end;
    必须记住在TDBcalendar 的类型声明部分声明SelectCell,并添加override 指令。
    如果现在将Calendar 加入窗体,会发现组件完全忽略鼠标和键盘事件。而且当改变日期时不能改
    变选择的位置。
    (2)允许所需的更新
    只读日历使用SelectCell 方法实现各种改变,包括设置Row 和Col 属性的值。当日期改变时,
    UpdateCalendar 方法设置Row 和Col 属性的值。但因为SelectCell 不允许改变,即使日期改变了,选
    择的日期仍留在原处。
    为了改变这种绝对禁止变化的情况,可以给日历增加一个布尔标志,当标志为True 时允许改变:
    type
    TDBCalendar = class(TSampleCalendar)
    private
    FUpdating: Boolean; {内部使用的标志}
    protected
    function SelectCell(ACol, ARow: Longint): Boolean; override;
    public
    procedure UpdateCalendar; override; {记住override 指示}
    end;
    ...
    function TDBCalendar.SelectCell(ACol, ARow: Longint): Boolean;
    begin
    if (not FUpdating) and FReadOnly then Result := False {如果更新时允许选择}
    else Result := inherited SelectCell(ACol, ARow); {否则使用继承的方法}
    end;
    procedure TDBCalendar.UpdateCalendar;
    begin
    FUpdating := True; {设置允许更新的标志}
    try
    inherited UpdateCalendar; {与平常一样更新}
    finally
    FUpdating := False; {一定要清除标志}
    end;
    end;
    现在日历仍旧不允许用户修改,但当改变日期属性时能正确反映改变;目前已有了一个真正只读
    的控件,下一步是增加数据浏览能力。
    3.增加数据连接
    控件与数据库的联接是由一个名为DataLink 的类处理的。Delphi 提供了几种类型的DataLink。将
    控件与数据库单个域相联的DataLink 对象是TFieldDatalink。Delphi 也提供了与整个表相联的
    DataLink。
    一个数据感知控件拥有DataLink 对象,就是说控件负责构造和销毁DataLink 对象。
    (1)声明对象域
    每个组件要为其拥有对象声明一个对象域。本例中日历对象需要为它的DataLink 声明一个
    TFieldDataLink 类型的域。
    type
    第16 章 开发新的VCL 组件
    ·481·
    TDBCalendar = class(TSampleCalendar)
    private
    FDataLink: TFieldDataLink;
    ...
    end;
    在编译应用程序之前,需要在uses 语句中加入DB 和DBCtrls。
    (2)声明访问属性
    每一个数据感知控件有一个DataSource 属性,该属性指定应用程序给控件提供数据的数据源类。
    另外,访问单个域的控件还需要一个DataField 属性指定数据源中的域。
    (3)DataLink 初始化
    数据感知控件在其生存期间要不停地访问DataLink 对象,在其构造方法中必须创建DataLink 对
    象,并且在析构时销毁DataLink 对象。因此当重载日历的构造方法和析构方法时也要创建和销毁
    DataLink 对象。
    type
    TDBCalendar = class(TSampleCalendar)
    public {构造方法和析构方法必须是public}
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;
    ...
    end;
    ...
    constructor TDBCalendar.Create(AOwner: TComponent);
    begin
    inherited Create(AOwner); {首先必须调用继承的构造方法}
    FDataLink := TFieldDataLink.Create; {构造DataLink 对象}
    FDataLink.Control := self; {使DataLink 知道日历对象}
    FReadOnly := True; {这应该已经存在}
    end;
    destructor TDBCalendar.Destroy;
    begin
    FDataLink.Free; {首先必须销毁拥有的对象}
    inherited Destroy; {然后调用继承的析构方法}
    end;
    现在,组件已拥有完整的DataLink 对象,但组件还不知从相关联的域中读取什么数据。
    4.响应数据变化
    一旦控件拥有了DataLink 和描述数据源和数据字段的属性,就需要响应字段中数据的变化(可以
    是移动到了另一条记录,也可以是该字段的数据发生了变化)。
    DataLink 对象都有个名为OnDataChange 的事件。当数据源指示数据发生变化时,DataLink 对象
    调用所有OnDataChange 所关联的事件处理过程。要在数据改变时更新数据,就需要给DataLink 对象
    的OnDataChange 事件增加事件处理过程。
    在本例中需要声明和实现OnDataChange 方法, 然后在构造方法中将它指定给DataLink 的
    OnDataChange 事件,在销毁对象之前,撤消它和OnDataChange 事件的关联。
    下面代码声明了DataChange 方法,并将其赋给DataLink 对象的OnDataChange 事件:
    type

    ·482·
    TDBCalendar=class(TCalendar)
    private
    procedure Datachange(Sender:TObject);
    end;
    constructor TDBCalendarCreate(AOwner:TComponent);
    begin
    inherited Create(AOwner);
    FReadOnly:=True;
    FDataLink:=TFieldDataLink.Create;
    FDataLink.OnDataChange:=DataChange;
    end;
    destructor TDBcalendar.Destroy;
    begin
    FDataLink.OnDataChange:=nil;
    FDataLink.Free;
    inherited Destroy
    end;
    procedure TDBCalendar.DataChange(Sender:TObject);
    begin
    if FDataLink.Filed=nil then
    CalendarDate:=0;
    else
    CalendarDate:=FDataLink.Field.AsDate;
    end;
    16.12.2 制作一个数据编辑控件
    当制作数据编辑控件时,首先创建和注册组件、加入DataLink,这与数据浏览组件是一样的。也
    可以以一种相似的方式在底层响应数据变化,但是必须处理更多的细节。
    例如可能需要组件对鼠标和键盘事件都作出反应。当用户改变控件的内容时,控件必须反应。当
    用户退出控件时,控件的变化应能反映到数据库中。
    这里的数据编辑控件与数据感知日历是同一个控件,只是它可以编辑和浏览它连接的字段的数
    据。
    1.改变FReadOnly 的默认值
    因为这是数据编辑控件,ReadOnly 的默认值应该是False,所以可以通过在构造方法中设置改
    FReadOnly 的值实现。
    constructor TDBCalendar.Create(AOwner: TComponent);
    begin
    ...
    FReadOnly := False; {设置默认值}
    ...
    end;
    第16 章 开发新的VCL 组件
    ·483·
    2.处理鼠标和键盘消息
    当控件用户和控件交互时,控件从Windows 既接收鼠标消息(包括WM_LBUTTONDOWN、
    WM_MBUTTONDOWN 和WM_RBUTTONDOWN),又接收键盘消息(WM_KEYDOWN)。为使控件
    响应这些消息,必须对这些消息进行处理。
    注意:如果开发CLX 应用程序,操作系统事件以来自操作系统的通知的形式表现的。
    (1)响应鼠标消息
    MouseDown 方法是控件的OnMouseDown 事件的protected 方法。控件自动调用MouseDown 响应
    Windows 的鼠标消息。当重载继承的MouseDown 方法时,可以包含提供附加给调用OnMouseDown
    事件的其他响应的代码。
    重载MouseDown 需要在TDBCalendar 中加入MouseDown 方法,重载之后的代码如下:
    type
    TDBCalendar = class(TSampleCalendar);
    ...
    protected
    procedure MouseDown(Button: TButton, Shift: TShiftState, X: Integer, Y: Integer);
    override;
    ...
    end;
    procedure TDBCalendar.MouseDown(Button: TButton; Shift: TShiftState; X, Y: Integer);
    var
    MyMouseDown: TMouseEvent;
    begin
    if not ReadOnly and FDataLink.Edit then
    inherited MouseDown(Button, Shift, X, Y)
    else
    begin
    MyMouseDown := OnMouseDown;
    if Assigned(MyMouseDown then MyMouseDown(Self, Button, Shift, X, Y);
    end;
    end;
    当MouseDown 响应鼠标消息时,只有控件的ReadOnly 属性为False,并且DataLink 对象处于Edit
    模式时(即字段可以编辑),继承的MouseDown 方法才会被调用。如果字段不能编辑,程序员设置的
    OnMouseDown 事件处理方法(如果存在)则会被执行。
    (2)响应键盘消息
    KeyDown 方法是控件的OnKeyDown 事件的protected 方法。控件自动调用KeyDown 响应Windows
    的键盘消息。当重载继承的KeyDown 方法时,可以包含提供附加给调用OnKeyDown 事件的其他响应
    的代码。
    重载KeyDown 的步骤如下。
    *在TDBCalendar 中增加KeyDown 方法。
    type
    TDBCalendar = class(TSampleCalendar);
    ...
    protected

    ·484·
    procedure KeyDown(var Key: Word; Shift: TShiftState; X: Integer; Y: Integer);
    override;
    ...
    end;
    *实现KeyDown 方法。
    procedure KeyDown(var Key: Word; Shift: TShiftState);
    var
    MyKeyDown: TKeyEvent;
    begin
    if not ReadOnly and (Key in [VK_UP, VK_DOWN, VK_LEFT, VK_RIGHT, VK_END,
    VK_HOME, VK_PRIOR, VK_NEXT]) and FDataLink.Edit then
    inherited KeyDown(Key, Shift)
    else
    begin
    MyKeyDown := OnKeyDown;
    if Assigned(MyKeyDown) then MyKeyDown(Self, Key, Shift);
    end;
    end;
    当KeyDown 响应键盘消息时,只有控件的ReadOnly 属性为False、按下的键是箭头键并且DataLink
    对象处于Edit 模式时(即字段可以编辑),继承的KeyDown 方法才会被调用。如果字段不能编辑或者
    按了其他键,程序员设置的OnKeyDown 事件处理方法(如果存在)则会被执行。
    3.更新字段DataLink 类
    有两种类型的数据变化:
    *字段值的变化必须反应到数据感知控件。
    *数据感知控件值的变化必须反应到字段值。
    TDBCalendar 组件已经有一个DataChange 方法,它通过赋值给CalendarDate 属性处理数据库中字
    段值的改变。DataChange 方法是OnDataChange 事件的处理方法。因此日历控件能处理第1 种类型的
    数据变化。
    DataLink 域也有一个OnUpdateData 事件,当控件使用者修改数据感知控件的内容时,该事件发
    生。日历控件有一个UpdateData 方法是OnUpdateData 事件的处理方法。UpdateData 将数据感知控件
    的变化的值赋给DataLink 域。
    *为反映日历中字段值的变化,在日历组件的private 部分加入UpdateData 方法。
    type
    TDBCalendar = class(TSampleCalendar);
    private
    procedure UpdateData(Sender: TObject);
    ...
    end;
    *实现UpdateData 方法。
    procedure UpdateData(Sender: TObject);
    begin
    FDataLink.Field.AsDateTime := CalendarDate; {设置字段连接到日历日期}
    end;
    *在TDBCalendar 的构造方法中,将UpdateData 方法赋给OnUpdateData 事件。
    第16 章 开发新的VCL 组件
    ·485·
    constructor TDBCalendar.Create(AOwner: TComponent);
    begin
    inherited Create(AOwner);
    FReadOnly := True;
    FDataLink := TFieldDataLink.Create;
    FDataLink.OnDataChange := DataChange;
    FDataLink.OnUpdateData := UpdateData;
    end;
    4.修改Change 方法
    新的日期值被设置时,TDBCalendar 的Change 方法都会被调用。如果存在,Change 将调用
    OnChange 事件处理方法。组件的使用者可以编写OnChange 事件的处理代码以响应日期的变化。
    当日历日期改变时,底层的数据库应该得到数据改变的通知。可以通过重载Change 方法加入几
    行代码实现这个功能。
    *在TDBCalendar 组件中加入Change 方法。
    type
    TDBCalendar = class(TSampleCalendar);
    private
    procedure Change; override;
    ...
    end;
    *编写Change 方法,调用Modified 方法通知数据库数据改变了,然后调用继承的Change 方法。
    procedure TDBCalendar.Change;
    begin
    FDataLink.Modified; {调用Modified 方法}
    inherited Change; {调用继承的Change 方法}
    end;
    5.更新数据库
    迄今为止,数据感知控件的变化可以改变DataLink 类的数据。最后一步是创建一个数据编辑控件
    用于更新数据库。这应该在改变了数据感知控件的值,然后单击控件外部或按tab 键退出控件之后发
    生。这个过程在VCL 和CLX 中是不同的。
    VCL 应用程序为控件中的操作定义消息控制IDs。例如当用户退出控件时,CM_EXIT 发送给控件,
    可以写一个消息处理方法响应这个消息。在本例中,当用户退出控件时,CMExit 方法(CM_EXIT 的
    消息处理方法)通过使用DataLink 中变化的值更新数据库中的记录来响应这个消息。
    要使用消息处理方法更新数据库,可以按照以下步骤进行。
    *在TDBCalendar 组件中加入消息处理方法。
    type
    TDBCalendar = class(TSampleCalendar);
    private
    procedure CMExit(var Message: TWMNoParams); message CM_EXIT;
    ...
    end;
    *实现CMExit 方法。
    procedure TDBCalendar.CMExit(var Message: TWMNoParams);
    begin

    ·486·
    try
    FDataLink.UpdateRecord; {告诉DataLink 更新数据库}
    except
    on Exception do SetFocus; {如果失败,不允许焦点离开}
    end;
    inherited;
    end;
    在CLX 应用程序中,TWidgetControl 有一个protected 的DoExit 方法,它在输入焦点从控件移开
    时调用。这个方法调用OnExit 事件的处理方法。可以重载这个方法在产生OnExit 事件处理方法之前
    更新记录。
    要在当用户退出控件之前更新数据库,可以按照以下步骤进行。
    *在TDBCalendar 组件中重载DoExit 方法。
    type
    TDBCalendar = class(TSampleCalendar);
    private
    procedure DoExit; override;
    ...
    end;
    *实现DoExit 方法。
    procedure TDBCalendar.CMExit(var Message: TWMNoParams);
    begin
    try
    FDataLink.UpdateRecord; {告诉DataLink 更新数据库}
    except
    on Exception do SetFocus; {如果失败,不让焦点离开}
    end;
    inherited; {让继承的方法产生OnExit 事件}
    end;
    16.13 开发非可视组件:制作Dialog组件
    将一个经常使用的对话框做成组件加到组件面板中,可以方便使用。对话框组件将和标准的通用
    对话框一样工作。本例的目标是开发一个使用者可以加到项目中并且在设计时设置属性的对话框。
    Delphi 与对话框联系的组件在运行时创建并运行一个对话框,传递用户指定的数据。对话框组件
    因此可以重用和定制。
    这一节可以看到如何创建一个与Delphi 中提供的普通的About 对话框类似的组件。
    注意:需要拷贝About.pas 和About.dfm 到工作目录中。
    16.13.1 定义组件接口
    在创建对话框组件之前,首先需要决定希望应用程序员如何使用它,因此必须在对话框和使用它
    的应用程序中定义接口。
    通用对话框组件的属性使应用程序员能设置对话框的初始状态,例如标题和初始的控件设置,然
    后在对话框关闭之后读回需要的信息。应用程序与对话框内部的单个控件没有交互,只是与组件的属
    性进行交互。
    因此接口必须包含足够的信息,以使对话框表单能够按照应用程序员指定的方式显示并返回应用
    第16 章 开发新的VCL 组件
    ·487·
    程序需要的所有信息。可以认为组件的属性是持续化的数据,而对话框是临时的。
    在About 对话框的例子中,不需要返回任何信息,因此封装的属性只需要包含正确显示About 对
    话框的信息即可。
    16.13.2 创建和注册组件
    本例根据下列步骤创建和注册组件。
    *调用组件单元AboutDlg。
    *从TComponent 派生一个新组件类,叫作TAboutBoxDlg。
    *在组件面板的Samples 页注册TAboutBoxDlg。
    结果单元应该是这样的:
    unit AboutDlg;
    interface
    uses
    SysUtils, WinTypes, WinProcs, Messages, Classes, Graphics, Controls, Forms;
    type
    TAboutBoxDlg = class(TComponent)
    end;
    procedure Register;
    implementation
    procedure Register;
    begin
    RegisterComponents(’Samples’, [TAboutBoxDlg]);
    end;
    end.
    现在新组件只有建立在TComponent 内部的功能,这是最简单的非可视组件。
    16.13.3 创建组件接口
    1.包含表单单元
    为了组件能初始化并显示对话框,必须在单元文件的uses 语句中加入表单单元。在AboutDlg 单
    元的uses 语句中加入About。现在的uses 语句是这样的:
    uses
    Windows, SysUtils, Messages, WinTypes, WinProcs, Messages, Classes, Graphics, Controls,
    Forms,About;
    表单单元将声明一个表单类的实例,在About 对话框的例子中,表单类是TAboutBox,并且About
    单元包含下列声明:
    var
    AboutBox: TAboutBox;
    因此通过将About 加入uses 语句,可以使AboutBox 为组件所用。
    2.加入界面属性
    在进入下一步之前,必须决定为了使对话框可以在应用程序中使用需要的属性,然后在组件类的
    声明中加入这些属性的声明。
    本例组件的属性比开发一个常规组件需要的属性要简单一些。在本例中,只是创建一些能够在对
    话框和组件之间传递的持续化数据。通过把这些数据设置为属性,可以使应用程序员在设计时设置数
    据,以便组件能够在运行时传递给对话框。

    ·488·
    声明一个界面属性需要在组件的类声明中加入两条。
    *一个private 域,它是组件用于存储属性值的变量。
    *申明它自己的published 属性,它指明了属性的名称,并告诉它使用哪个域存储数据。
    这种类型的界面属性不需要访问方法。它们直接访问存储的数据。根据惯例,存储属性值的域名
    称与属性的名称一样,只是在前面加上“F”。要注意的是,域和属性必须是同一种类型。
    3.加入Execute 方法
    组件界面的最后部分是打开对话框并在关闭时返回值的方法。与通用对话框组件一样,使用一个
    布尔函数Execute 实现,它在用户单击“OK”按钮时返回True,取消对话框时返回False。
    Execute 的声明应该都是这样的:
    type
    TMyWrapper = class(TComponent)
    public
    function Execute: Boolean;
    end;
    Execute 的最小实现需要构造对话框表单,并显示模式对话框,然后根据ShowModal 的结果返回
    True 或False。
    16.13.4 测试对话框组件
    一旦已经安装对话框组件,可以与使用其他对话框一样使用,将它放在一个表单中并执行。测试
    About 对话框的快速方法是在一个表单中加入一个按钮,当用户单击按钮时执行对话框。
    例如当创建完About 对话框后,并使之成为一个组件,然后把它加入到组件面板,就可以按照下
    列步骤测试。
    *创建新项目。
    *在主表单中加入一个About 对话框组件。
    *在表单中加入一个按钮。
    *双击按钮创建一个空的Click 事件处理过程。
    *在Click 事件处理过程中,加入下列代码:
    AboutBoxDlg1.Execute;
    *运行程序。
    当主表单出现时,单击“确定”按钮,显示About 对话框,单击“OK”按钮关闭对话框。也可以
    进一步设置About 对话框的各种属性、运行程序来测试组件。

  • 相关阅读:
    C# ToString格式大全
    如何将数据库中的表导入到PowerDesigner中
    IIS配置wap服务器
    URL加随机数的作用
    程序员人生路
    Asp.net三种事务处理
    20121107荣迪信息技术有限公司面试总结
    ubuntu 安装之后的问题
    windows系统+VS2013编译GDAL(使用cmd命令行)
    VS2013+OPENCV+GDAL处理多光谱数据
  • 原文地址:https://www.cnblogs.com/luckForever/p/7255072.html
Copyright © 2011-2022 走看看