65.9K
CodeProject 正在变化。 阅读更多。
Home

改进 Delphi TDBGrid

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.92/5 (14投票s)

2011 年 5 月 20 日

CPOL

8分钟阅读

viewsIcon

86160

downloadIcon

3154

通过添加一些新功能来改进 Delphi 的 TDBGrid。

引言

TDBGrid 自发布以来已经过去很久了,直到现在,这个组件的行为也没有发生重大变化。在本文中,我们将添加一些功能,使 TDBGrid 更加用户友好,易于使用。其中一些是新的,一些可以在互联网上找到。

背景

以下是一些可以增强传统 DBGrid 并使其更用户友好的功能:

  • 根据单元格类型设置渐变背景
  • string 字段集成搜索功能
  • 列排序
  • 鼠标悬停高亮
  • 自动调整列宽
  • 行和列大小调整
  • 支持鼠标滚轮
  • 单元格自动换行
  • 音效
  • 集成且可配置的弹出菜单,用于对单个记录应用命令
  • 支持双向模式
  • 加载和保存配置

用于为我们的网格提供新外观的库是 GDI+。Delphi 中有一些免费的 GDI+ 翻译,但本组件中使用的翻译可以从 www.progdigy.com 下载,不过它已经包含在项目中了。请记住,此组件是在 Delphi 7 中开发的,其排序功能仅适用于 TCustomADODataset 的后代。它生成的默认过滤表达式与 TCustomADODataset 兼容,但在应用过滤器之前有一个用于操作过滤表达式的事件。请注意,在此组件中,我们假定第一列和第一行是固定的,其他行是数据内容,因此如果您需要其他条件,请根据需要更改 DrawCell。最终产品看起来像这样

外观

为了创建渐变位图背景,我们首先应该在内存中创建它。然后,我们将 StrechDraw 它到每个相关的单元格。要绘制垂直渐变,只需生成一个宽度为 1 像素、高度任意的位图,高度可以从 1 像素到例如 50 像素不等。此高度将被称为 **Step**,它与渐变的分辨率具有相同的含义。函数 drawVerticalGradient 接收一个位图变量,为其分配内存,然后根据“开始颜色”、“中心颜色”和“结束颜色”绘制一个渐变条,其中心位置是可调的。

procedure TEnhDBGrid.drawVerticalGradient(var grBmp: TBitmap; gHeight: integer;
  color1, color2, color3: TColor; centerPosition: integer);
var
  graphics: TGPGraphics;
  linGrBrush: TGPLinearGradientBrush;
  r1, g1, b1, r2, g2, b2, r3, g3, b3: byte;
  colors: array [0 .. 2] of TGPColor;
  blendPoints: array [0 .. 2] of single;
begin
  try
    if Assigned(grBmp) then
      grBmp.Free;
    grBmp := TBitmap.create;

    grBmp.Width := 1;
    grBmp.Height := gHeight;

    extractRGB(color1, r1, g1, b1);
    extractRGB(color2, r2, g2, b2);
    extractRGB(color3, r3, g3, b3);
    graphics := TGPGraphics.create(grBmp.Canvas.Handle);
    linGrBrush := TGPLinearGradientBrush.create(MakePoint(0, 0),
      MakePoint(0, gHeight), MakeColor(255, 255, 255, 255),
      MakeColor(255, 255, 255, 255));

    colors[0] := MakeColor(r1, g1, b1);
    blendPoints[0] := 0;
    colors[1] := MakeColor(r2, g2, b2);
    blendPoints[1] := centerPosition / 100;
    colors[2] := MakeColor(r3, g3, b3);
    blendPoints[2] := 1;

    linGrBrush.SetInterpolationColors(@colors, @blendPoints, 3);

    graphics.FillRectangle(linGrBrush, 0, 0, 1, gHeight);

    linGrBrush.Free;
    graphics.Free;
  except
    OutputDebugString('Error in creating gradient.');
  end;
end;

我们有这 5 种渐变,在必要时使用 StrechDraw 绘制它们。

    grBmpTitle: TBitmap;
    grBmpSelected: TBitmap;
    grBmpActive: TBitmap;
    grBmpAlt1: TBitmap;
    grBmpAlt2: TBitmap;

