导航

自制精美易用的DBGrid

Posted on 2010-08-04 16:44  beeone  阅读(393)  评论(0编辑  收藏  举报

了以上这么多的技巧和方法,想必大家未免会有一种冲动吧-自己动手做一个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.

{以上代码在Windows2000,Delphi6上测试通过}