t e m p o r a l 
 d o o r w a y 

Resizing Columns In TStringGrid Without Using The Header

 

When a TStringGrid has a FixedRows property greater than zero, you can set it up so that the columns can be resized, using the Options property. This allows the user to grab the line between the fixed row cells and drag the column narrower or wider.

However, if you do not have a fixed row, or you'd like to have the user be able to resize the columns in the space below the fixed row, you need to implement your own way of doing that. This example shows how that can be accomplished. It does this in the context of a form, but a similar effect could be achieved with greater reusability by moving the event handler code into an override of the appropriate methods (for instance MouseMove() for OnMouseMove) in a TStringGrid descendant.

First, the form in question:

object MainForm: TMainForm
  Left = -2
  Top = 110
  Width = 467
  Height = 333
  Caption = 'MainForm'
  Color = clBtnFace
  Font.Charset = DEFAULT_CHARSET
  Font.Color = clWindowText
  Font.Height = -11
  Font.Name = 'MS Sans Serif'
  Font.Style = []
  OldCreateOrder = False
  PixelsPerInch = 96
  TextHeight = 13
  object StringGrid: TStringGrid
    Left = 0
    Top = 0
    Width = 459
    Height = 299
    Align = alClient
    Options = [goFixedVertLine, goFixedHorzLine, goVertLine, goHorzLine, goRangeSelect, goColSizing]
    TabOrder = 0
    OnMouseDown = StringGridMouseDown
    OnMouseMove = StringGridMouseMove
    OnMouseUp = StringGridMouseUp
  end
end

Next, the header file; note the special variables to keep track of what's happening while the resize is occurring:

//---------------------------------------------------------------------------

#ifndef MainFormUnitH
#define MainFormUnitH
//---------------------------------------------------------------------------
#include <Classes.hpp>
#include <Controls.hpp>
#include <StdCtrls.hpp>
#include <Forms.hpp>
#include <Grids.hpp>
#include <ExtCtrls.hpp>
//---------------------------------------------------------------------------
class TMainForm : public TForm
{
__published:   // IDE-managed Components

   TStringGrid *StringGrid;

   void __fastcall StringGridMouseMove(TObject *Sender, TShiftState Shift,
          int X, int Y);

   void __fastcall StringGridMouseDown(TObject *Sender,
          TMouseButton Button, TShiftState Shift, int X, int Y);

   void __fastcall StringGridMouseUp(TObject *Sender, TMouseButton Button,
          TShiftState Shift, int X, int Y);

private:   // User declarations

   typedef enum {NoBoundaryPicked,CloserToLeftBoundary,CloserToRightBoundary} aBoundaryPicked;

   bool myCellResizeInProgress;
   aBoundaryPicked myBoundaryPicked;
   int myColumnBeingResized;

   bool WithinThreePixelsOfGridLine(const int theX, const int theY);

   TMainForm::aBoundaryPicked WhichBoundaryPicked(const int theX,const int theY);

   int NewWidth(const int theColumnIndex,const int theX,const int theY);

public:      // User declarations

   __fastcall TMainForm(TComponent* Owner);
};
//---------------------------------------------------------------------------
extern PACKAGE TMainForm *MainForm;
//---------------------------------------------------------------------------
#endif

And the implementation:

//---------------------------------------------------------------------------

#include <vcl.h>
#pragma hdrstop

