自制精美易用的DBGrid

看了以上這麼多的技巧和方法,想必大家未免會有一種衝動吧-自己動手做一個DBGrid,下面就介紹一種自制DBGrid的方法啦。

    Delphi中的TDBGrid是一個使用頻率很高的VCL元件。TDBGrid有許多優良的特性,例如它是數據綁定的,能夠定義功能強大的永久字段,事件豐富等,特別是使用非常簡單。但是,與FoxPro、VB 、PB中的DBGrid相比就會發現,TDBGrid也有明顯的缺陷:它的鍵盤操作方式非常怪異難用。雖然很多人都通過編程把回車鍵轉換成Tab鍵來改進TDBGrid的輸入方式,但是仍然不能很好地解決問題,這是爲什麼呢?本文將對造成這種缺陷的根本原因進行分析,並在此基礎上製作一個輸入極其簡便、界面風格類似Excel的DBGridPro元件。

     DBGrid的格子(Cell)有四種狀態:輸入狀態(有輸入光標,可以輸入,記作狀態A1);下拉狀態(彈出了下拉列表,可以選擇,記作狀態A2);高亮度狀態(沒有輸入光標,可以輸入,記作狀態B);顯示狀態(不能輸入,記作狀態C)。DBGrid接受的控制鍵有回車,Tab,Esc,以及方向鍵。據此可以畫出每個Cell的狀態轉換圖:

    不難看出,當用戶移動輸入焦點時,對不同的移動方向要用不同的操作方法,甚至可能必須使用多個不同的鍵或藉助鼠標來完成一個操作。當有下拉列表和要斜向移動的時候這種問題尤爲明顯。因此,輸入困難的根本原因是其狀態圖過於複雜和不一致。基於這種認識,我們可以對DBGrid作三點改造:

    改造1:顯然B狀態是毫無意義的,應該去掉。這意味着焦點每進入一個新的Cell,就立即進入編輯狀態,而不要再按回車了。每個進入狀態B的Cell都需要重新繪製,因此我們可以在繪製動作中判斷是否有狀態爲gdFocused的Cell,若有則設置EditorMode爲真。值得注意的是,TDBGrid用來畫Cell的函數DefaultDrawColumnCell並不是虛函數,因此不能通過繼承改變其行爲,而只能使用其提供的事件OnDrawColumnCell來插入一些動作。在DBGridPro中,這一點是通過實現顯示事件OnDrawColumnCell來實現的。但是這樣一來,外部對象就不能使用該事件了,所以提供了一個OnOwnDrawColumnCell事件來替代它。見代碼中的Create和DefaultDrawColumnCell函數。

    改造2:控制鍵應該簡化,儘量增加每個控制鍵的能力。在DBGridPro中,強化了方向鍵和回車鍵的功能:當光標在行末行首位置時,按方向鍵就能跳格;回車能橫向移動輸入焦點,並且還能彈出下拉列表(見改造3)。在實現方法上,可以利用鍵盤事件API(keybd_event)來將控制鍵轉換成TDBGrid的控制鍵(如在編輯狀態中回車,則取消該事件並重新發出一個Tab鍵事件)。當監測到左右方向鍵時,通過向編輯框發送EM_CHARFROMPOS消息判斷編輯框中的光標位置,以決定是否應該跳格。見代碼中的DoKeyUped函數。

    改造3:簡化下拉類型Cell的輸入方式。在DBGridPro中,用戶可以用回車來彈出下拉列表。這種方式看起來可能會造成的回車功能的混淆,但是隻要處理得當,用戶會覺得非常方便:當進入下拉類型的Cell之後,如果用戶直接鍵入修改,則按回車進入下一格;否則彈出下拉列表,選擇之後再按回車時關閉下拉列表並立即進入下一格。見代碼中的DoKeyUped函數和DefaultDrawColumnCell函數。

    一番改造之後,用戶輸入已經非常方便了,但是又帶來了新的問題:在TDBGrid中,用戶可以通過高亮度的Cell很快知道焦點在哪裏,而DBGridPro中根本不會出現這種Cell,所以用戶可能很難發現輸入焦點!一種理想的方法是像Excel一樣在焦點位置處放一個黑框--這一點是可以實現的(如圖2)。

    Windows中提供了一組API,用於在窗口上建立可接受鼠標點擊事件的區域(Region)。多個Region可以以不同的方式組合起來,從而得到"異型"窗口,包括空心窗口。DBGridPro就利用了這個功能。它在內部建立了一個黑色的Panel,然後在上面設置空心的Region,並把它"套"在有輸入焦點的Cell上,這樣用戶就能看到一個醒目的邊框了。

    好事多磨,現在又出現了新的問題:當Column位置或寬度改變時,其邊框必須同步變化。僅利用鼠標事件顯然不能完全解決這個問題,因爲在程序中也可以設置Column的寬度;用事件OnDrawColumnCell也不能解決(寬度改變時並不觸發該事件)。幸運的是,TDBGrid中的輸入框實際上是一個浮動在它上面的TDBGridInplaceEdit(繼承自TInplaceEdit),如果我們能監測到TDBGridInplaceEdit在什麼時候改變大小和位置,就可以讓邊框也跟着改變了。要實現這一點,用一個從TDBGridInplaceEdit繼承的、處理了WM_WINDOWPOSCHANGED消息的子類來替換原來的TDBGridInplaceEdit將是最簡單的辦法。通過查看源代碼發現,輸入框由CreateEditor函數創建的,而這是個虛函數--這表明TDBGrid願意讓子類來創建輸入框,只要它是從TInplaceEdit類型的。從設計模式的角度來看,這種設計方法被稱爲"工廠方法"(Factory Method),它使一個類的實例化延遲到其子類。看來現在我們的目的就要達到了。

    不幸的是,TDBGridInplaceEdit在DBGrids.pas中定義在implement中(這樣外部文件就無法看到其定義了),因此除非把它的代碼全部拷貝一遍,或者直接修改DBGrids.pas文件(顯然這前者不可取;後者又會帶來版本兼容性問題),我們是不能從TDBGridInplaceEdit繼承的。難道就沒有好辦法了嗎?當然還有:我們可以利用TDBGridInplaceEdit的可讀寫屬性WindowProc來捕獲WM_WINDOWPOSCHANGED消息。WindowProc實際上是一個函數指針,它指向的函數用來處理髮到該窗口元件的所有消息。於是,我們可以在CreateEditor中將創建出的TDBGridInplaceEdit的WndProc替換成我們自己實現的勾掛函數的指針,從而實現和類繼承相同的功能。這樣做的缺點是破壞了類的封裝性,因爲我們不得不在DBGridPro中處理屬於TDBGridInplaceEdit的工作。當然,可能還有其他更好的方法,歡迎讀者提出建議。

    至此,TDBGrid已經被改造成一個操作方便、界面美觀的DBGridPro了,我們可以把它註冊成VCL元件使用。以下是它的源代碼:


