In the April 2002 issue I showed you how to add controls to a string grid’s cells. This month, I’ll focus on the grid’s in-place editor. I’ll review the string grid’s editor-related events, show how to access the existing editor in a standard TStringGrid instance, and explain how to create your own in-place editor class.
As most of you know, when the Options property of a TStringGrid object contains the goEditing enumerator, the string grid permits in-place editing of a cell’s contents via a small edit control that’s displayed over the cell. This edit control, which is created and managed by the TCustomGrid class, is an instance of a TCustomMaskEdit descendant class called TInplaceEdit. The TCustomGrid class provides access to its TInplaceEdit instance via the InplaceEditor property.
From an application end user’s perspective, the grid’s in-place editor is a standard edit control. From the perspective of a developer using the TStringGrid class, the in-place editor is essentially a built-in TCustomMaskEdit.
I say the editor is "built in" because there’s no formal means of accessing the grid’s TInplaceEdit instance—the TCustomGrid::InplaceEditor property is protected, and is thus accessible only in descendant classes. However, the in-place editor forwards certain events to its associated grid. This provides users of the TStringGrid class indirect control over what the end user can input into the grid’s cells. In most cases, this limited control suffices; in other cases, it doesn’t.
Later I’ll show you how to gain direct access to the grid’s TInplaceEdit instance. For now, let’s focus on the standard, indirect means of controlling the in-place editor.
Before the user can edit a cell, the TCustomGrid class has to position the in-place editor over that cell. The grid does this via its internal UpdateEditor() method, which, in turn, calls the TInplaceEdit::UpdateContents() method. Here’s a condensed C++ translation of the UpdateContents() method:
void __fastcall
TInplaceEdit::UpdateContents()
{
Text = "";
EditMask = Grid->GetEditMask(
Grid->Col, Grid->Row);
Text = Grid->GetEditText(
Grid->Col, Grid->Row);
}
As you can see from this code, the editor first clears its current text, then calls the grid’s GetEditMask() method (assigning the return value to its EditMask property), and then calls the grid’s GetEditText() method (assigning the return value to its Text property).
The TCustomGrid::GetEditMask() and GetEditText() methods correspond, respectively, to the grid’s OnGetEditMask and OnGetEditText events. Both of these events have the same signature, which is of the following form:
typedef void __fastcall
(__closure *TGetEditEvent)(
System::TObject* Sender,
long Col, long Row,
AnsiString& Value);
The Sender parameter refers to the grid itself. The Col and Row parameters specify the indices of the "to-be-edited" cell. You use the Value parameter to specify either the editor’s mask or its text (depending on which event you’re working with).
As I mentioned, the TInplaceEdit class is a TCustomMaskEdit descendant--this design provides the in-place editor with inherent text-filtering capabilities. If you were working with a TMaskEdit object, you’d normally specify its text mask by using the TCustomMaskEdit::EditMask property. In the string grid however, you don’t have access to the in-place editor; rather, you specify the text mask by using the TStringGrid::OnGetEditMask event. For example, if the third (non-fixed) column of your grid represents dates, you’d use the OnGetEditMask event like so:
void __fastcall TForm1::
StringGrid1GetEditMask(TObject *Sender,
int Col, int Row, AnsiString& Value)
{
if (Col == StringGrid1->FixedRows + 2)
{
Value = "!99/99/00;1;_";
}
}
Recall from the definition of the TInplaceEdit::UpdateContents() method that the editor replaces its current text with the return value of the grid’s GetEditText() method. The TStringGrid class defines the GetEditText() method to simply return the text of the cell over which the editor is placed. Before returning though, the string grid will call its OnGetEditText event handler (if one is assigned), giving you an opportunity to change the text assigned to the editor. For example, if you want the editor to display the cell’s current text surrounded by parentheses, you’d use the OnGetEditText event as follows:
void __fastcall TForm1::
StringGrid1GetEditText(TObject *Sender,
int Col, int Row, AnsiString& Value)
{
Value = "(" + Value + ")";
}
Whenever the user changes the editor’s contents, the TCustomGrid class calls its private UpdateText() method, which, in turn, calls the grid’s virtual SetEditText() method. This latter method is passed the editor’s current text. Within its definition of the SetEditText() method, the TStringGrid class first assigns the specified text (i.e., the editor’s text) to the corresponding cell and then calls its OnSetEditText event handler, if one is assigned. Here’s what the OnSetEditText event looks like:
typedef void __fastcall
(__closure *TSetEditEvent)(
System::TObject* Sender,
long Col, long Row,
const AnsiString Value);
Because the string grid updates the cell’s text before firing its OnSetEditText event, the Value parameter will always be the same as the text held in the cell located at Col and Row. Thus, the OnSetEditText event notifies you that both the editor’s text and the cell’s text have changed (both to the same value).
The TInplaceEdit class augments the virtual TWinControl::KeyDown(), KeyPress(), and KeyUp() methods to forward keyboard-related events to its associated grid. This allows you to use the string grid’s OnKeyDown, OnKeyPress, and OnKeyUp events to monitor (and perhaps filter) keystrokes while a cell is being edited. For example, if you want to convert all characters to uppercase, you’d use the OnKeyPress event like so:
void __fastcall TForm1::
StringGrid1KeyPress(TObject *Sender,
char& Key)
{
Key = std::toupper(Key);
}
Similarly, suppose you want to allow the user to press the escape key to restore the cell’s text to its value prior to editing. In this case, you’d use a combination of the OnGetEditText and OnKeyDown events as follows:
void __fastcall TForm1::
StringGrid1GetEditText(TObject *Sender,
int Col, int Row, AnsiString& Value)
{
// NOTE: old_cell_text_ is an
// AnsiString-type member of TForm1
old_cell_text_ = Value;
}
void __fastcall TForm1::
StringGrid1KeyDown(TObject* Sender,
WORD& Key, TShiftState Shift)
{
if (Key == VK_ESCAPE &&
StringGrid1->EditorMode)
{
const int col = StringGrid1->Col;
const int row = StringGrid1->Row;
StringGrid1->Cells[col][row] =
old_cell_text_;
StringGrid1->EditorMode = false;
}
}
Notice the use of the EditorMode property in this code. EditorMode is a Boolean property (first introduced in the TCustomGrid class) that simply indicates whether or not a cell is being edited. (Note that you can also set EditorMode to true to initiate the cell-editing process.)
Here’s an example that demonstrates how to use the string grid’s EditorMode property along with the OnKeyPress, OnSelectCell, and OnExit events to determine when a user has finished editing a cell’s text. This scheme allows you to validate the user’s input. Here’s the code:
// trivial validation function
bool TForm1::IsValidEntry()
{
const int col = StringGrid1->Col;
const int row = StringGrid1->Row;
const AnsiString text(
StringGrid1->Cells[col][row]
);
if (text != "some_valid_entry")
{
Application->MessageBox(
"Please input a valid value.",
"Input Error", MB_OK);
return false;
}
return true;
}
// OnKeyPress event handler
void __fastcall TForm1::
StringGrid1KeyPress(TObject *Sender,
char& Key)
{
if (
Key == '\r' &&
StringGrid1->EditorMode &&
!IsValidEntry()
)
{
Key = 0;
}
}
// OnSelectCell event handler
void __fastcall TForm1::
StringGrid1SelectCell(TObject *Sender,
int Col, int Row, bool& CanSelect)
{
if (StringGrid1->EditorMode)
{
CanSelect = IsValidEntry();
if (!CanSelect)
{
Editor_->SelectAll();
}
}
}
// OnExit event handler
void __fastcall TForm1::
StringGrid1Exit(TObject *Sender)
{
if (!IsValidEntry())
{
ActiveControl = StringGrid1;
PostMessage(StringGrid1->Handle,
WM_KEYDOWN, VK_F2, 0);
}
}
Earlier I mentioned that the TCustomGrid class positions its in-place editor from within the TCustomGrid::UpdateEditor() method. Here’s a C++ translation of that method:
void __fastcall
TCustomGrid::UpdateEdit()
{
if (CanEditShow())
{
if (FInplaceEdit == NULL)
{
FInplaceEdit = CreateEditor();
FInplaceEdit->SetGrid(this);
FInplaceEdit->Parent = this;
UpdateEditor();
}
else if (Col != FInplaceCol ||
Row != FInplaceRow)
{
HideEdit();
UpdateEditor();
}
// position and show the editor...
}
}
The TCustomGrid class stores a pointer to the in-place editor in its private FInplaceEdit member. If this member is NULL, a call is made to the virtual CreateEditor() method to create the editor. And, after the editor is created, the grid’s assigns itself (this) as the editor’s Parent—this assignment allows you to access the in-place editor from outside the scope of the TStringGrid class. Namely, although you can’t access the string grid’s InplaceEditor property (because it’s protected), you can access the string grid’s Controls property. Because the grid sets itself as the in-place editor’s Parent, and because the grid contains no other controls (unless you explicitly put them there yourself), StringGrid->Controls[0] will always to refer to the in-place editor (if it has been created).
When can you safely access the in-place editor via the Controls property? Well, recall that the grid’s GetEditMask() method is called from within the TInplaceEdit::UpdateContents() method. Because the in-place editor is deleted only within the grid’s destructor, the editor is guaranteed to exist anytime after the string grid’s OnGetEditText event handler is first called. So an easy way to grab a pointer to the editor is to use the OnGetEditText event, like so:
#include <cassert>
void __fastcall TForm1::
StringGrid1GetEditMask(TObject *Sender,
int Col, int Row, AnsiString& Value)
{
if (Editor_ == NULL)
{
assert(StringGrid1->
ControlCount == 1);
Editor_ =static_cast<TInplaceEdit*>(
StringGrid1->Controls[0]);
}
}
Here, Editor_ is a class member of type TInplaceEdit* that’s initialized to NULL in the form’s constructor.
With a pointer to the in-place editor, you can use properties and methods of the TInplaceEdit class that you wouldn’t otherwise have access to. For example, if you want to give the editor a thin-line border and a new background color, you’d do the following:
const LONG style = GetWindowLong(
Editor_->Handle, GWL_STYLE);
SetWindowLong(Editor_->Handle,
GWL_STYLE, style | WS_BORDER);
Editor_->Brush->Color = clInfoBk;
Editor_->Invalidate();
Similarly, if you’d rather have newly-entered text appended to the end of the current text (rather than replace it), you could use the TInplaceEdit::Modified property and Deselect() method as follows:
void __fastcall TForm1::
StringGrid1KeyPress(TObject *Sender,
char &Key)
{
if (Editor_ && !Editor_->Modified)
{
Editor_->Deselect();
}
}
Unfortunately, having access to the string grid’s in-place editor isn’t as promising as it might sound. For one, the TInplaceEdit class fails to expose several key properties (e.g., CharCase, Font). Second, and more importantly, many of the changes that you’ll likely want to make require knowledge of when the editor is positioned and shown. In the previous code snippet, for example, the Modified property is queried each time a character-key is pressed. It would make more sense to deselect the text only after the editor is first shown.
I just mentioned that the TInplaceEdit class is somewhat restrictive because it lacks and/or fails to expose several key properties and events. Fortunately, the TCustomGrid class provides a way to overcome these limitations; namely via the CreateEditor() method.
Recall from the definition of the TCustomGrid::UpdateEdit() method that if the grid’s FInplaceEdit member is NULL, the grid creates the in-place editor via a call to the virtual TCustomGrid::CreateEditor() method. Here’s how that method is declared:
virtual TInplaceEdit*
__fastcall CreateEditor();
Because the TCustomGrid class assigns the return value of CreateEditor() method to its FInplaceEdit member, a descendant class can override the CreateEditor() method to specify a custom in-place editor.
In short, if your application requires functionality beyond that provided by the TInplaceEdit class, you can effectively instruct the grid to use your own TInplaceEdit descendant class by overriding the CreateEditor() method. Listing A provides an example of a TInplaceEdit descendant class, which I’ve named TInplaceEditEx. This class does nothing more than expose the BorderStyle, CharCase, Color, and Font properties.
As I just mentioned, in order to have the string grid use an instance of the TInplaceEditEx class instead of the default TInplaceEdit object, you need to override the TCustomGrid::CreateEditor() method. To demonstrate this, I’ve created a barebones TStringGrid descendant class, TStringGridEx, which is declared in Listing B.
Whereas the TCustomGrid class creates the in-place editor from within its UpdateText() method, the TStringGridEx class creates an instance of the TInplaceEditEx class from within the TStringGridEx constructor, like so:
__fastcall TStringGridEx::
TStringGridEx(TComponent* Owner) :
TStringGrid(Owner),
Editor_(new TInplaceEditEx(this))
{
Editor_->Parent = this;
}
Notice that a pointer to the TInplaceEditEx instance is stored in the private Editor_ member. Accordingly, from within the (re)definition of the CreateEditor() method, the TStringGridEx class simply return Editor_’s value, like so:
TInplaceEdit* __fastcall TStringGridEx::
CreateEditor()
{
return Editor_;
}
That’s all there is to it—the string grid will now use the specified TInplaceEditEx object instead of its default editor.
As you can see from Listing B, I’ve redefined (and exposed) the InplaceEditor property to provide external access to Editor_. This eliminates the hassle of typecasting the return value of the Controls property. Moreover, because the editor is created in the class constructor, you don’t have to worry about when it’s safe to access the editor. (Recall that with a standard TStringGrid object, we had to rely on the OnGetEditMask event to determine when it was safe to access Controls[0].) For example, if StringGridEx1 is an instance of the TStringGridEx class (created at design time), you could set its InplaceEditor’s properties from within its parent form’s constructor like so:
__fastcall TForm1::TForm1(
TComponent* Owner) : TForm(Owner)
{
StringGridEx1->InplaceEditor->
BorderStyle = bsSingle;
StringGridEx1->InplaceEditor->
CharCase = ecUpperCase;
StringGridEx1->InplaceEditor->
Color = clInfoBk;
StringGridEx1->InplaceEditor->
Font->Color = clRed;
}
In this article, I’ve presented three ways to access a string grid’s in-place editor: (1) indirect access by using the string grid’s OnGetEditMask, OnGetEditText, and OnSetEditText events; (2) direct access by using the Controls property; and (3) direct access by creating TInplaceEdit and TStringGrid descendant classes.
The third approach offers the most flexibility because you can tailor your TInplaceEdit descendant class to suit the specific needs of your application. See the sample code that accompanies this article (available at www.bridgespublishing.com) for some more examples of working with the in-place editor.
Listing A: Declaration of the TInplaceEditEx class
#include <cassert>
class TInplaceEditEx : public TInplaceEdit
{
public:
__fastcall TInplaceEditEx(TComponent* Owner)
: TInplaceEdit(Owner) {}
__property BorderStyle;
__property CharCase;
__property Color;
__property Font;
// publish other properties as needed...
};
Listing B: Declaration of the TStringGridEx class
#include <grids.hpp>
class TStringGridEx : public TStringGrid
{
public:
__fastcall TStringGridEx(TComponent* Owner);
__property TInplaceEditEx* InplaceEditor =
{read = Editor_};
protected:
TInplaceEdit* __fastcall CreateEditor();
private:
TInplaceEditEx* Editor_;
};