#include "MainFormUnit.h"
#include "Math.h"
//---------------------------------------------------------------------------
#pragma package(smart_init)
#pragma resource "*.dfm"
TMainForm *MainForm;
//---------------------------------------------------------------------------
__fastcall TMainForm::TMainForm(TComponent* Owner)
   :
   TForm(Owner),

   myCellResizeInProgress(false),
   myBoundaryPicked(NoBoundaryPicked),
   myColumnBeingResized(-1)
{
}
//---------------------------------------------------------------------------
bool TMainForm::WithinThreePixelsOfGridLine(const int X, const int Y)
{
   int Column,Row;
   StringGrid->MouseToCell(X,Y,Column,Row);

   return
   (
      ((abs(StringGrid->CellRect(Column,Row).Left - X)) <= 3) ||
      ((abs(X - StringGrid->CellRect(Column,Row).Right)) <= 3)
   );
}
//---------------------------------------------------------------------------
int TMainForm::NewWidth(const int theColumnIndex,const int theX,const int theY)
{
   int OriginalCellWidth = StringGrid->ColWidths[theColumnIndex];
   TRect CurrentCellRect = StringGrid->CellRect(theColumnIndex,0);
   int ResultWidth = OriginalCellWidth + (theX - CurrentCellRect.Right);

   if (ResultWidth < 5) ResultWidth = 5; // Keep minimum column size to 5

   return ResultWidth;
}
//---------------------------------------------------------------------------
void __fastcall TMainForm::StringGridMouseMove(TObject *Sender,
      TShiftState Shift, int X, int Y)
{
   if (!myCellResizeInProgress)
   {
      if (WithinThreePixelsOfGridLine(X,Y))
      {
         StringGrid->Cursor = crHSplit;
      }
      else
      {
         StringGrid->Cursor = crDefault;
      };
   };
}
//---------------------------------------------------------------------------
TMainForm::aBoundaryPicked TMainForm::WhichBoundaryPicked(const int theX,const int theY)
{
   int Column,Row;
   StringGrid->MouseToCell(theX,theY,Column,Row);

   if ((abs(StringGrid->CellRect(Column,Row).Left - theX)) <= 3)
   {
      return CloserToLeftBoundary;
   }
   else if((abs(theX - StringGrid->CellRect(Column,Row).Right)) <= 3)
   {
      return CloserToRightBoundary;
   }
   else
   {
      throw new Exception("Unable to determine boundary");
   };
}
//---------------------------------------------------------------------------
void __fastcall TMainForm::StringGridMouseDown(TObject *Sender,
      TMouseButton Button, TShiftState Shift, int X, int Y)
{
   int Column,Row;
   StringGrid->MouseToCell(X,Y,Column,Row);

   if (WithinThreePixelsOfGridLine(X,Y))
   {
      myCellResizeInProgress = true;
      myBoundaryPicked = WhichBoundaryPicked(X,Y);
      myColumnBeingResized = Column;
   };
}
//---------------------------------------------------------------------------
void __fastcall TMainForm::StringGridMouseUp(TObject *Sender,
      TMouseButton Button, TShiftState Shift, int X, int Y)
{
   myCellResizeInProgress = false;

   int IntendedWidth;

   if (myBoundaryPicked == CloserToRightBoundary) // Resize the cell being resized
   {
      IntendedWidth = NewWidth(myColumnBeingResized,X,Y);

      if (IntendedWidth != StringGrid->ColWidths[myColumnBeingResized])
      {
         StringGrid->ColWidths[myColumnBeingResized] = IntendedWidth;
      };
   }
   else // Resize the cell to the left of the cell being resized
   {
      if (myColumnBeingResized > 0) // Can't resize col -1
      {
         IntendedWidth = NewWidth(myColumnBeingResized-1,X,Y);

         if (IntendedWidth != StringGrid->ColWidths[myColumnBeingResized-1])
         {
            StringGrid->ColWidths[myColumnBeingResized-1] = IntendedWidth;
         };
      };
   };
}
//---------------------------------------------------------------------------

The Sequence Of Events

The code implements appropriate event handlers to sense and respond to the position of the mouse with a special cursor over the grid lines, to sense the mouse button press so that it can start resizing, and to sense the mouse button press ended so it can complete the resize of the column.

  • The OnMouseMove handler looks for the pointer to be positioned over a cell boundary. If it is within 3 pixels to either side, it changes the cursor. In a component, you would make this a design-time property.

  • The OnMouseDown handler identifies which cell boundary has been picked of which column and saves that information for later use.

  • Resizing always acts as if the right boundary of the current cell or the right boundary of the cell to its left (the same as the left boundary of the current cell) has been picked.

  • The OnMouseUp handler calculates the new width, and if it is different from the old width, it resizes the cell.

There are some things it doesn't do. For instance, it doesn't track the mouse movements and resize as that occurs. Attempting to do that (by placing the resize code from OnMouseUp in the OnMouseMove), caused unacceptable flicker. It also doesn't draw an animated line to mark the current cursor position, which is what resizing the fixed row does. That could, however, be easily implemented.

Conclusion

It is not terribly hard to let the user resize the grid lines in your program. But it does require some thought and experimentation.

Copyright © 2004 by Mark Cashman (unless otherwise indicated), All Rights Reserved