grBmpTitle 用于固定单元格背景。grBmpSelected 用于绘制选定项背景,grBmpActive 用于绘制活动单元格背景,grBmpAlt1grBmpAlt1 用于交替绘制普通行背景。

使用这些位图之一的过程是重写的 DrawColumnCell 过程。

    row := DataSource.DataSet.recNo;

    if (gdSelected in State) then
    begin
      Canvas.StretchDraw(Rect, grBmpActive);
      tempFont.Color:=FActiveCellFontColor;
    end
    else if isMultiSelectedRow then
    begin
      Canvas.StretchDraw(Rect, grBmpSelected);
      tempFont.Color:=FSelectedCellFontColor;
    end
    else if Odd(row) then
      Canvas.StretchDraw(Rect, grBmpAlt1);
    else
      Canvas.StretchDraw(Rect, grBmpAlt2);

    if Column.Field<>nil then
      myDrawText(Column.Field.DisplayText, Canvas, Rect, Column.alignment, tempFont);

myDrawText 透明地绘制一个 string,如果其宽度大于绘图矩形宽度,它将换行,并在后续行中尽可能多地写入单词。

要绘制标题栏、固定单元格和指示器,我们应该重写 DrawCell。指示器形状在 Data.res 中,它是项目的一部分。

    if ARow > 0 then  //draw contents
    begin

      if ACol = 0 then  // draw indicators
      begin
        dec(ARow);
        Canvas.StretchDraw(ARect, grBmpTitle);
        // shape borders like a button
        DrawEdge(Canvas.Handle, ARect, BDR_RAISEDOUTER, BF_RECT);

        if (gdFixed in AState) then
        begin
          if Assigned(DataLink) and DataLink.Active  then
          begin
            MultiSelected := False;
            if ARow >= 0 then
            begin
              prevousActive := DataLink.ActiveRecord;
              try
                Datalink.ActiveRecord := ARow;
                MultiSelected := isMultiSelectedRow;
              finally
                Datalink.ActiveRecord := prevousActive;
              end;
            end;
            if (ARow = DataLink.ActiveRecord) or MultiSelected then
            begin
              indicIndex := 0;
              if DataLink.DataSet <> nil then
                case DataLink.DataSet.State of
                  dsEdit: indicIndex := 1;
                  dsInsert: indicIndex := 2;
                  dsBrowse:
                    if MultiSelected then
                      if (ARow <> Datalink.ActiveRecord) then
                        indicIndex := 3
                      else
                        indicIndex := 4;  // multiselected and current row
                end;
              myIndicators.BkColor := FixedColor;
              myLeft := ARect.Right - myIndicators.Width - 1;
              if Canvas.CanvasOrientation = coRightToLeft then Inc(myLeft);
              myIndicators.Draw(Canvas, myLeft,
                (ARect.Top + ARect.Bottom - myIndicators.Height) shr 1, 
		indicIndex, dsTransparent, itImage,True);
            end;
          end;
        end;
        inc(ARow);
      end
      else // draw grid content
        inherited;
    end
    else // draw titles
    begin
      // draw title gradient bitmap
      Canvas.StretchDraw(ARect, grBmpTitle);

      ar:=ARect;
      // shape borders like a button
      DrawEdge(Canvas.Handle, AR, BDR_RAISEDOUTER, BF_RECT);

      // write title
      if ACol > 0 then
        myDrawText(Columns[ACol - 1].Title.Caption, 
	Canvas, AR, Columns[ACol - 1].Title.Alignment , Columns[ACol - 1].Title.Font)
    end;

myDrawText 使用 DrawText API,因为它具有对齐和自动换行功能。

procedure TEnhDBGrid.myDrawText(s:string; outputCanvas: Tcanvas; drawRect: TRect;
                  drawAlignment:TAlignment ; drawFont:TFont);
const
  drawFlags : array [TAlignment] of Integer =
    (DT_WORDBREAK or DT_LEFT  or DT_NOPREFIX,
     DT_WORDBREAK or DT_RIGHT  or DT_NOPREFIX,
     DT_WORDBREAK or DT_CENTER or DT_NOPREFIX );