unit DBGridPro;

interface

uses
  Windows, Messages, SysUtils, Classes, Controls, Grids, DBGrids, ExtCtrls, richEdit, DBCtrls, DB;

type TCurCell = Record {當前焦點Cell的位置}
  X : integer; {有焦點Cell的ColumnIndex}
  Y : integer; {有焦點Cell所在的紀錄的紀錄號}
  tag : integer; {最近進入該Cell後是否彈出了下拉列表}
  r : TRect; {沒有使用}
end;

type
  TDBGridPro = class(tcustomdbgrid)
  private
    hr,hc1 : HWND; {創建空心區域的Region Handle}
    FPan : TPanel; {顯示黑框用的Panel}
    hInplaceEditorWndProc : TWndMethod; {編輯框原來的WindowProc}
    {勾掛到編輯框的WindowProc}
    procedure InPlaceEditorWndProcHook(var msg : TMessage);
    procedure AddBox; {顯示邊框}
    {實現TCustomDBGrid的OnDrawColumnCell事件}
    procedure DoOwnDrawColumnCell(Sender: TObject; const Rect: TRect; DataCol: Integer; Column: TColumn; State: TGridDrawState);
    {處理鍵盤事件}
    procedure DoKeyUped(Sender: TObject; var Key: Word; Shift: TShiftState);

  protected
    curCell : TCurCell; {記錄當前有焦點的Cell}
    FOwnDraw : boolean; {代替TCustomDBGrid.DefaultDrawing}
    FOnDraw : TDrawColumnCellEvent; {代替TCustomDBGrid.OnDrawColumnCell}
    function CreateEditor : TInplaceEdit; override;
    procedure KeyUp(var Key: Word; Shift: TShiftState); override;
    procedure DefaultDrawColumnCell(const Rect: TRect;DataCol: Integer; Column: TColumn; State: TGridDrawState); overload;

  public
    constructor Create(AOwner : TComponent); override;
    destructor Destroy; override;

  published
    property Align;
    property Anchors;
    property BiDiMode;
    property BorderStyle;
    property Color;
    property Columns stored False; //StoreColumns;
    property Constraints;
    property Ctl3D;
    property DataSource;
    property OwnDraw : boolean read FOwnDraw write FOwnDraw default false;
    property DragCursor;
    property DragKind;
    property DragMode;
    property Enabled;
    property FixedColor;
    property Font;
    property ImeMode;
    property ImeName;
    property Options;
    property ParentBiDiMode;
    property ParentColor;
    property ParentCtl3D;
    property ParentFont;
    property ParentShowHint;
    property PopupMenu;
    property ReadOnly;
    property ShowHint;
    property TabOrder;
    property TabStop;
    property TitleFont;
    property Visible;
    property OnCellClick;
    property OnColEnter;
    property OnColExit;
    property OnColumnMoved;
    property OnDrawDataCell; { obsolete }
    property OnOwnDrawColumnCell : TDrawColumnCellEvent read FOnDraw write FOnDraw;
    property OnDblClick;
    property OnDragDrop;
    property OnDragOver;
    property OnEditButtonClick;
    property OnEndDock;
    property OnEndDrag;
    property OnEnter;
    property OnExit;
    property OnKeyup;
    property OnKeyPress;
    property OnKeyDown;
    property OnMouseDown;
    property OnMouseMove;
    property OnMouseUp;
    property OnStartDock;
    property OnStartDrag;
    property OnTitleClick;
