我的主页: http://www.tommstudio.com/
属性编辑器对于大多数Delphi程序员来说无疑是很熟悉的,在对象编辑器的内核中有着大量的属性编辑器,每个对象编辑器中的属性都对应一个属性编辑器类的实例。
Delphi5中提供了一些新的高级特性,使我们能够定义新的属性编辑器,为以有的属性提供新的功能,或者设定和显示新的控件的新的属性的显示方法。在Delphi5以前,对象编辑器只能够以文本的形式显示属性值。在Delphi 5中给属性编辑器提供了新的特性,使我们能够以任何形式显示属性的名称和值,如下图所示如果属性有一个下拉列表,我们就可以为每一个列表项添加一个图标。下面我们就来研究一下如何实现属性编辑器的自绘画的功能。
属性编辑刷新器
所有的属性编辑器都是从TpropertyEditor继承下来的。我们可以为特定的属性类型、属性名或控件注册一个属性编辑器。对象编辑器检查每一个要显示的属性的名称和类型,选择合适的属性编辑器类。然后它会创建这个类的一个实例(每个属性对应一个实例)。当我们选择了另一个控件,对象编辑器会释放全部的属性编辑器对象,然后为新的控件创建新的对象。
属性编辑器可以决定如何显示属性的值以及用户如何设定一个新的属性值。比如,TintegerProperty调用IntToStr函数以字符串的形式显示整数值并用StrToInt函数来转换用户输入的新值。 当用户输入了一个新的属性值时,TcolorProperty同样使用一个整型值来表示,但把整数解释为颜色,并尽可能地映射颜色值为一个名称(如clBlack或clBtnFace) 。
一个属性编辑器实现上述功能是通过重载TpropertyEditor的一个或多个方法来实现的。绝大多数的属性编辑器需要重载GetValue方法,GetValue方法获得属性值的字符串形式。以及SetValue方法,SetValue方法把一个字符串转化为属性值。要想了解关于编写属性编辑器的进一步信息,需要仔细研究DsgnIntf.pas文件(在Delphi5\Source\Toolsapi目录下)以及Delphi 5 在线帮助(在"property editors, creating"部分里)。
基础步骤
要实现一个最基本的自绘画属性编辑器,我们只需要重载TpropertyEdiotr的PropDrawValue 方法。比如如前面图中所见到的,TcolorProperty属性重载了PropDrawValue方法在颜色名前显示一个对应于相应颜色的彩色小方块。为了理解如何使用PropDrawValue方法,我们为Tfont对象写一个新的属性编辑器,新的编辑器将会用当前字体名对应的字体来显示Tfont对应的属性。
Delphi本身已经提供了一个属性编辑器TfontProperty,它在对象编辑器中添加了一个省略按钮,用户可以点击按钮调出标准的Windows字体选择对话框来设定字体的属性。我们可以直接从TfontProperty继承新的编辑器,类的声明如下:
type
TVisualFontProperty = class(TFontProperty)
public
procedure PropDrawValue(Canvas: TCanvas;
const Rect: TRect; Selected: Boolean); override;
end;
当对象编辑器需要显示属性值的时候,IDE会调用PropDrawValue方法来画属性值。Delphi传递一个画布对象(Canvas)及绘画区域来供程序画属性值。Selected参数现在还没用,我们可以忽略它。
注意:Delphi并不会为给定的绘画区域设定剪裁区域,也就是说我们必须严格按照给定的区域绘画,如果超出界限,会把别的属性值给覆盖掉。
TvisualFontProperty对象的唯一任务就是选择相应于Font名字的字体来画属性值。它设定字体的名称,样式以及颜色(当颜色不同于背景色的时候),字体的大小显示保留不动,以免使用非常大或非常小的字体大小画值的时候会出现的问题。下面就是PropDrawValue的实现部分:
// 替换乏味的Tfont属性值的显示方式,用选定的字体样式
//和字体来画相应的属性值,用户可能会选择比较大的字体
//尺寸,所以这里保留字体大小不动,只有当字体颜色不同
//于背景色的时候,才用相应的颜色画,否则前景背景一样
//的话就无法看到字体的属性值了
procedure TVisualFontProperty.PropDrawValue(
Canvas: TCanvas; const Rect: TRect; Selected: Boolean);
var
Font: TFont;
begin
Font := TFont(GetOrdValue);
if Font <> nil then begin
if ColorToRGB(Font.Color) <> ColorToRGB(clBtnFace) then
Canvas.Font.Color := Font.Color;
Canvas.Font.Name := Font.Name;
Canvas.Font.Style := Font.Style;
end;
inherited;
end;
另外我们重载GetValue方法来提供更多的信息,比如字体名和大小。
function TVisualFontProperty.GetValue: string;
var
Font: TFont;
begin
Font := TFont(GetOrdValue);
if Font = nil then
Result := inherited GetValue
else
Result := Format('%s, %d', [Font.Name, Font.Size]);
end;
我们可以画任何东西到画布上,比如图标和位图的属性编辑器是TgraphicProperty。它显示把图标属性显示为一个乏味的字符串”TIcon”。 我们可以把图标属性显示为对应的图标,这样的界面更加友好。这里我们继承一个TvisualGraphicProperty对象重载PropDrawValue来实现这一功能。
Tpicture属性的情况也是类似的,所以我们用一个公用的过程DrawGraphic来实现,DraGraphic缩放图形对象使之符合对象编辑器可用空间的大小,同时它维持原来的宽高比,缩放图像为最小的可能的尺寸。对于图标来说,由于Windows不能缩放图标,所以DrawGraphic调用StretchIcon过程把图标画到位图上,然后缩放位图。下面是过程代码:
// Windows不能缩放图标,所以如果图标大小不匹配的话,
//把它画到一个临时的位图上,然后缩放位图。
procedure StretchIcon(Canvas: TCanvas;
const Rect: TRect; Icon: TIcon);
var
Bitmap: TBitmap;
begin
Bitmap := TBitmap.Create;
try
Bitmap.Height := Icon.Height;
Bitmap.Width := Icon.Width;
Bitmap.Canvas.Brush.Color := clBtnFace;
Bitmap.Canvas.FillRect(Rect);
Bitmap.Canvas.Draw(0, 0, Icon);
Canvas.StretchDraw(Rect, Bitmap);
finally
Bitmap.Free;
end;
end;
procedure DrawGraphic(Canvas: TCanvas; const Rect: TRect;
Graphic: TGraphic; const Value: string);
var
R: TRect;
HeightRatio, WidthRatio: Single;
begin
Canvas.FillRect(Rect);
//缩放图像使其符合给定空间大小,
//同时保持图像宽高比不变
HeightRatio := (Rect.Bottom - Rect.Top) / Graphic.Height;
WidthRatio := (Rect.Right - Rect.Left) / Graphic.Width;
R := Rect;
if HeightRatio < WidthRatio then
R.Right := R.Left + Trunc(Graphic.Width * HeightRatio)
else
R.Bottom := R.Top + Trunc(Graphic.Height * WidthRatio);
if (Graphic is TIcon) and
((HeightRatio > 1) or (WidthRatio > 1)) then
StretchIcon(Canvas, R, TIcon(Graphic))
else
Canvas.StretchDraw(R, Graphic);
// 在图像的右边,让继承的编辑器画缺省的文本,比如“Ticon“
R.Left := R.Right;
R.Right := Rect.Right;
R.Top := Rect.Top;
R.Bottom := Rect.Bottom;
Canvas.TextRect(R, R.Left+1, R.Top+1, Value);
end;
我们在DrawGraphic过程中写了主要的代码,所以PropDrawValue就显得简单多了,主要的作用是确保属性有一个有效的图形对象,如果没有就调用继承的方法来处理。代码如下:
procedure TVisualGraphicProperty.PropDrawValue(
Canvas: TCanvas; const Rect: TRect; Selected: Boolean);
var
Graphic: TGraphic;
begin
Graphic := TGraphic(GetOrdValue);
if (Graphic = nil) or Graphic.Empty or
(Graphic.Height = 0) or (Graphic.Width = 0) then
inherited
else
DrawGraphic(Canvas, Rect, Graphic, GetVisualValue);
end;
自绘画的名字
我们可以重载PropDrawName方法,它同PropDrawValue方法工作方式类似,只不过一个是画值,一个是画名称。大多数属性的名字不需要任何特殊的处理,对于BoldFace属性的名字来说,把名字加粗可以便于用户了解BoldFace属性的情况。下面的代码显示了TboldComponentNameProperty类的如何实现PropDrawName方法的。
type
TBoldComponentNameProperty =
class(TComponentNameProperty)
public
procedure PropDrawName(Canvas: TCanvas;
const Rect: TRect; Selected: Boolean); override;
end;
procedure TBoldComponentNameProperty.PropDrawName(
Canvas: TCanvas; const Rect: TRect; Selected: Boolean);
var
Style: TFontStyles;
begin
Style := Canvas.Font.Style;
Canvas.Font.Style := Canvas.Font.Style + [fsBold];
try
inherited;
finally
//恢复字体的样式以便Delphi正确的画属性值
Canvas.Font.Style := Style;
end;
end;
下拉列表
一个属性编辑器可能会拥有一个下拉列表框,用户可以通过选择列表项来改变属性值。Delphi 5使用了自绘画的特性来改进Tcolor和Tcursor属性的界面友好性,我们也可以作同样的事情,通过重载ListDrawValue,ListMeasureHeight和ListMeasureWidth方法可以很容易的做到。下面是这几个方法的声明:
procedure ListDrawValue(const Value: string;
Canvas: TCanvas; const Rect: TRect; Selected: Boolean);
procedure ListMeasureHeight(const Value: string;
Canvas: TCanvas; var Height: Integer);
procedure ListMeasureWidth(const Value: string;
Canvas: TCanvas; var Width: Integer);
ListDrawValue方法类似于PropDrawValue方法,但它的Selected是有意义的,表示用户已经选择了这个列表项。Delphi会根据Selected参数自动设定画布的颜色为合适的值,所以通常情况下我们可以忽略这个参数。Value参数是要显示的字符串,Delphi调用GetValue方法来获得这些字符串,
在对象编辑器显示列表框之前,它会调用ListMeasureHeight和ListMeasureWidth方法来获得每个列表项的尺寸,我们可以设定Height和Width参数来获得想要得到的高度和宽度。下拉列表框使用全部列表项中最大的尺寸,然后显示相同区域大小的列表项。
当用户滚动列表框时,Delphi调用ListDrawValue方法来画心新的可见的列表项。用户可能会前后滚动多次,如果列表项很多,每次重绘需要很多时间的话,我们应该建立一个临时的位图,把列表项先画到位图上,然后在ListDrawValue方法中快速显示位图。这实际上就是双缓冲技术。
下面的例子是一个扩展的集合类型属性,下拉列表显示全部的集合元素,并在每个集合元素旁边添加一个复选框。复选框是通过位图来模仿的,属性编辑器先取得复选框位图,并在不同情况下显示打叉和未打叉的位图。全局变量Checked和Unchecked保存这两个位图 为Tbitmap类型。下面的代码显示了TSetPropertyEx.类是如何实现自绘画集合类型的:
// 在下拉列表框的每一个列表项旁边画一个复选框
procedure TSetPropertyEx.ListDrawValue(const Value: string;
Canvas: TCanvas; const Rect: TRect; Selected: Boolean);
var
IsChecked: Boolean;
OrdValue: Integer;
begin
OrdValue := GetOrdValue;
IsChecked := GetEnumValue(EnumInfo, Value) in
TIntegerSet(OrdValue);
Canvas.FillRect(Rect);
Canvas.TextRect(Rect, Rect.Left + Checked.Width + 2,
Rect.Top + 1, Value);
if IsChecked then
Canvas.Draw(Rect.Left + 1, Rect.Top + 1, Checked)
else
Canvas.Draw(Rect.Left + 1, Rect.Top + 1, Unchecked);
end;
procedure TSetPropertyEx.ListMeasureHeight(
const Value: string; Canvas: TCanvas;
var Height: Integer);
begin
if Height < Checked.Height then
Height := Checked.Height;
end;
procedure TSetPropertyEx.ListMeasureWidth(
const Value: string; Canvas: TCanvas;
var Width: Integer);
begin
Width := Width + Checked.Width + 2;
end;
类似于显示集合元素,对于布耳类型的属性我们也可以加一个复选框。下面我们要实现TBooleanPropertyEx 属性编辑器对布耳类型进行了扩展,对于不同的布耳类型,比如ByteBool, WordBool和LongBool属性的实现方式是类似的,当时需要不同的属性编辑器。下面就是TbooleanPropertyEx的实现代码,对于复选框如何相应消息,有点小问题,因为通常我们是希望单击实现复选框切换状态,Delphi不支持单击,我们只好使用双击了(估计在Delphi 6中属性编辑器可能会支持单击),注意双击会调用属性编辑器的Edit方法。对于集合元素或布耳属性,双击可以切换属性值。估计在Delphi 6中属性编辑器可能会支持单击。
//根据True或者False来画一个复选框及布耳值的文本标签
procedure DrawBoolCheckBox(Canvas: TCanvas;
const Rect: TRect; const Value: string);
begin
Canvas.FillRect(Rect);
Canvas.TextRect(Rect, Rect.Left + Checked.Width + 2,
Rect.Top + 1, Value);
if Value = BooleanIdents[False] then
Canvas.Draw(Rect.Left + 1, Rect.Top + 1, UnChecked)
else
Canvas.Draw(Rect.Left + 1, Rect.Top + 1, Checked);
end;
{ TSetElementPropertyEx }
// 每个列表项旁边显示一个复选框,用户必须双击
//而不是单击才能切换复选框状态
procedure TSetElementPropertyEx.PropDrawValue(
Canvas: TCanvas; const Rect: TRect; Selected: Boolean);
begin
DrawBoolCheckBox(Canvas, Rect, Value);
end;
{ TBoolPropertyEx }
// 为ByteBool, WordBool和LongBool类型显示复选框
procedure TBoolPropertyEx.PropDrawValue(Canvas: TCanvas;
const Rect: TRect; Selected: Boolean);
begin
DrawBoolCheckBox(Canvas, Rect, Value);
end;
使用属性编辑器
最后我们需要作的就是注册这些新的属性编辑器,大多数的编辑器比较容易注册,但是新的集合类属性编辑器存在一个问题,每一个集合都是一个独立的类型,我们必须分别为每个集合类型注册一遍属性编辑器。幸运的是,Delphi有一个不为人知的特性就是允许为所有的集合类型注册同一个属性编辑器。同通常的为单独一个类型注册属性编辑器不同的是,我们可以通过提供一个属性映射函数来实现注册,这个函数把对象和属性信息作为参数,然后返回属性编辑器类或是nil。这种情况下,映射函数校验属性类型,并为所有属性类型是tkSet的属性返回新的集合属性编辑器。下面是注册过程的代码:
//为全部的集合属性注册一个统一的属性编辑器
function SetMapper(Obj: TPersistent; PropInfo: PPropInfo):
TPropertyEditorClass;
begin
if PropInfo.PropType^.Kind = tkSet then
Result := TSetPropertyEx
else
Result := nil;
end;
procedure Register;
begin
RegisterPropertyEditor(TypeInfo(TFont), nil, '',
TVisualFontProperty);
RegisterPropertyEditor(TypeInfo(TGraphic), nil, '',
TVisualGraphicProperty);
RegisterPropertyEditor(TypeInfo(TComponentName),
TComponent, 'Name', TBoldComponentNameProperty);
RegisterPropertyEditor(TypeInfo(Boolean), nil, '',
TBooleanPropertyEx);
RegisterPropertyEditor(TypeInfo(ByteBool), nil, '',
TBoolPropertyEx);
RegisterPropertyEditor(TypeInfo(WordBool), nil, '',
TBoolPropertyEx);
RegisterPropertyEditor(TypeInfo(LongBool), nil, '',
TBoolPropertyEx);
RegisterPropertyMapper(SetMapper);
end;
写完注册代码后,我们要作的只是把新的属性编辑器添加到包中,然后在Delphi中安装。关闭所有的窗体,确保原来的属性编辑器全部被释放。新建一个窗体,我们就可以看到如下图所示的属性编辑器了,看起来还是很漂亮的
其它新的属性编辑器特性
这里我们主要讲了自绘画的属性编辑器,但Delphi 5还提供了许多其它的新特性,感兴趣的朋友可以自己进行一下研究。
比如新增的GetVisualValue方法类似于GetValue方法,但它可以用来返回一个比简单转换的字符串更有意义的描述字符串(但不能用来编辑)。有的时候,我们可能想用更有意义的字符串,而不是简单的转换来表示属性值,这时我们通过重载GetVisualValue方法来返回一个用于显示的字符串,用GetValue方法来返回一个用于编辑的字符串。
还有一个新的标志paFullWidthName置位的话,对象编辑器会完全显示属性名,而不会给属性值留出空间。第一眼的印象是这好象是一个很奇怪的特性,但很多控件都有类似于About的属性(尤其是商业控件),这些属性(是PaDialog类型的)只是在左边显示一个带省略号的按钮。 点击属性编辑器只是显示一个关于的对话框,它没有什么特别有意义的值,这时paFullWidthName标志就有用了,重载PropDrawName 方法我们可以把自己公司的Logo,显示在属性名的旁边。
最后,我想要发发牢骚,Borland的这些新的特性从来就没有很好的文档(这点上borland比微软可是差了好几个数量级),所以要想研究的话,只能是多看看Delphi带的源代码了:(,不过不管怎么说,使用这些新的特性我们可以作出非常Cool的编辑界面来,这才是我们最关心的。