var
  r:trect;
  bw, bh, cw, ch, difX:integer;
begin
    if s='' then
      exit;

    if UseRightToLeftAlignment then
      case drawAlignment of
        taLeftJustify:  drawAlignment := taRightJustify;
        taRightJustify: drawAlignment := taLeftJustify;
      end;

    r:= drawRect;
    cw:=ClientWidth;
    ch:=ClientHeight;

    //set dimensions for output
    bmpDrawText.Width:=( r.Right - r.Left);
    bmpDrawText.Height:=r.Bottom- r.Top;
    bw:=bmpDrawText.Width;
    bh:=bmpDrawText.Height;

    //set drawing area in output bmp
    drawRect.Left:=0;
    drawRect.Top:=0;
    drawRect.Right:=bw;
    drawRect.Bottom:=bh;

    // if the drawing font color is same as transparent color
    //change transparent color
    if ColorToRGB( drawFont.Color )=(ColorToRGB
	( bmpDrawText.TransparentColor) and $ffffff) then
       toggleTransparentColor;

    //to make entire surface of canvas transparent
    bmpDrawText.Canvas.FillRect(drawRect);

    //shrink the rectangle
    InflateRect(drawRect, -2,-2);

    bmpDrawText.Canvas.Font:= drawFont;

    DrawText(bmpDrawText.Canvas.Handle,
               pchar(s), length(s), drawRect,
               drawFlags[drawAlignment]
               );

    if UseRightToLeftAlignment then
    begin
       if r.Right > ClientWidth then
       begin
          bmpClipped.Width:=cw-r.Left;
          bmpClipped.Height:=bh;
          bmpClipped.Canvas.CopyRect(bmpClipped.Canvas.ClipRect, 
		bmpDrawText.Canvas, Rect(bw, 0, bw-( cw - r.Left ), bh) );
          outputCanvas.StretchDraw(rect(r.Left , r.Top, cw, r.Bottom), bmpClipped);
       end
       else
          outputCanvas.StretchDraw(Rect(r.Right, r.Top, r.Left, r.Bottom), bmpDrawText);
    end
    else
       outputCanvas.Draw(r.Left, r.top, bmpDrawText);
end;

BiDiModeRightToLeft 时,Canvas.Draw 会反转绘制我们的 bmpDrawText。要解决此问题,应使用一个矩形调用 StretchDraw,该矩形的左右边界坐标已互换。

搜索

如果用户想搜索 string 字段,就像右键单击 string 字段标题栏并键入所需语句的一部分一样简单。它将更新 datasetSort 属性,以仅显示所需的结果。用户可以通过按 Escape 键取消过滤。
因此,我们在 Create 过程中创建一个 TEditBox 和一些控制变量开始。

  edtSearchCriteria := TEdit.create(Self);
  edtSearchCriteria.Width := 0;
  edtSearchCriteria.Height := 0;
  edtSearchCriteria.Parent := Self;
  edtSearchCriteria.Visible := false;
  searchVisible := false;

  lastEditboxWndProc := edtSearchCriteria.WindowProc;
  edtSearchCriteria.WindowProc := edtSearchCriteriaWindowProc;

  filtering := false;

下一步是检测标题栏上的鼠标右键单击并显示搜索编辑框。因此,我们重写 MouseDown 过程。

  // detect right clicking on a column title
  if (Button = mbRight) and FAllowFilter then
  begin
    for i := 0 to Columns.Count - 1 do
    begin
      r := CellRect(i + 1, 0);

      mp := CalcCursorPos;

      // if mouse in column title
      if pointInRect(mp, r) then
      begin
        if (Columns[i].Field.DataType = ftString) or
          (Columns[i].Field.DataType = ftWideString) then
        begin
          if not(filtering and (lastSearchColumn = Columns[i])) then
            ClearFilter;

          lastSearchColumn := Columns[i];
          edtSearchCriteria.Visible := true;
          searchVisible := true;

          if searchFieldName <> Columns[i].FieldName then
            searchFieldName := Columns[i].FieldName
          else
            edtSearchCriteria.Text := lastSearchStr;

          edtSearchCriteria.Font := Columns[i].Title.Font;

          edtSearchCriteria.Left := r.Left;
          edtSearchCriteria.top := r.top;
          edtSearchCriteria.Width := r.Right - r.Left;
          edtSearchCriteria.Height := r.bottom - r.top;

          filtering := true;
          LeftCol:=myLeftCol;
          windows.SetFocus(edtSearchCriteria.Handle);
          break;
        end;
      end;

    end;
  end;

如果您想将非 string 字段添加到允许的过滤字段中,您应该修改上述过程以处理这些字段。
为了在网格滚动时移动编辑框,我们只需在 DrawCell 过程中设置 edtSearchCriteria 坐标。

    // make search editbox visible if it is necessary
    if lastSearchColumn <> nil then
      if (ACol > 0) and (ARow = 0) then
      begin

        if searchVisible then
        begin
          edtSearchCriteria.Visible :=isVisibleColumn(lastSearchColumn);

          // reposition edit box
          if (Columns[ACol - 1].FieldName = searchFieldName) then
          begin
            // adjust search edit box position
            ar := CellRect(ACol, 0);
            if edtSearchCriteria.Visible then
            begin
              if UseRightToLeftAlignment then
                edtSearchCriteria.Left := ClientWidth - ARect.Right
              else
                edtSearchCriteria.Left := ARect.Left;
              edtSearchCriteria.Width := ARect.Right - ARect.Left;
            end;
          end;

        end
      end;

输入到编辑框中的 string 应该作为 Filter 应用到 Dataset。执行此操作的地方是处理传递给 edtSearchCriteria 的消息的 edtSearchCriteriaWindowProc

  // there was a change in search criteria
  if lastSearchStr<>edtSearchCriteria.Text then
  begin
    if filtering then
    begin
      plc := leftCol;
      lastSearchStr := edtSearchCriteria.Text;
      psp:=edtSearchCriteria.SelStart;

      if lastSearchStr <> '' then
      begin
        DataSource.DataSet.Filtered := false;

        critStr := '[' + searchFieldName + '] LIKE ''%' + lastSearchStr + '%''';
        //critStr := '[' + searchFieldName + '] = ''' + lastSearchStr + '*''';
        if Assigned(FOnBeforeFilter) then
          FOnBeforeFilter(Self, lastSearchColumn, lastSearchStr, critStr);
        DataSource.DataSet.Filter := critStr;

        try
          DataSource.DataSet.Filtered := true;
        except
          ShowMessage('Couldn''t filter data.');
        end;
      end
      else
      begin
        DataSource.DataSet.Filtered := false;
      end;

      leftCol := plc;
      if not edtSearchCriteria.Focused then
      begin
        windows.SetFocus(edtSearchCriteria.Handle);
        edtSearchCriteria.SelStart:=psp;
      end;
    end;
  end;

它在应用过滤器之前调用 OnBeforeFilter,以防用户想要操作 Filter string。此外,它还处理特殊字符,如 Escape 键和上下箭头键,以将焦点切换到网格。

  if Message.Msg = WM_KEYDOWN then
  begin

    if Message.WParam = VK_ESCAPE then
    begin

      playSoundInMemory(FEscSoundEnabled, sndEsc, 'Escape');

      // first escape disappears the search box
      // second escape disables searches and  sorting
      if searchVisible then
      begin
        // there are some remaining messages that cause windows to play an
        // exclamation sound because editbox is not visible after this.
        // by removing remaining messages we prevent that unwanted sounds
        while (GetQueueStatus(QS_ALLINPUT)) > 0 do
          PeekMessage(Msg, 0, 0, 0, PM_REMOVE);

        edtSearchCriteria.Visible := false;
        searchVisible := false;
        edtSearchCriteria.invalidate;
      end
      else
        ClearFilter;

    end
    else if (Message.WParam = VK_DOWN) then
    begin
      // if user presses down arrow it means that he/she needs to go forward
      // in records
      DataSource.DataSet.Next;
      windows.SetFocus(Handle);
    end
    else if (Message.WParam = VK_UP) then
    begin
      DataSource.DataSet.Prior;
      windows.SetFocus(Handle);
    end;

  end;

排序

