Posts from April 2012

MonkeyStyler Build 3 Beta

This build adds the ability to create a link to your application and have the styles within it updated with a single click. Read more about this in the wiki.

The full list of changes for this update:
Added: ‘Apply’ styles: One click to modify the style of a running FireMonkey application
Added EurekaLog error handling (some bugs with EL and FMX).

FireMonkey ListBox With In Place Editing

I’ll admit to having a personal bias against modal dialog boxes. They interrupt my workflow and thought process. So, I rather like the way Windows Explorer works when editing a filename - the text changes to an edit box and I can type in the new filename, or simply hit escape and move on.

I saw no reason why I shouldn’t be able to do the same thing in FireMonkey. A FireMonkey list box is simply a container for other controls, so I reasoned I could simply replace the existing text control with an edit control when the user began an edit, and swap back to the text control when the edit was finished. We’ll start editing when the user hits F2, finish on pressing the enter key and let users back out by hitting escape.

Here’s a video of what I came up with:

EditListBoxItem

A listbox is a container for other controls, but only children of TListBoxItem will actually show up within the list, so first we need to create such a child, we’ll call it TEditListBoxItem:

type TEditListBoxItem = class(TListBoxItem)
  private
    
FEditControlTStyledControl;
    
procedure SetEditControl(const ValueTStyledControl);
  protected
    
procedure SetFocus;reintroduce;
    
procedure SetText(const ValueString);override;
    function 
GetTextString;
    
procedure EVKeyDown(SenderTObject; var KeyWord; var KeyCharWideCharShiftTShiftState);
    function 
GetDataVariant;override;
    
procedure SetData(const ValueVariant);override;
  public
    
constructor Create(AOwnerTComponent);override;
    
destructor Destroy;override;
    
property TextString read GetText write SetText;
    
property EditControlTStyledControl read FEditControl write SetEditControl;
  
end


EditControl is the control which will do the editing. I could have explicitly made this a TEdit, but if we use a TSyledControl then users of our code can substitute any suitable control. They could use some kind of date or filtered editor, or one with validation. Or they could use something completely unrelated to text and text editing.

procedure TEditListBoxItem.SetEditControl(const ValueTStyledControl);
var 
DataVariant;
begin
  
if EditControl <> nil then
    Data 
:= GetData
  
else
    
Data := varNull;
  
FreeAndNil(FEditControl);
  
FEditControl := Value;
  
FEditControl.Align := TAlignLayout.alClient;
  
FEditControl.Data := Data;
  
FEditControl.Parent := Self;
  
FEditControl.OnKeyDown := EVKeyDown;
end

We’re relying on the creator of the list box item to create and assign the edit control. When they do we need to do some setting of properties, as shown above. We need to monitor the OnKeyDown event so we can intercept the Enter and Escape keys to finish editing.

Because we don’t know that our edit control will be an edit box we can’t simply read and write the Text property. Fortunately FireMonkey gives us a more generic way of getting and setting content using the Data property. This is a Variant and can read or write any data type as used by the control.

Note in the SetEditControl method above that we took care to preserve the value of any control already in existence by getting the old Data and assigning it to the new Control.

And refer back to the class definition above to see that we override the GetData and SetData methods to get and set data. We also add our own Text property since GetText and SetText aren’t virtual. Our GetText and SetText simply pass values to and from the edit controls Data property. It’s a bit redundant in our usage, but adding this functionality should help prevent obscure errors somewhere down the line.

function TEditListBoxItem.GetDataVariant;
begin
  
if EditControl <> nil then
    Result 
:= EditControl.Data
  
else
    
Result := varNull;
end;

function 
TEditListBoxItem.GetTextString;
begin
  Result 
:= GetData;
end;

procedure TEditListBoxItem.SetData(const ValueVariant);
begin
  inherited
;
  if 
EditControl <> nil then
    EditControl
.Data := Value;
end;

procedure TEditListBoxItem.SetText(const ValueString);
begin
  inherited
