• 自制精美易用的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.

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

  • 相关阅读:
    Linux IO模型
    linux进程管理
    shell之判断文件是否存在
    python之hashlib模块(MD5校验)
    Docker实现退出container后保持继续运行的解决办法
    Pycharm上python运行和unittest运行两种执行方式解析
    Linux du与df命令的差异
    Linux lsof命令详解
    Selenium执行完毕未关闭chromedriver/geckodriver进程的解决办法(java版+python版)
    理解Python中的闭包
  • 原文地址:https://www.cnblogs.com/beeone/p/1792374.html
Copyright © 2020-2023  润新知