虽然许多网站都提到了如何在 DBGrid 中排序数据,但我们在这里仍然会讨论它,因为 TEnhDBGrid 具有此功能,并且应该描述其功能。

排序仅适用于 TCustomADOGrid 的后代 DataSets。此网格对单击标题的每一列进行排序,并在此之后跟踪升序或降序模式。此外,它会显示一个箭头来指示排序的列和排序的类型。为此重写的过程是 TitleClick

Type
  TSortType = (stNone, stAsc, stDesc);

procedure TEnhDBGrid.TitleClick(Column: TColumn);
var
  p: pointer;
  plc: integer; // previous left column
begin
  inherited;

  if not(DataSource.DataSet is TCustomADODataSet) then
    Exit;

  plc := leftCol;
  p := DataSource.DataSet.GetBookmark;

  if lastSortColumn <> Column then
  begin
    // new column to sort
    lastSortColumn := Column;
    lastSortType := stAsc;
    try
      TCustomADODataset(DataSource.DataSet).Sort := '[' + Column.FieldName + '] ASC';
    except
      ShowMessage('Didn''t sorted !');
      lastSortColumn := nil;
      lastSortType := stNone;
    end;

  end
  else
  begin
    // reverse sort order
    if lastSortType = stAsc then
    begin
      lastSortType := stDesc;
      TCustomADODataset(DataSource.DataSet).Sort := '[' + Column.FieldName + '] DESC';
    end
    else
    begin
      lastSortType := stAsc;
      TCustomADODataset(DataSource.DataSet).Sort := '[' + Column.FieldName + '] ASC';
    end;
  end;

  if DataSource.DataSet.BookmarkValid(p) then
  begin
    DataSource.DataSet.GotoBookmark(p);
    DataSource.DataSet.FreeBookmark(p);
  end;
  leftCol := plc;
end;

为了显示指示排序列和排序类型的箭头,DrawCell 是执行此操作的合适位置。

    // draw an arrow in sorted columns
    if (lastSortColumn <> nil) then
      if (lastSortColumn.Index + 1 = ACol) and (ARow = 0) then
        drawTriangleInRect(ARect, lastSortType, Columns[ACol - 1].Title.Alignment);

drawTriangleInRect 顾名思义,根据排序类型在已排序列的标题中绘制一个三角形。如果您对如何显示标题栏中的排序类型有更艺术的想法,请根据您的意愿修改 drawTriangleInRect 过程。

鼠标悬停高亮

为了实现鼠标悬停高亮行为,我们应该确定鼠标下方的行号,并将数据库的 RecNo 移动到该位置。在原始的 DBgrid 中,记录编号和边界行矩形之间没有关系,因此我们不得不制作一个已绘制记录及其位置的列表,并在可见记录更改时更新它们。所以我们有一个行信息数组。

type 
  TRowInfo = record
    recNo,
    top,
    bottom: integer;
  end;

{**************************}
{     class members        }
{**************************}
  ri: array of TRowInfo;
  lastRowCount: integer;

每当 lastRowCountRowCount 不同时,我们都会重新分配数组并开始更新其内容。更新发生在 DrawColumnCell

    if RowCount <> lastRowCount then
    begin
      SetLength(ri, RowCount);
      lastRowCount := RowCount;
      // reset all records
      for i := 0 to RowCount - 1 do
      begin
        ri[i].recNo := -1;
        ri[i].top := 0;
        ri[i].bottom := 0;
      end;
    end;

    // find first empty rowInfo element or same row position
    // and store this row info
    for i := 0 to RowCount - 1 do
      if (ri[i].recNo = -1) OR
        ((ri[i].top = Rect.top) AND (ri[i].bottom = Rect.bottom)) then
      begin
        ri[i].recNo := row;
        ri[i].top := Rect.top;
        ri[i].bottom := Rect.bottom;
        break;
      end;

现在我们有了记录编号和行的可见位置之间的关系,所以我们可以通过重写 MouseMove 来在鼠标移动时进行鼠标悬停高亮。

  if FHotTrack then
  if DataSource.DataSet.State = dsBrowse then  	//do not bother user edit 
						//or insert operations
  begin
    // prevent repetitive mouse move events
    if (lastMouseX = X) and (lastMouseY = Y) then
      Exit
    else
    begin
      lastMouseX := X;
      lastMouseY := Y;
    end;

    // move to the suitable row
    // ri was filled in CellDraw
    for i := 0 to high(ri) do
      if (Y >= ri[i].top) and (Y <= ri[i].bottom) then
      begin

        if ri[i].recNo < 1 then
          continue;

        // movebackward or forward to reach to the pointer
        // you could set RecNo exactly to the desired no to
        // see the disastrous results
        
        if ri[i].recNo > DataSource.DataSet.recNo then
        begin
          while ri[i].recNo > DataSource.DataSet.recNo do
            DataSource.DataSet.Next;
          break;
        end
        else if ri[i].recNo < DataSource.DataSet.recNo then
        begin
          while ri[i].recNo < DataSource.DataSet.recNo do
            DataSource.DataSet.Prior;
          break;
        end
      end;

    // if row select is not enabled
    if not(dgRowSelect in Options) then
    begin
      // move to cell under mouse pointer
      gc := MouseCoord(X, Y);
      if (gc.X > 0) and (gc.Y > 0) then
      begin
        gr.Left := gc.X;
        gr.Right := gc.X;
        gr.top := gc.Y;
        gr.bottom := gc.Y;
        Selection := gr;
      end;
    end;
    // update indicator column
    InvalidateCol(0);
  end;

自动调整列宽

与排序一样,许多网站都提到了这一点,如果您不感兴趣,可以跳过此部分。
我们的自动宽度调整发生在用户双击列的右侧边界时,不仅仅是在标题栏上,当然也可以是整个列的右边框。
为了实现这一点,我们必须重写 DblClick 方法。

  plc := leftCol;
  p := CalcCursorPos;

  // find the column that should be auto widthed
  for i := 0 to Columns.Count - 1 do
  begin
    r := CellRect(i + 1, 0);
    // if you want just title DblClicks uncomment this line
    // if (p.Y>=r.Top) and (p.Y<=r.Bottom) then
    begin
      if (UseRightToLeftAlignment and (abs(p.X - r.Left) < 5)) or
        ((not UseRightToLeftAlignment) and (abs(p.X - r.Right) < 5)) then
      begin
        autoFitColumn(i, true);
        leftCol := plc;
        // don't allow an extra click event
        dblClicked := true;
        break;
      end
    end;
  end;

此外,用户可以通过双击零行零列的第一个单元格来自动调整所有列的宽度。

  // if cell is the corner one then autofit all columns
  if pointInRect(p, CellRect(0, 0)) then
  begin
    autoFitAll;
    Exit;
  end;

如上代码所示,左侧列索引被保留,并且在自动调整列宽度后不会改变。

行和列大小调整

为此,我们应该重写 CalcSizingState 过程,并允许父 Grid 对象调整行和列的大小。

决定是否允许调整列大小

  for i := myLeftCol - 1 to Columns.Count - 1 do
    if abs(getColumnRightEdgePos(Columns[i]) - X) < 5 then
    begin
      State := gsColSizing;
      Index := i + 1;
      if IsRightToLeft then
        SizingPos := ClientWidth - X
      else
        SizingPos := X;
      SizingOfs := 0;
    end;

决定是否允许调整行大小

  if FAllowRowResize then
    if State <> gsColSizing then
        for i := 0 to high(ri) do
        begin //search rows bottom line positions
          if (abs(ri[i].bottom - Y) < 3) and  (ri[i].bottom>0) then
          begin
            State := gsRowSizing;
            Index := i + 1;
            SizingPos := Y;
            SizingOfs := 0;
            lastResizedRow := Index;
            Break;
          end;
        end;

防止在单元格区域外进行大小调整

  if MouseCoord(x,y).X=-1 then
    exit;

支持鼠标滚轮

当鼠标滚轮滚动时,Windows 会向控件发送 WM_MOUSEWHEEL,我们应该将 dataset 当前记录移动到下一个或上一个位置,为了用户更舒适,如果用户在转动鼠标滚轮时按住 Ctrl 键,则水平滚动。我们将要重写的过程是 WndProc(var Message: TMessage)

  // the control should have focus to receive this message
  if Message.Msg = WM_MOUSEWHEEL then
  begin
    ctrlPressed := ((Message.WParam and $FFFF) and (MK_CONTROL)) > 0;

    if Message.WParam < 0 then
    begin
      if not checkDBPrerequisites then
        Exit;
      if ctrlPressed then
      begin
        // horizontal scroll
        incLeftCol;
      end
      else
      begin
        // vertical scroll
        if not DataSource.DataSet.Eof then
        begin
          DataSource.DataSet.Next;
          InvalidateCol(0);
        end;
      end;
    end
    else
    begin
      if not checkDBPrerequisites then
        Exit;
      if ctrlPressed then
        // horizontal scroll
        decLeftCol
      else
      begin
        // vertical scroll
        if not DataSource.DataSet.Bof then
        begin
          DataSource.DataSet.Prior;
          InvalidateCol(0);
        end;
      end;
    end;
  end;

控件必须获得焦点才能接收 WM_MOUSEWHEEL,因此我们必须提供一些自动获取焦点的方法,如果用户将指针移到此网格上。实现此功能的地点是 MouseMove

  // if need auto focus then take focus to this control
  if (not searchVisible) and FAutoFocus and (not Focused) then
    windows.SetFocus(Handle);

单元格自动换行

myDrawText 中,通过在调用 DrawText 时使用 DT_WORDBREAK 来实现自动换行。事实上,在 RightToLeft 模式下使用 DrawText 函数将文本直接绘制到控件画布上时,存在一些关于反转坐标的问题。为了解决这个问题,我们应该在 TBitmaps 画布上绘制文本,然后将其绘制到控件画布上。这样,我们也将获得双缓冲输出。

音效

用户将在“鼠标悬停”、“双击”、“排序”和“按 Escape 键”时听到声音。显然,我们应该在 KeyDownDblClickTitleClickScroll 过程中异步播放声音。我们将要调用的过程是 playSoundInMemory

procedure TEnhDBGrid.playSoundInMemory(cnd: boolean; m: TResourceStream;
  name: string);
begin
  try
    if cnd then
      sndPlaySound(m.Memory, SND_MEMORY or SND_ASYNC);
  except
    OutputDebugString(PChar('Error in playing ' + name + ' sound !'));
  end;
end;

声音嵌入在 Data.res 中,并在 Create 过程中加载。

  try
    sndHover := TResourceStream.create(HInstance, 'hover', RT_RCDATA);
    sndDblClick := TResourceStream.create(HInstance, 'dblclick', RT_RCDATA);
    sndSort := TResourceStream.create(HInstance, 'click', RT_RCDATA);
    sndEsc := TResourceStream.create(HInstance, 'esc', RT_RCDATA);
  except
    OutputDebugString('Error in loading sounds from resources');
  end;

集成且可配置的弹出菜单,用于对单个记录应用命令

程序员在放置 TDabaseGrid 后通常要做的常见工作是提供一些方法来对 dataset 的单个记录执行操作。为了简化这项工作,有一个可自定义的弹出菜单和一个回调机制,可以加速实现记录上的常见操作。

存储命令标题及其值的成员变量是 FPopupMenuCommands,它是一个 TStrings 对象,并保存 CommandTitle, CommandID 对的列表。

这将生成一个如下所示的弹出菜单:

当用户单击弹出菜单项时,控件会触发此事件。

TOnPopupCommandEvent = procedure(Sender: TObject; commandID, rowNo: integer ) of object;

虽然 TEnhDBGrid.DataSource.Dataset 显示当前活动行,但 dataset 的当前行号会传递给事件处理程序。

加载和保存配置

saveConfig(fn: String)loadConfig(fn: String) 这两个过程中,我们保存了此组件的一些视觉属性。可以修改它们以保存和加载您认为遗漏的其他属性。

结语

我希望这个组件能为您的数据库应用程序带来新的、有吸引力的外观。任何错误报告或建议都将受到欢迎和赞赏。已包含一个示例项目来测试此组件。

历史

  • 2011 年 5 月 20 日:初始发布
© . All rights reserved.