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.