自制精美易用的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张图片

  不难看出,当用户移动输入焦点时,对不同的移动方向要用不同的操作方法,甚至可能必须使用多个不同的键或借助鼠标来完成一个操作。当有下拉列表和要斜向移动的时候这种问题尤为明显。因此,输入困难的根本原因是其状态图过于复杂和不一致。基于这种认识,我们可以对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一样在焦点位置处放一个黑框——这一点是可以实现的(如图)。

  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.


你可能感兴趣的:(自制精美易用的DBGrid)