程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> 更多編程語言 >> Delphi >> 支持自繪畫的屬性編輯器

支持自繪畫的屬性編輯器

編輯:Delphi

      我的主頁: http://www.tommstudio.com/

        屬性編輯器對於大多數Delphi程序員來說無疑是很熟悉的,在對象編輯器的內核中有著大量的屬性編輯器,每個對象編輯器中的屬性都對應一個屬性編輯器類的實例。 
  
  Delphi5中提供了一些新的高級特性,使我們能夠定義新的屬性編輯器,為以有的屬性提供新的功能,或者設定和顯示新的控件的新的屬性的顯示方法。在Delphi5以前,對象編輯器只能夠以文本的形式顯示屬性值。在Delphi 5中給屬性編輯器提供了新的特性,使我們能夠以任何形式顯示屬性的名稱和值,如下圖所示如果屬性有一個下拉列表,我們就可以為每一個列表項添加一個圖標。下面我們就來研究一下如何實現屬性編輯器的自繪畫的功能。
  
  
  屬性編輯刷新器
  
  所有的屬性編輯器都是從TpropertyEditor繼承下來的。我們可以為特定的屬性類型、屬性名或控件注冊一個屬性編輯器。對象編輯器檢查每一個要顯示的屬性的名稱和類型,選擇合適的屬性編輯器類。然後它會創建這個類的一個實例(每個屬性對應一個實例)。當我們選擇了另一個控件,對象編輯器會釋放全部的屬性編輯器對象,然後為新的控件創建新的對象。
  
  屬性編輯器可以決定如何顯示屬性的值以及用戶如何設定一個新的屬性值。比如,TintegerProperty調用IntToStr函數以字符串的形式顯示整數值並用StrToInt函數來轉換用戶輸入的新值。 當用戶輸入了一個新的屬性值時,TcolorProperty同樣使用一個整型值來表示,但把整數解釋為顏色,並盡可能地映射顏色值為一個名稱(如clBlack或clBtnFace) 。
  
  一個屬性編輯器實現上述功能是通過重載TpropertyEditor的一個或多個方法來實現的。絕大多數的屬性編輯器需要重載GetValue方法,GetValue方法獲得屬性值的字符串形式。以及SetValue方法,SetValue方法把一個字符串轉化為屬性值。要想了解關於編寫屬性編輯器的進一步信息,需要仔細研究DsgnIntf.pas文件(在Delphi5SourceToolsapi目錄下)以及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 = nilor 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[Falsethen
    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的編輯界面來,這才是我們最關心的。
  
  
 
  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved