Let us analyze your application - now have a free initial consultation.

Are you looking for a company to support or support your Delphi project?
Click here!

Jetzt Anfrage einreichenTelefon ERPwerk Button.fw

Nils Eilers, 09.05.2017

Grid Spaltenbreite

 

Improving Delphi's DBGrid: Automatic Column Width Adjustment

Delphi developers often find that Delphi's DBGrid component lacks certain desired functionalities. One of the most commonly missed features is the automatic adjustment of column widths at the click of a button. This example demonstrates a solution to this problem and shows how the relevant code can be used alongside the column sorting code from the article "Column Sorting with DBGrid" without conflicts.

Like the sorting functionality, this approach is just one of many possible solutions, and the specific requirements may lead to different implementations. This example is written with Delphi XE5 and uses FireDAC components.


Components Used

  • TDBGrid

  • TDBNavigator

  • TFDConnector

  • TDataSource

  • TFDQuery

  • TFDGUIxWaitCursor and TFDPhysMySQLDriverLink (Required for FireDAC with a MySQL database, can be ignored for other use cases)

The TFDConnection component is used to establish the connection to the server. TDBGrid and TDBNavigator are linked with TDataSource, which is connected to TFDQuery, which in turn is linked to TFDConnection.


TFDQuery Configuration

The query for accessing the test database is kept simple:

SELECT * 
FROM Artikel;

Adjusting Column Width Automatically

The following code is executed when the OnTitleClick event of the DBGrid is triggered:

procedure TForm1.DBGrid1TitleClick(Column: TColumn);
var
  i: Integer;
  TmpRecNo: Integer;
  NewColumnWidth: Integer;
  ColumnTitleWidth: Integer;
  ColumnCellWidth: Integer;
  ColTitleAdd: Integer;
  ColCellAdd: Integer;
  ColTitleMult: Real;
  ColCellMult: Real;
begin
  // Set modifiers for additional column width adjustment
  // Column width = Round(Base value * Multiplier) + Addition
  ColTitleAdd := 0;
  ColTitleMult := 1.10;
  ColCellAdd := 0;
  ColCellMult := 1.10;

  // Adjusting the column width
  ColumnTitleWidth := Round(DBGrid1.Canvas.TextExtent(Column.Title.Caption).cx * ColTitleMult) + ColTitleAdd;
  ColumnCellWidth := Round(DBGrid1.Canvas.TextExtent(Column.Field.DisplayText).cx * ColCellMult) + ColCellAdd;

  // Finding the widest cell
  TmpRecNo := FDQuery1.RecNo;
  FDQuery1.First;
  FDQuery1.DisableControls;
  for i := 0 to FDQuery1.RecordCount - 1 do
  begin
    if ColumnCellWidth < Round(DBGrid1.Canvas.TextExtent(Column.Field.DisplayText).cx * ColTitleMult) + ColTitleAdd then
      ColumnCellWidth := Round(DBGrid1.Canvas.TextExtent(Column.Field.DisplayText).cx * ColCellMult) + ColCellAdd;
    FDQuery1.Next;
  end;
  FDQuery1.RecNo := TmpRecNo;
  FDQuery1.EnableControls;

  // Use the title width if it is wider than the widest cell
  if ColumnTitleWidth > ColumnCellWidth then
    NewColumnWidth := ColumnTitleWidth
  else
    NewColumnWidth := ColumnCellWidth;

  Column.Width := NewColumnWidth;
end;

Explanation

  • This code adjusts the column width dynamically by determining the width of the widest cell or the column title.

  • Modifiers (ColTitleAdd, ColTitleMult, ColCellAdd, ColCellMult) allow you to fine-tune the width calculation.

  • The OnTitleClick event of the DBGrid is utilized to trigger the resizing.


Preventing Conflicts with Column Sorting

To avoid conflicts between the sorting functionality and the column width adjustment, both are implemented using the OnTitleClick event instead of the OnDblClick event. This is because OnDblClick would trigger OnTitleClick twice, causing undesired behavior.

Solution: Using TTimer Component

By defining a double-click independently from the DBGrid’s default settings, we can ensure compatibility with the sorting code. To achieve this, a TTimer component is required on the form along with two global variables:

private
  SingleClick: Boolean = True;
  SelectedColumn: TColumn;

These variables are used to transfer data between the OnTitleClick event and the Timer procedure.

Would you like me to proceed with explaining how to implement the timer-based solution to differentiate single-clicks from double-clicks and ensure compatibility with sorting? 😊

Objektinspector

 

 

 

If the TTimer is placed on the form, it must be disabled by setting its Enabled property to False. The Interval property determines how quickly the user must click to register a double-click. In this example, an Interval of 200 is chosen. Now, the following lines need to be added to the OnTimer event:

 

procedure TForm1.Timer1Timer(Sender: TObject);
begin
SingleClick := true;
Timer1.Enabled := false;
DBGrid1TitleClick(SelectedColumn);
end;

The combined code from the article dated May 5th, 2017, and the column sorting functionality, taking into account the double-click issue, can now look like this:

procedure TForm1.DBGrid1TitleClick(Column: TColumn);
var
   OldOrderString: String;
   NewOrderString: String;
   i: integer;
   TmpRecNo: Integer;
   NewColumnWidth: Integer;
   ColumnTitleWidth: Integer;
   ColumnCellWidth: Integer;
   ColTitleAdd: Integer;
   ColCellAdd: Integer;
   ColTitleMult: Real;
   ColCellMult: Real;
begin
   //Ausgewählte Spalte für den Timer speichern
   SelectedColumn := Column;

   //-- Spaltenbreite

//Der Timer erkennt den Doppelklick
If Timer1.Enabled = true then
begin
   //Festlegen der Modifikatoren für zusätzliche Anpassung der Spaltenbreite
   //Spaltenbreite = Round(Basiswert * Multiplikator) + Summand
   ColTitleAdd := 0;
   ColTitleMult := 1.10;
   ColCellAdd := 0;
   ColCellMult := 1.10;

   //Anpassung der Spaltenbreite
   ColumnTitleWidth := Round(DBGrid1.Canvas.TextExtent(Column.Title.Caption).cx * ColTitleMult) + ColTitleAdd;
   ColumnCellWidth := Round(DBGrid1.Canvas.TextExtent(Column.Field.DisplayText).cx * ColCellMult) + ColCellAdd;

   //Die breiteste Zelle finden
   TmpRecNo := FDQuery1.RecNo;
   FDQuery1.First;
   FDQuery1.DisableControls;
   For i := 0 to FDQuery1.RecordCount - 1 do
   begin
       If ColumnCellWidth < Round(DBGrid1.Canvas.TextExtent(Column.Field.DisplayText).cx * ColTitleMult) + ColTitleAdd then
          ColumnCellWidth := Round(DBGrid1.Canvas.TextExtent(Column.Field.DisplayText).cx * ColCellMult) + ColCellAdd;
       FDQuery1.Next;
       end;
       FDQuery1.RecNo := TmpRecNo;
       FDQuery1.EnableControls;

       //Falls der Titel breiter ist, als die breiteste Zeile, an diesem orientieren
       If ColumnTitleWidth > ColumnCellWidth then
          NewColumnWidth := ColumnTitleWidth
       Else
          NewColumnWidth := ColumnCellWidth;

          Column.Width := NewColumnWidth;

       Timer1.Enabled := false;
end
Else If SingleClick = false then
   Timer1.Enabled := true;
//-- Sortierung
If SingleClick = true then
begin
   //Speichern der aktuell ausgewählten Zeile
   //Nach dem Reaktivieren der Query wird sonst immer die erste Zeile markiert
   TmpRecNo := FDQuery1.RecNo;
   
   //Prüfung, ob bereits eine Sortierung vorhanden ist
   If pos('ORDER BY', Uppercase(FDQuery1.SQL.Text)) <> 0 then
   begin
       NewOrderString := 'ORDER BY ' + Column.FieldName;

       //Ersetze eine bereits vorhandene Sortierung durch die neue Spalte
       For i:= 0 to DBgrid1.Columns.Count - 1 do
       begin
          OldOrderString := 'ORDER BY ' + DBGrid1.Columns[i].FieldName;
          FDQuery1.SQL.Text := StringReplace(FDQuery1.SQL.Text, OldOrderString, NewOrderString, [rfReplaceAll, rfIgnoreCase]);

          //Prüfe, ob dieselbe Spalte noch einmal angeklickt wurde
          //Falls ja, wechsel zwischen auf- und absteigend
          If OldOrderString = NewOrderString then
          begin
              FDQuery1.SQL.Text := FDQuery1.SQL.Text;

              If pos(Uppercase(NewOrderString + ' ASC'), Uppercase(FDQuery1.SQL.Text)) <> 0 then
                FDQuery1.SQL.Text := StringReplace(FDQuery1.SQL.Text, NewOrderString + ' ASC', NewOrderString + ' DESC', [rfReplaceAll, rfIgnoreCase])
              Else
                FDQuery1.SQL.Text := StringReplace(FDQuery1.SQL.Text, NewOrderString + ' DESC', NewOrderString + ' ASC', [rfReplaceAll, rfIgnoreCase]);

           end;
        end;
   end
   Else
      FDQuery1.SQL.Append(' ORDER BY ' + Column.FieldName + ' ASC');

   FDQuery1.Active := true;

   //Springe zur markierten Zeile zurück
   FDQuery1.RecNo := TmpRecNo;

   //Die Spalte, nach der sortiert wird, fett machen und alle anderen wieder normal darstellen
   For i := 0 to DBGrid1.Columns.Count - 1 do
   begin
      If DBGrid1.Columns[i].Title.Font.Style = [fsBold] then
           DBGrid1.Columns[i].Title.Font.Style := [];
   end;

   If DBGrid1.Columns[Column.Index].Title.Font.Style <> [fsBold] then
           DBGrid1.Columns[Column.Index].Title.Font.Style := [fsBold];
       SingleClick := false;
   end;
end;

procedure TForm1.Timer1Timer(Sender: TObject);
begin
   SingleClick := true;
   Timer1.Enabled := false;
   DBGrid1TitleClick(SelectedColumn);
end;