end;

procedure Register;

implementation

procedure Register;
begin
  RegisterComponents('Data Controls', [TDBGridPro]);
end;

{ TDBGridPro }
procedure TDBGridPro.AddBox;
var
  p,p1 : TRect;
begin
  GetWindowRect(InPlaceEditor.Handle,p);
  GetWindowRect(FPan.Handle,p1);
  if (p.Left=p1.Left) and (p.Top=p1.Top) and (p.Right=p1.Right) and (p.Bottom=p1.Bottom) then exit;
  if hr<>0 then DeleteObject(hr);
  if hc1<>0 then DeleteObject(hc1);
 {創建內外兩個Region}
  hr := CreateRectRgn(0,0,p.Right-p.Left+4,p.Bottom-p.Top+4);
  hc1:= CreateRectRgn(2,2,p.Right-p.Left+2,p.Bottom-p.Top+2);
  {組合成空心Region}
  CombineRgn(hr,hc1,hr,RGN_XOR);
  SetWindowRgn(FPan.Handle,hr,true);
  FPan.Parent := InPlaceEditor.Parent;
  FPan.ParentWindow := InPlaceEditor.ParentWindow;
  FPan.Height := InPlaceEditor.Height+4;
  FPan.Left := InPlaceEditor.Left-2;
  FPan.Top :=InPlaceEditor.Top-2;
  FPan.Width := InPlaceEditor.Width+4;
  FPan.BringToFront;
end;

constructor TDBGridPro.Create(AOwner: TComponent);
begin
  inherited;
  {創建作爲邊框的Panel}
  FPan := TPanel.Create(nil);
  FPan.Parent := Self;
  FPan.Height := 0;
  FPan.Color := 0;
  FPan.Ctl3D := false;
  FPan.BevelInner := bvNone;
  FPan.BevelOuter := bvNone;
  FPan.Visible := true;
  DefaultDrawing := false;
  OnDrawColumnCell := DoOwnDrawColumnCell;
  OnOwnDrawColumnCell := nil;
  curCell.X := -1;
  curCell.Y := -1;
  curCell.tag := 0;
  hr := 0;
  hc1 := 0;
end;

function TDBGridPro.CreateEditor: TInplaceEdit;
begin
  result := inherited CreateEditor;
  hInPlaceEditorWndProc := result.WindowProc;
  result.WindowProc := InPlaceEditorWndProcHook;
end;