;
  if 
EditControl <> nil then
    EditControl
.Data := Value;
end

TInPlaceEditListBox

Now we can move on to the list box itself:

type TInPlaceEditListBox = class(TListBox)
  private
    
EditItemTEditListBoxItem;
    
EditedItemTListBoxItem;
    
EditUpdateInteger;
    
FOnCreateEditControlTCreateControlEvent;
  protected
    
procedure KeyDown(var KeyWord; var KeyCharSystem.WideCharShiftTShiftState); override;
    
procedure EVEditKeyDown(SenderTObject; var KeyWord; var KeyCharWideCharShiftTShiftState);
    
procedure SetItemIndex(const ValueInteger);override;
    
procedure InPlaceEdit;
    
procedure EndInPlaceEdit(SaveBoolean);
  public
    
destructor Destroy;override;
    function 
CreateEditControlTStyledControl;virtual;
    
property OnCreateEditControlTCreateControlEvent read FOnCreateEditControl write FOnCreateEditControl;
  
end

We’ll start in the overridden KeyDown method, which simply monitors for the F2 key:

procedure TInPlaceEditListBox.KeyDown(var KeyWord;
  var 
KeyCharSystem.WideCharShiftTShiftState);
begin
  
if Key vkF2 then
    InPlaceEdit
  
else
    
inherited;
end;[/b]

Things get interesting in the inPlaceEdit method
:

[code]procedure TInPlaceEditListBox.InPlaceEdit;
var
  
OldYSingle;
  
ControlTStyledControl;
begin
  
