How can I detect when a user is finished editing a TStringGrid cell?

16.2k views Asked by At

I would like to return the contents of a cell in a string grid when the user finishes entering the data. The user is finished when pressing the enter key on the keyboard, or single- or double-clicking another cell.

In Lazarus there is a method of FinishedCellEditing, but not in Delphi. How can I detect it in Delphi?

8

There are 8 answers

2
Jeroen Wiert Pluimers On

Basically, there are many ways a user can end editing, and not all these are always a good interception point:

  1. it moves the focus to another cell in the grid
  2. it moves the focus to another control on the form
  3. it moves the focus to another form
  4. it moves the focus to another application.

You need to ask yourself under which circumstances you want to update the content.

For instance: do you want to update it, when the user cancels out of a modal form, or ends the application?

--jeroen

1
Marjan Venema On

With the VCL's TStringGrid you need the OnSetEditText event. Please note however that it fires everytime the user changes something in any cell. So, if you only want to do something after the user is finished editing, you will have to watch the row and col values of the event's parameters. And of course, you need to take care of the situation when a user ends editing a cell and does not edit another cell, for example by clicking outside the TStringGrid. Something like:

TForm1 = class(TForm)
...
private
  FEditingCol, FEditingRow: Longint;
...
end;

procedure Form1.DoYourAfterEditingStuff(ACol, ARow: Longint);
begin
...
end;

procedure Form1.StringGrid1OnEnter(...)
begin
  EditingCol := -1;
  EditingRow := -1;
end;

procedure Form1.StringGrid1OnSetEditText(Sender: TObject; ACol, ARow: Longint; const Value: string)
begin
  if (ACol <> EditingCol) and (ARow <> EditingRow) then
  begin
    DoYourAfterEditingStuff(EditingCol, EditingRow);
    EditingCol := ACol;
    EditingRow := ARow;
  end;
end;

procedure Form1.StringGrid1OnExit(...)
begin
  if (EditingCol <> -1) and (EditingRow <> -1) then
  begin
    DoYourAfterEditingStuff(EditingCol, EditingRow);
    // Not really necessary because of the OnEnter handler, but keeps the code
    // nicely symmetric with the OnSetEditText handler (so you can easily 
    // refactor it out if the desire strikes you)
    EditingCol := -1;  
    EditingRow := -1;
  end;
end;
1
David Heffernan On

I do this by responding to WM_KILLFOCUS messages sent to the inplace editor. I have to subclass the inplace editor to make this happen.

I understand from Raymond Chen's blog that this is not appropriate if you then perform validation that changes the focus.

4
z666zz666z On

I have quite the same problem, but easier solution since i force user to press Enter key...

The trick: I do not let the user change to another cell while is editing one, so i force user to must press Intro/Enter to end editing, then i allow to change to other cell.

The bad part is that OnKeyPress happens before OnSetEditText, so i tried with OnKeyUp...

And what i found is that just when editing a cell, after pressing Enter/Intro, OnKeyUp is not fired... that is a BUG on VCL... a key has being released and OnKeyUp has not being fired.

So, i make another trick to bypass that... use a Timer to differ what i would do just a little, so i let time to event OnSetEditText be fired before.

Let me explain what i have done to success...

I have locked selecting another cell by putting code on OnSelectCell, quite similar to this:

CanSelect:=Not UserIsEditingOneCell;

And on OnSetEditText i put code like this:

UserIsEditingOneCell:=True;

So now, what is needed is to detect when the user press Enter/Intro... and i found a horrible thing as i said... OnKeyUp is not fired for such key... so, i will simulate that by using a Timer and using OnKeyPress, because OnKeyPress is fired, but OnKeyUp not, for Enter key...

So, on OnKeyPress i put something like:

TheTimerThatIndicatesUserHasPressEnter.Interval:=1; // As soon as posible
TheTimerThatIndicatesUserHasPressEnter.Enabled:=True; // But after event OnSetEditText is fired, so not jsut now, let some time pass

An on such timer event:

UserIsEditingOneCell:=False;
// Do whatever needed just after the user has finished editing a cell

That works, but i know that is horrible because i need to use a Timer... but i do not know a better way... and since i need to not let user go to another cell while the one that is edinting does not have a valid value... i can use that.

Why on the hell there is not an event like OnEndingEditing?

P.D.: I have also noticed that OnSetEditText is fired multiple times for each key being pressed, and with different value on Value parameter... at least when working with EditMask value '00:00:00' set on OnGetEditMask event.

0
Alan On

Answer from BCB 6:

String tmp = "";

void __fastcall TForm1::SetEditText(TObject *Sender, int ACol, int ARow, const AnsiString Value) {
  if (tmp != Value)
      tmp = Value;
  else
      ;// end editing 
}

void __fastcall TForm1::GetEditText(TObject *Sender, int ACol, int ARow, AnsiString &Value) {
  tmp = Value;
}
2
z666z666z On

This is the final version... Wow, I improved my own code (the other post I put before was the code I was using for years until today... I saw this post and I put the code I had... then I tried to fix my own code and I got it, wow!, I was trying that for years, now I finally got it).

It is quite tricky since, how on the hell I could imagine a cell could be selected with editor active?

Let's see how to do it:

var
  MyStringGrig_LastEdited_ACol, MyStringGrig_LastEdited_ARow: Integer;
  //To remember the last cell edited

procedure TmyForm.MyStringGrigSelectCell(Sender: TObject; ACol, ARow: Integer;
  var CanSelect: Boolean);
begin
  //When selecting a cell
  if MyStringGrig.EditorMode then begin //It was a cell being edited
    MyStringGrig.EditorMode:= False;    //Deactivate the editor
    //Do an extra check if the LastEdited_ACol and LastEdited_ARow are not -1 already.
    //This is to be able to use also the arrow-keys up and down in the Grid.
    if (MyStringGrig_LastEdited_ACol <> -1) and (MyStringGrig_LastEdited_ARow <> -1) then
      MyStringGrigSetEditText(Sender, MyStringGrig_LastEdited_ACol, MyStringGrig_LastEdited_ARow,
        MyStringGrig.Cells[MyStringGrig_LastEdited_ACol, MyStringGrig_LastEdited_ARow]);
    //Just make the call
  end;
  //Do whatever else wanted
end;

procedure TmyForm.MyStringGrigSetEditText(Sender: TObject; ACol, ARow: Integer;
  const Value: string);
begin
  //Fired on every change
  if Not MyStringGrig.EditorMode         //goEditing must be 'True' in Options
  then begin                             //Only after user ends editing the cell
    MyStringGrig_LastEdited_ACol:= -1;   //Indicate no cell is edited
    MyStringGrig_LastEdited_ARow:= -1;   //Indicate no cell is edited
    //Do whatever wanted after user has finish editing a cell
  end else begin                         //The cell is being editted
    MyStringGrig_LastEdited_ACol:= ACol; //Remember column of cell being edited
    MyStringGrig_LastEdited_ARow:= ARow; //Remember row of cell being edited
  end;
end;

This works for me like a charm.

Please note it requires two variables to hold last edited cell coordinates.

Please remember goEditing must be True in Options.

Please sorry for the other post... that other code was the one I was using for years, since I did not get any better solution... until now.

I hope this helps others.

0
sav On

Its probably best to just use virtual string grid as the string grid control in Delphi does not really seem to support this very well.

0
IceCold On

SOLUTION:

  TMyGrid= class(TStringGrid)
   private
    EditorPrevState: Boolean;    //init this to false!
    EditorPrevRow  : LongInt;
    EditorPrevCol  : LongInt;
    procedure WndProc(VAR Message: TMessage); override;     
    procedure EndEdit (ACol, ARow: Longint);  // the user closed the editor      
    etc
end;


constructor TMyGrid.Create(AOwner: TComponent);
begin
 inherited Create(AOwner);     
 EditorPrevRow    := Row;   
 EditorPrevCol    := Col;
 EditorPrevState:= false; 
end;


procedure TMyGrid.WndProc(var Message: TMessage);                                                  
begin
 inherited;
 if EditorPrevState then   { The editor was open }
  begin
    if NOT EditorMode then                 { And not is closed }
     begin
      EditorPrevState:= EditorMode;
      EndEdit(EditorPrevCol, EditorPrevRow);     <------ editor is closed. process the text here
     end;
    EditorPrevRow := Row;
    EditorPrevCol := Col;
  End;

 EditorPrevState := EditorMode;
end;


procedure TMyGrid.EndEdit(aCol, aRow: Integer);         { AlwaysShowEditror must be true in Options }
begin

 Cells[ACol, ARow]:= StringReplace(Cells[ACol, ARow], CRLF, ' ', [rfReplaceAll]);                      { Replace ENTERs with space - This Grid cannot draw a text on multiple rows so enter character will he rendered as 2 squares. }

 if Assigned(FEndEdit)
 then FEndEdit(Self, EditorPrevCol, EditorPrevRow); // optional
end;