procedure TDBGridPro.DefaultDrawColumnCell(const Rect: TRect; DataCol: Integer; Column: TColumn; State: TGridDrawState);
begin
  {如果要畫焦點,就讓DBGrid進入編輯狀態}
  if (gdFocused in State) then
  begin
    EditorMode := true;
    AddBox;
    {如果是進入一個新的Cell,全選其中的字符}
    if (curCell.X <> DataCol) or (curCell.Y <> DataSource.DataSet.RecNo)
    then begin
      curCell.X := DataCol;
      curCell.Y := DataSource.DataSet.RecNo;
      curCell.tag := 0;
      GetWindowRect(InPlaceEditor.Handle,curCell.r);
      SendMessage(InPlaceEditor.Handle,EM_SETSEL,0,1000);
    end;
    end else {正常顯示狀態的Cell}
  TCustomDBGrid(Self).DefaultDrawColumnCell(Rect,DataCol,Column,State);
  end;

destructor TDBGridPro.Destroy;
begin
  FPan.Free;
  inherited;
end;

procedure TDBGridPro.DoKeyUped(Sender: TObject; var Key: Word; Shift: TShiftState);
var
  cl : TColumn;
begin
  cl := Columns[SelectedIndex];
  case Key of
    VK_RETURN:
    begin
    {一個Column爲下拉類型,如果:
      1 該Column的按鈕類型爲自動類型
      2 該Column的PickList非空,或者其對應的字段是lookup類型}
    if (cl.ButtonStyle=cbsAuto) and ((cl.PickList.Count>0) or (cl.Field.FieldKind=fkLookup)) and (curCell.tag = 0) and not (ssShift in Shift) then
    begin
    {把回車轉換成Alt+向下彈出下拉列表}
      Key := 0;
      Shift := [ ];
      keybd_event(VK_MENU,0,0,0);
      keybd_event(VK_DOWN,0,0,0);
      keybd_event(VK_DOWN,0,KEYEVENTF_KEYUP,0);
      keybd_event(VK_MENU,0,KEYEVENTF_KEYUP,0);
      curCell.tag := 1;
      exit;
    end;
    {否則轉換成Tab}
    Key := 0;
    keybd_event(VK_TAB,0,0,0);
    keybd_event(VK_TAB,0,KEYEVENTF_KEYUP,0);
  end;
  VK_RIGHT :
  begin
  {獲得編輯框中的文字長度}
  i := GetWindowTextLength(InPlaceEditor.Handle);
  {獲得編輯框中的光標位置}
  GetCaretPos(p);
  p.x := p.X + p.Y shr 16;
  j := SendMessage(InPlaceEditor.Handle,EM_CHARFROMPOS,0,p.X);
  if (i=j) then {行末位置}
    begin
      Key := 0;
      keybd_event(VK_TAB,0,0,0);
      keybd_event(VK_TAB,0,KEYEVENTF_KEYUP,0);
    end;
  end;
  VK_LEFT:
  begin
    GetCaretPos(p);
    p.x := p.X + p.Y shr 16;
    if SendMessage(InPlaceEditor.Handle,EM_CHARFROMPOS,0,p.X)=0 then
    begin {行首位置}
      Key := 0;
      keybd_event(VK_SHIFT,0,0,0);
      keybd_event(VK_TAB,0,0,0);
      keybd_event(VK_TAB,0,KEYEVENTF_KEYUP,0);
      keybd_event(VK_SHIFT,0,KEYEVENTF_KEYUP,0);
    end;
  end;
  else begin {記錄用戶是否作了修改}
    if (Columns[SelectedIndex].PickList.Count>0) and (curCell.tag = 0) then
      if SendMessage(InPlaceEditor.Handle,EM_GETMODIFY,0,0)=1 then
        curCell.tag := 1;
    end;
  end;
end;

procedure TDBGridPro.DoOwnDrawColumnCell(Sender: TObject; const Rect: TRect; DataCol: Integer; Column: TColumn; State: TGridDrawState);
begin
  if FOwnDraw=false then DefaultDrawColumnCell(Rect,DataCol,Column,State);
  if @OnOwnDrawColumnCell<>nil then OnOwnDrawColumnCell(Sender,Rect,DataCol, Column,State);
end;

procedure TDBGridPro.InPlaceEditorWndProcHook(var msg: TMessage);
var m : integer;
begin
  m := msg.Msg;
  {=inherited}
  hInplaceEditorWndProc(msg);
  {如果是改變位置和大小,重新加框}
  if m=WM_WINDOWPOSCHANGED then AddBox;
end;

procedure TDBGridPro.KeyUp(var Key: Word; Shift: TShiftState);
begin
  inherited;
  DoKeyUped(Self,Key,Shift);
end;

end.
發佈了1 篇原創文章 · 獲贊 1 · 訪問量 5萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章