if (ItemIndex 0then
    
EXIT;

  
EndInPlaceEdit(True);

  
OldY := VScrollBar.Value;
  try
    
Inc(EditUpdate);
    
FreeAndNil(EditItem);
    
EditItem := TEditListBoxItem.Create(Self);
    
EditItem.EditControl := CreateEditControl;
    
EditItem.OnKeyDown := EVEditKeyDown;

    
BeginUpdate;
    
EditedItem := ListItems[ItemIndex];
    
AddObject(EditItem);
        
Exchange(EditedItemEditItem);
    
EditedItem.Parent := nil;
    
EditItem.SetData((THackListBoxItem(EditedItem)).GetData);
    
EditItem.SetFocus;
    
EditItem.IsSelected := True;
    
VScrollBar.Value := OldY;
    
EditItem.Opacity := 0;
     
EndUpdate;
    
EditItem.AnimateFloat('Opacity'10.2);
  finally
    
dec(EditUpdate);
  
end;
end

Here we,
* Check we have a valid ItemIndex.
* Stop any exising edits.
* Preserve the scollbar value so we can restore it later.
* EditUpdate is used to prevent some icky side effects (see below).
* We free any existing EditItem (our TEditListBoxItem), create a new one and call CreateEditControl to instantiate the control to go inside it.
* We preserve the existing ListBoxItem into EditedItem, so we can restore it once editing is complete.

Now we need to get our TEditListBoxItem (EditItem) into the list. TListBox has an InsertObject method to insert an item anywhere in the list. Unfortunately a bug in FireMonkey (as of XE2 update 4) means this always fails with an exception. The workaround for this is to call AddObject which adds the item to the end of the list (and is a synonym for setting the Parent property), and then call Exchange with the two items we want to swap.

EditItem (Our new item) is now in the correct place in the list, and EditedItem (the old text item) is now at the end. We can simply null EditedItems Parent property to remove it from the list.

Next comes a line lifted from the dark side, and one I’m not proud of. Recall our discussion on setting a generic data value through the Data property? What I should have been able to write here is

EditItem.Data := EditedItem.Data

But the FireMonkey designers borxed things up in TListBoxItem by adding a fresh Data property of type TObject which, as far as I can see, isn’t actually used anywhere in FireMonkey.

The workaround is to explicitly call the inherited GetData and SetData methods which aren’t overridden in TListBoxItem, but they are hidden because they’re protected and out of scope. We can bring them into scope by creating a do nothing descendant of TListBoxItem (

type THackListBoxItem = class(TListBoxItem); 

) in our unit and use some typecasting as shown above to get to the methods. Like I say, nasty but necessary.

After that we can return to sunnier climes. We:
* Give the edit control focus.
* Select the new item, to restore the list boxes ItemIndex property.
* Restore the scrollbar to where it was.
* Animate the opacity property for a pretty effect.

Tidying up

To handle when the user finishes editing we use the EVKeyDown event handler which monitors for Escape and Enter keys and calls EndInPlaceEdit, which is pretty much a reversed copy of InPlaceEdit: swapping out our editor for whatever was there before and saving (or not) the Data.

There’s only a couple more things of note.

We override the SetItemIndex method to stop editing if another item is selected (e.g. by a mouse click). Here’s where our EditUpdate field comes in useful: all that inserting and moving list items around plays havoc with the ItemIndex property and we need to protect against accidentally finishing editing before we’ve even started.

procedure TInPlaceEditListBox.SetItemIndex(const ValueInteger);
begin
  
if EditUpdate 0 then
    EndInPlaceEdit
(True);
  
inherited;
end

In EVKeyDown we add code to move the editor when the user keys up or down:

procedure TInPlaceEditListBox.EVEditKeyDown(SenderTObject; var KeyWord;
  var 
KeyCharWideCharShiftTShiftState);
begin
  
if EditItem nil then
    
EXIT;

    if 
Key vkUp then
    
if ItemIndex 0 then
    begin
      KeyDown
(KeyKeyCharShift);
      
InPlaceEdit;
    
end
    
else
  else if 
Key vkDown then
    
if ItemIndex Count-1 then
    begin
      KeyDown
(KeyKeyCharShift);
      
InPlaceEdit;
    
end;
end

but we double check ItemIndex can be changed. Otherwise moving up from the first item or down from the last would cause the editor to be recreated and look nasty.

Finally we’ll look at CreateEditControl which lets developers use their own edit control, or defaults to a TEdit:

function TInPlaceEditListBox.CreateEditControlTStyledControl;
begin
  Result 
:= nil;
  if 
Assigned(OnCreateEditControlthen
    OnCreateEditControl
(SelfResult)
  else
    
Result := TEdit.Create(Self);
end

Back to the list box item

Before I leave you in peace I’ve got something else to show you. Look at the constructor of TEditListBoxItem:

constructor TEditListBoxItem.Create(AOwnerTComponent);
begin
  inherited
;
  
CanClip := False;
end

We’re setting the CanClip property to false. This means that the item and it’s children can show outside the bounds of it’s owner (the list box). The first effect of this is that the editors glow effect can show outside the list box. IMHO this simply looks better.

The second and potentially more exciting effect is that the editor itself can appear outside the listbox. Imagine the list box contained some potentially long strings but you don’t want a wide form. The user can now have a compact display but with a nice long edit box when necessary.

The code to achieve this is simple, in the list items SetEditControl method simply comment out the line that sets the Align property and set a reasonable width for the edit control:

procedure TEditListBoxItem.SetEditControl(const ValueTStyledControl);
var 
DataVariant;
begin
  
if EditControl <> nil then
    Data 
:= GetData
  
else
    
Data := varNull;
  
FreeAndNil(FEditControl);
  
FEditControl := Value;
//  FEditControl.Align := TAlignLayout.alClient;
  
FEditControl.Width := 200;
  
FEditControl.Data := Data;
  
FEditControl.Parent := Self;
  
FEditControl.OnKeyDown := EVKeyDown;
end

Download the full source:

Enjoy, Mike.

Full Source

unit InPlaceEditList;

interface
uses FMX.ListBoxSystem.ClassesFMX.TypesFMX.Edit;

type TEditListBoxItem = class(TListBoxItem)
  private
    
FEditControlTStyledControl;
    
procedure SetEditControl(const ValueTStyledControl);
  protected
    
procedure SetFocus;reintroduce;
    
procedure SetText(const ValueString);override;
    function 
GetTextString;
    
procedure EVKeyDown(SenderTObject; var KeyWord; var KeyCharWideCharShiftTShiftState);
    function 
GetDataVariant;override;
    
procedure SetData(const ValueVariant);override;
  public
    
constructor Create(AOwnerTComponent);override;
    
destructor Destroy;override;
    
property TextString read GetText write SetText;
    
property EditControlTStyledControl read FEditControl write SetEditControl;
  
end;

type TCreateControlEvent procedure(SenderTObject;out ControlTStyledControlof object;

type TInPlaceEditListBox = class(TListBox)
  private
    
EditItemTEditListBoxItem;
    
EditedItemTListBoxItem;
    
EditUpdateInteger;
    
FOnCreateEditControlTCreateControlEvent;
  protected
    
procedure KeyDown(var KeyWord; var KeyCharSystem.WideCharShiftTShiftState); override;
    
procedure EVEditKeyDown(SenderTObject; var KeyWord; var KeyCharWideCharShiftTShiftState);
    
procedure SetItemIndex(const ValueInteger);override;
    
procedure InPlaceEdit;
    
procedure EndInPlaceEdit(SaveBoolean);
  public
    
destructor Destroy;override;
    function 
CreateEditControlTStyledControl;virtual;
    
property OnCreateEditControlTCreateControlEvent read FOnCreateEditControl write FOnCreateEditControl;
  
end;

procedure Register;

implementation
uses System
.UITypesSysutils;

procedure Register;
begin
  RegisterComponents
('SolentFMX'[TInPlaceEditListBox]);
end;

{ TEditListBoxItem }

constructor TEditListBoxItem
.Create(AOwnerTComponent);
begin
  inherited
;
  
CanClip := False;
end;

destructor TEditListBoxItem.Destroy;
begin
  FreeAndNil
(FEditControl);
  
inherited;
end;

procedure TEditListBoxItem.EVKeyDown(SenderTObject; var KeyWord;
  var 
KeyCharWideCharShiftTShiftState);
begin
  
if Assigned(OnKeyDownthen
    OnKeyDown
(SelfKeyKeyCharShift);
end;

function 
TEditListBoxItem.GetDataVariant;
begin
  
if EditControl <> nil then
    Result 
:= EditControl.Data
  
else
    
Result := varNull;
end;

function 
TEditListBoxItem.GetTextString;
begin
  Result 
:= GetData;
end;

procedure TEditListBoxItem.SetData(const ValueVariant);
begin
  inherited
;
  if 
EditControl <> nil then
    EditControl
.Data := Value;
end;

procedure TEditListBoxItem.SetEditControl(const ValueTStyledControl);
var 
DataVariant;
begin
  
if EditControl <> nil then
    Data 
:= GetData
  
else
    
Data := varNull;
  
FreeAndNil(FEditControl);
  
FEditControl := Value;
  
FEditControl.Align := TAlignLayout.alClient;
//  FEditControl.Width := 200;
  
FEditControl.Data := Data;
  
FEditControl.Parent := Self;
  
FEditControl.OnKeyDown := EVKeyDown;
end;

procedure TEditListBoxItem.SetFocus;
begin
  inherited
;
  if 
FEditControl <> nil then
    FEditControl
.SetFocus;
end;

procedure TEditListBoxItem.SetText(const ValueString);
begin
  inherited
;
  if 
EditControl <> nil then
    EditControl
.Data := Value;
end;

{ TInPlaceEditListBox }

function TInPlaceEditListBox.CreateEditControlTStyledControl;
begin
  Result 
:= nil;
  if 
Assigned(OnCreateEditControlthen
    OnCreateEditControl
(SelfResult)
  else
    
Result := TEdit.Create(Self);
end;

destructor TInPlaceEditListBox.Destroy;
begin
  FreeAndNil
(EditItem);
  
inherited;
end;

type THackListBoxItem = class(TListBoxItem);

procedure TInPlaceEditListBox.EndInPlaceEdit(SaveBoolean);
begin
  
if (EditItem nil) or (EditedItem nilthen
    
EXIT;

  try
    
Inc(EditUpdate);
    
BeginUpdate;
    if 
Save then
      THackListBoxItem
(EditedItem).SetData((EditItem).GetData);
    
AddObject(EditedItem);
    
Exchange(EditItemEditedItem);
    
EditedItem.IsSelected := True;
    
FreeAndNil(EditItem);
    
SetFocus;
    
EditedItem.Opacity := 0;
    
EndUpdate;
    
EditedItem.AnimateFloat('Opacity'10.2);
    
EditedItem := nil;
  finally
    
dec(EditUpdate);
  
end;
end;

procedure TInPlaceEditListBox.EVEditKeyDown(SenderTObject; var KeyWord;
  var 
KeyCharWideCharShiftTShiftState);
begin
  
if EditItem nil then
    
EXIT;

  if 
Key vkReturn then
  begin
    EndInPlaceEdit
(True);
    
Key := 0;
  
end
  
else if Key vkEscape then
  begin
    EndInPlaceEdit
(False);
  
end
  
else if Key vkUp then
    
if ItemIndex 0 then
    begin
      KeyDown
(KeyKeyCharShift);
      
InPlaceEdit;
    
end
    
else
  else if 
Key vkDown then
    
if ItemIndex Count-1 then
    begin
      KeyDown
(KeyKeyCharShift);
      
InPlaceEdit;
    
end;
end;

procedure TInPlaceEditListBox.InPlaceEdit;
var
  
OldYSingle;
  
ControlTStyledControl;
begin
  
if (ItemIndex 0then
    
EXIT;

  
EndInPlaceEdit(True);

  
OldY := VScrollBar.Value;
  try
    
Inc(EditUpdate);
    
FreeAndNil(EditItem);
    
EditItem := TEditListBoxItem.Create(Self);
    
EditItem.EditControl := CreateEditControl;
    
EditItem.OnKeyDown := EVEditKeyDown;

    
BeginUpdate;
    
AddObject(EditItem);
    
EditedItem := ListItems[ItemIndex];
    
Exchange(EditedItemEditItem);
    
EditedItem.Parent := nil;
    
EditItem.SetData((THackListBoxItem(EditedItem)).GetData);
    
EditItem.SetFocus;
    
EditItem.IsSelected := True;
    
VScrollBar.Value := OldY;
    
EditItem.Opacity := 0;
    
EndUpdate;
    
EditItem.AnimateFloat('Opacity'10.2);
  finally
    
dec(EditUpdate);
  
end;
end;

procedure TInPlaceEditListBox.KeyDown(var KeyWord;
  var 
KeyCharSystem.WideCharShiftTShiftState);
begin
  
if Key vkF2 then
    InPlaceEdit
  
else
    
inherited;
end;

procedure TInPlaceEditListBox.SetItemIndex(const ValueInteger);
begin
  
if EditUpdate 0 then
    EndInPlaceEdit
(True);
  
inherited;
end;

initialization
  RegisterFMXClasses
([TInPlaceEditListBoxTEditListBoxItem]);
end

MonkeyStyler build 2 beta

MonkeyStyler build 2 beta is now available. If you’re on the beta programme, use the link you already have to download.

Changes list

Added: Tooltips for toolbar buttons.
Fix: Error message popups now size properly on first showing.
Fix: Warnings for duplicate element names are no longer case sensitive.
Improved: validation of element names when adding, renaming or copying elements.
Added: Pasting a component switches the property editor to the pasted component.
Fix: Stepping of values now works for TAngleEditor (ie values in steps of 3 etc.)
Added: Copy To dialog remembers it’s settings between shows (not between sessions).
Added: Property grid scrolls up if necessary when expanding items.