Delphi 7 Invalid Pointer Operation with Custom Component - delphi

I have a custom component class that I use as a framework to load comments and user details, etc, onto a form. The Class is a sub-class of a panel and contains 3 labels and a memo.
Upon Closing my form, or trying to free the object, i get a "Invalid Pointer Operation" error. I know this is from trying to free an object twice, or accessing RAM that is not available. However, I don't know how to fix it. It's holding me back quite a bit, as I have to load different comments on different topics, and I cannot until I clear the form of the current comments.
Here is the class-related code:
type
TSkeleton = class(TPanel)
private
fName : TLabel;
fStudNo : TLabel;
fTimeAndDate : TLabel;
fComment : TMemo;
public
Constructor Create (AOwner: TComponent); overload; override;
constructor Create(AOwner:TForm; sName, sStudNo, sTime, sDate, sComment: string; ComCount: integer); overload;
end;
{ TSkeleton }
constructor TSkeleton.Create(AOwner: TComponent);
begin
//
end;
constructor TSkeleton.Create(AOwner: TForm; sName, sStudNo, sTime, sDate,
sComment: string; ComCount: integer);
begin
inherited Create(AOwner);
Parent := AOwner;
Width := 800;
Height := 250;
Top := 448+((ComCount-1)*250);
Left := 16;
BevelInner := bvSpace;
BevelOuter := bvLowered;
fName := TLabel.Create(fName);
self.InsertControl(fName);
with fName do
begin
Caption := sName;
Font.Name := 'Garamond';
Font.Size := 30;
Left := 7;
Top := 4;
end;
fStudNo := TLabel.Create(fStudNo);
self.InsertControl(fStudNo);
with fStudNo do
begin
Caption := sStudNo;
Font.Name := 'Garamond';
Font.Size := 15;
Left := 15;
Top := 52;
end;
fTimeAndDate := TLabel.Create(fTimeAndDate);
self.InsertControl(fTimeAndDate);
with fTimeAndDate do
begin
Caption := sTime + ' ' + sDate;
Font.Name := 'Garamond';
Font.Size := 20;
Left := 583;
Top := 4;
end;
fComment := TMemo.Create(fComment);
self.InsertControl(fComment);
with fComment do
begin
Lines.Add(sComment);
Font.Name := 'Garamond';
Font.Size := 12;
Left := 152;
Top := 56;
Height := 161;
Width := 633;
ReadOnly := True;
ScrollBars := ssVertical;
end;
end;
If you would like to see the other code used (reading the textfile, creating the array of objects, etc) please say so. It's not directly related to the class so I did not think it would be necessary.
Thank you in advance.
Edit: Based on #Remy Lebeau's code, and #NGLN's comments, I have decided to post everything necessary.
After fixing the class based on #Remy's code, I was still receiving the error. This led me to believe the error was where I was using the class, particularly in the array of objects I was creating.
Previously, my code was
for i := 0 to ComCount-1 do
begin
fArrObjects[i+1] := TSkeleton.Create(TargetForm);
with fArrObjects[i+1] do
begin
Parent := TargetForm;
TheName := fArrComments[i][0];
StudNo := fArrComments[i][1];
Time := fArrComments[i][2];
Date := fArrComments[i][3];
Comment := fArrComments[i][4];
ComCount := i+1;
end;
Changing
fArrObjects[i+1]
to
fArrObjects[i]
solved the issue.
Thank you to #Remy for correcting the errors in the class.

There are some problems with your code.
Parent := AOwner;
Do not set the Parent from inside of the constructor. It is the responsibility of the caller to set the Parent after the object is fully constructed first.
You do not have a destructor defined, and you are creating your child objects with nil Owners. TSkeleton should be the Owner instead, eg:
//fName := TLabel.Create(fName);
fName := TLabel.Create(Self);
...
You should not be calling InsertControl() directly. Use the Parent property instead, eg:
//self.InsertControl(fName);
fName.Parent := Self;
...
Why do you have two constructors? Your overridden constructor does not do anything, not even call the base constructor, and your custom constructor is not called at design-time (unless you have other code that is creating instances of TSkeleton programmably). I suggest you get rid of the custom constructor and expose published properties to manipulate the child controls as needed.
Lastly, since you are creating sub-components, you should mark them as such via TComponent.SetSubComponen().
With that said, try something more like this:
type
TSkeleton = class(TPanel)
private
fName : TLabel;
fStudNo : TLabel;
fTimeAndDate : TLabel;
fComment : TMemo;
fTime: string;
fDate: string;
fComCount: Integer;
function GetTheName: string;
procedure SetTheName(const AValue: string);
function GetStudNo: string;
procedure SetStudNo(const AValue: string);
procedure SetTime(const AValue: string);
procedure SetDate(const AValue: string);
function GetComment: string;
procedure SetComment(const AValue: string);
procedure SetComCount(AValue: integer);
public
constructor Create (AOwner: TComponent); override;
published
property TheName: string read GetTheName write SetTheName;
property StudNo: string read GetStudNo write SetStudNo;
property Time: string read fTime write SetTime;
property Date: string read fDate write SetDate;
property Comment: string read GetComment write SetComment;
property ComCount: integer read fComCount write SetComCount;
end;
constructor TSkeleton.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
Width := 800;
Height := 250;
Left := 16;
BevelInner := bvSpace;
BevelOuter := bvLowered;
fName := TLabel.Create(Self);
fName.SetSubComponent(True);
fName.Parent := Self;
fName.Font.Name := 'Garamond';
fName.Font.Size := 30;
fName.Left := 7;
fName.Top := 4;
fStudNo := TLabel.Create(Self);
fStudNo.SetSubComponent(True);
fStudNo.Parent := Self;
fStudNo.Font.Name := 'Garamond';
fStudNo.Font.Size := 15;
fStudNo.Left := 15;
fStudNo.Top := 52;
fTimeAndDate := TLabel.Create(Self);
fTimeAndDate.SetSubComponent(True);
fTimeAndDate.Parent := Self;
fTimeAndDate.Font.Name := 'Garamond';
fTimeAndDate.Font.Size := 20;
fTimeAndDate.Left := 583;
fTimeAndDate.Top := 4;
fComment := TMemo.Create(Self);
fComment.SetSubComponent(True);
fComment.Parent := Self;
fComment.Font.Name := 'Garamond';
fComment.Font.Size := 12;
fComment.Left := 152;
fComment.Top := 56;
fComment.Height := 161;
fComment.Width := 633;
fComment.ReadOnly := True;
fComment.ScrollBars := ssVertical;
end;
function TSkeleton.GetTheName: string;
begin
Result := fName.Caption;
end;
procedure TSkeleton.SetTheName(const AValue: string);
begin
fName.Caption := AValue;
end;
function TSkeleton.GetStudNo: string;
begin
Result := fStudNo.Caption;
end;
procedure TSkeleton.SetStudNo(const AValue: string);
begin
fStudNo.Caption := AValue;
end;
procedure TSkeleton.SetTime(const AValue: string);
begin
if fTime <> AValue then
begin
fTime := AValue;
fTimeAndDate.Caption := fTime + ' ' + fDate;
end;
end;
procedure TSkeleton.SetDate(const AValue: string);
begin
if fDate <> AValue then
begin
fDate := AValue;
fTimeAndDate.Caption := fTime + ' ' + fDate;
end;
end;
function TSkeleton.GetComment: string;
begin
Result := fComment.Text;
end;
procedure TSkeleton.SetComment(const AValue: string);
begin
fComment.Text := AValue;
end;
procedure TSkeleton.SetComCount(AValue: integer);
begin
if fComCount <> AValue then
begin
fComCount := AValue;
Top := 448+((FComCount-1)*250);
end;
end;

Related

Stack Overflow error on adding component to form

Hi I have just completed a component which implements a Table search on typing in an edit box and showing the results in a drop down dbCtrlGrid. It compiles and installs without any problem. But when placed on a form Delphi (7) I get a Stack Overflow save your work and restart Delphi. I cannot debug it as it is not on the form so can anyone help please?
unit QueryPnl;
interface
uses WinTypes, WinProcs, Messages, SysUtils, Classes, Controls,
Forms, Graphics, Extctrls, Eedit, Stdctrls, ABSMain,
Db, DBCtrls, EDBEdit, dbcgrids;
type
TQueryPanel = class(TPanel)
private
Addressmem : TDBMemo;
Display : TDBCtrlGrid;
DsQ1 : TDataSource;
Head : TLabel;
FDbase : TABSDatabase;
FTableName : string;
FOnInTextChange : TNotifyEvent;
procedure AutoInitialize;
procedure AutoDestroy;
protected
InText : TEedit;
NmText : TEDBEdit;
NumText : TEDBEdit;
Q1 : TABSQuery;
procedure InTextChange(Sender : TObject); overload;
procedure DoEnter; override;
procedure DoExit; override;
procedure Click; override;
procedure KeyPress(var Key : Char); override;
procedure Loaded; override;
procedure Paint; override;
function GetFDbase : TABSDatabase;
procedure SetFDbase(Value : TABSDatabase);
function GetFTableName : string;
procedure SetFTableName(Value : string);
public
procedure InTextChange;overload;
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
published
property OnClick;
property OnDblClick;
property OnDragDrop;
property OnEnter;
property OnExit;
property OnKeyDown;
property OnKeyPress;
property OnKeyUp;
property OnMouseDown;
property OnMouseMove;
property OnMouseUp;
property OnResize;
property DBase : TABSDatabase read FDBase write SetFDBase;
property TableName : String read FTableName write SetFTableName;
property OnInTextChange : TNotifyEvent read FOnInTextChange write FOnInTextChange;
end;
procedure Register;
implementation
procedure Register;
begin
RegisterComponents('Data Controls', [TQueryPanel]);
end;
procedure TQueryPanel.AutoInitialize;
begin
Addressmem.Parent := Display;
with Addressmem do
begin
Left := 2;
Top := 50;
Width := 115;
Height := 50;
DataField := 'Address';
TabOrder := 1;
end;
Display.Parent := Self;
with Display do
begin
DataSource:=DsQ1;
Left := 0;
Top := 54;
Width := 138;
Height := 315;
Color := $00E8DBCE;
PanelHeight := 105;
PanelWidth := 121;
ParentColor := False;
TabOrder := 2;
end;
DsQ1.DataSet:=Q1;
Head.Parent := Self;
with Head do
begin
Left := 1;
Top := 1;
Width := 136;
Height := 13;
Align := alTop;
Alignment := taCenter;
Caption := 'Quick Name Search';
Font.Charset := DEFAULT_CHARSET;
Font.Color := clRed;
Font.Height := -12;
Font.Name := 'MS Sans Serif';
Font.Style := [fsBold];
ParentFont := False;
end;
InText.Parent := Self;
with InText do
begin
Left := 0;
Top := 25;
Width := 121;
Height := 21;
TabOrder := 0;
OnChange := InTextChange;
UpCaseFirst := True;
ColorOnFocus := clYellow;
end;
NmText.Parent := Display;
with NmText do
begin
Left := 2;
Top := 8;
Width := 115;
Height := 21;
DataField := 'Name';
Font.Charset := DEFAULT_CHARSET;
Font.Color := clWindowText;
Font.Height := -11;
Font.Name := 'MS Sans Serif';
Font.Style := [fsBold];
ParentFont := False;
TabOrder := 0;
end;
NumText.Parent := Display;
with NumText do
begin
Left := 1;
Top := 29;
Width := 115;
Height := 21;
DataField := 'Number';
Font.Style := [fsBold];
TabOrder := 2;
end;
Q1.DatabaseName:=FDBase.Name;
Q1.RequestLive:=True;
end;
procedure TQueryPanel.AutoDestroy;
begin
Addressmem.Free;
Display.Free;
DsQ1.Free;
Head.Free;
InText.Free;
NmText.Free;
NumText.Free;
Q1.Free;
end;
procedure TQueryPanel.DoEnter;
begin
inherited DoEnter;
Height := 370;
end;
procedure TQueryPanel.DoExit;
begin
inherited DoExit;
Height := 55;
end;
function TQueryPanel.GetFDbase : TABSDatabase;
begin
Result := FDbase;
end;
procedure TQueryPanel.SetFDBase(Value : TABSDatabase);
begin
FDBase := Value;
// Other code to do when selecting the database
end;
function TQueryPanel.GetFTableName : string;
begin
Result := FTableName;
end;
procedure TQueryPanel.SetFTableName(Value : String);
begin
FTableName := Value;
// Other code to do when selecting the table
end;
procedure TQueryPanel.InTextChange(Sender : TObject);
begin
if Assigned(FOnInTextChange) then
FOnInTextChange(Sender);
Q1.Close;
Q1.SQL.Text:='select * from '+FTableName+' where Name like :nem';
Q1.ParamByName('nem').asString:=InText.Text;
Q1.Open;
end;
procedure TQueryPanel.Click;
begin
inherited Click;
end;
procedure TQueryPanel.KeyPress(var Key : Char);
const
TabKey = Char(VK_TAB);
EnterKey = Char(VK_RETURN);
begin
inherited KeyPress(Key);
end;
constructor TQueryPanel.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
Addressmem := TDBMemo.Create(Self);
Display := TDBCtrlGrid.Create(Self);
DsQ1 := TDataSource.Create(Self);
InText := TEedit.Create(Self);
Head := TLabel.Create(Self);
NmText := TEDBEdit.Create(Self);
NumText := TEDBEdit.Create(Self);
Q1 := TABSQuery.Create(Self);
AutoInitialize;
end;
destructor TQueryPanel.Destroy;
begin
inherited Destroy;
end;
procedure TQueryPanel.InTextChange;
begin
//
end;
procedure TQueryPanel.Loaded;
begin
inherited Loaded;
end;
procedure TQueryPanel.Paint;
begin
inherited Paint;
end;
end.
Many times, StackOverflow errors are due to an infinite recursion.
I think the code of these two procedures causes that:
procedure SetDBase(Value : TABSDatabase);
procedure SetTableName(Value : String);
Use Field like FDBase and FTableName to store the values (for example).
The 2 methods are causing the Set method to be called again infinitely.
In the private section, add this:
FDBase : TABSDataBase;
FTableName : String;
In the published section, add:
property DBase : TABSDataBase read FDBase write SetDBase;
property TableName : String read FTableName write SetTableName;
In the implementation, write:
procedure TQueryPanel.SetDBase(Value : TABSDatabase);
begin
FDBase := Value;
// Other code to do when selecting the database
end;
procedure TQueryPanel.SetTableName(Value : String);
begin
FTableName := Value;
// Other code to do when selecting the table
end;
Where I put comments "Other code...", if there is nothing to do, you don't need the setter at all.
Please keep attention that fields begin by letter F and properties don't.
I tracke the error - it was in the TQueryPanel.AutoInitialize;
I guess that the line
Q1.DatabaseName:=FDBase.Name;
was in the wrong place so I put in
procedure TQueryPanel.SetDbase(Value : TABSDatabase);
begin
FDbase:= Value;
Q1.DatabaseName:=FDBase.DatabaseName;
end;
and it works fine now.Thank you everyone for your help and suggestions
Actually it doesn't. The first panel of the DBCtrlGrid is fine but apparently you have to have the DBCtrlGrid.Panel as parent which is reserved and cannot be accessed. Shame it seemed an ideal solution in a limited space.

Writing a custom property inspector - How to handle inplace editor focus when validating values?

Overview
I am trying to write my own simple property inspector but I am facing a difficult and rather confusing problem. First though let me say that my component is not meant to work with or handle component properties, instead it will allow adding custom values to it. The full source code of my component is further down the question and it should look something like this once you have installed it in a package and run it from a new empty project:
Problem (brief)
The issue is regarding the use of inplace editors and validating the property values. The idea is, if a property value is not valid then show a message to the user notifying them that the value cannot be accepted, then focus back to the row and inplace editor that was originally focused on.
We can actually use Delphi's very own Object Inspector to illustrate the behavior I am looking for, for example try writing a string in the Name property that cannot be accepted then click away from the Object Inspector. A message is shown and upon closing it, it will focus back to the Name row.
Source Code
The question becomes too vague without any code but due to the nature of the component I am trying to write it's also quite large. I have stripped it down as much as possible for the purpose of the question and example. I am sure there will be some comments asking me why I didn't do this or do that instead but it's important to know that I am no Delphi expert and often I make wrong decisions and choices but I am always willing to learn so all comments are welcomed, especially if it aids in finding my solution.
unit MyInspector;
interface
uses
Winapi.Windows,
Winapi.Messages,
System.Classes,
System.SysUtils,
Vcl.Controls,
Vcl.Dialogs,
Vcl.StdCtrls,
Vcl.Graphics,
Vcl.Forms;
type
TMyInspectorItems = class(TObject)
private
FPropertyNames: TStringList;
FPropertyValues: TStringList;
procedure AddItem(APropName, APropValue: string);
procedure Clear;
public
constructor Create;
destructor Destroy; override;
end;
TOnMouseMoveEvent = procedure(Sender: TObject; X, Y: Integer) of object;
TOnSelectRowEvent = procedure(Sender: TObject; PropName, PropValue: string; RowIndex: Integer) of object;
TMyCustomInspector = class(TGraphicControl)
private
FInspectorItems: TMyInspectorItems;
FOnMouseMove: TOnMouseMoveEvent;
FOnSelectRow: TOnSelectRowEvent;
FRowCount: Integer;
FNamesFont: TFont;
FValuesFont: TFont;
FSelectedRow: Integer;
procedure SetNamesFont(const AValue: TFont);
procedure SetValuesFont(const AValue: TFont);
procedure CalculateInspectorHeight;
function GetMousePosition: TPoint;
function MousePositionToRowIndex: Integer;
function RowIndexToMousePosition(ARowIndex: Integer): Integer;
function GetRowHeight: Integer;
function GetValueRowWidth: Integer;
function RowExists(ARowIndex: Integer): Boolean;
function IsRowSelected: Boolean;
protected
procedure Loaded; override;
procedure Paint; override;
procedure WMKeyDown(var Message: TMessage); message WM_KEYDOWN;
procedure WMMouseDown(var Message: TMessage); message WM_LBUTTONDOWN;
procedure WMMouseMove(var Message: TMessage); message WM_MOUSEMOVE;
procedure WMMouseUp(var Message: TMessage); message WM_LBUTTONUP;
public
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
function RowCount: Integer;
property Items: TMyInspectorItems read FInspectorItems write FInspectorItems;
property OnMouseMove: TOnMouseMoveEvent read FOnMouseMove write FOnMouseMove;
property OnSelectRow: TOnSelectRowEvent read FOnSelectRow write FOnSelectRow;
published
property Align;
end;
TMyPropertyInspector = class(TScrollBox)
private
FInspector: TMyCustomInspector;
FInplaceStringEditor: TEdit;
FSelectedRowName: string;
FLastSelectedRowName: string;
FLastSelectedRow: Integer;
function SetPropertyValue(RevertToPreviousValueOnFail: Boolean): Boolean;
procedure InplaceStringEditorEnter(Sender: TObject);
procedure InplaceStringEditorExit(Sender: TObject);
procedure InplaceStringEditorKeyPress(Sender: TObject; var Key: Char);
procedure SelectRow(Sender: TObject; PropName, PropValue: string; RowIndex: Integer);
function ValidateStringValue(Value: string): Boolean;
protected
procedure Loaded; override;
procedure WMSize(var Message: TMessage); message WM_SIZE;
public
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
procedure AddItem(APropName, APropValue: string);
function GetSelectedPropertyName: string;
function GetSelectedPropertyValue: string;
function RowCount: Integer;
end;
var
FCanSelect: Boolean;
implementation
{ TMyInspectorItems }
constructor TMyInspectorItems.Create;
begin
inherited Create;
FPropertyNames := TStringList.Create;
FPropertyValues := TStringList.Create;
end;
destructor TMyInspectorItems.Destroy;
begin
FPropertyNames.Free;
FPropertyValues.Free;
inherited Destroy;
end;
procedure TMyInspectorItems.AddItem(APropName, APropValue: string);
begin
FPropertyNames.Add(APropName);
FPropertyValues.Add(APropValue);
end;
procedure TMyInspectorItems.Clear;
begin
FPropertyNames.Clear;
FPropertyValues.Clear;
end;
{ TMyCustomInspector }
constructor TMyCustomInspector.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
FInspectorItems := TMyInspectorItems.Create;
FNamesFont := TFont.Create;
FNamesFont.Color := clWindowText;
FNamesFont.Name := 'Segoe UI';
FNamesFont.Size := 9;
FNamesFont.Style := [];
FValuesFont := TFont.Create;
FValuesFont.Color := clNavy;
FValuesFont.Name := 'Segoe UI';
FValuesFont.Size := 9;
FValuesFont.Style := [];
end;
destructor TMyCustomInspector.Destroy;
begin
FInspectorItems.Free;
FNamesFont.Free;
FValuesFont.Free;
inherited Destroy;
end;
procedure TMyCustomInspector.Loaded;
begin
inherited Loaded;
end;
procedure TMyCustomInspector.Paint;
procedure DrawBackground;
begin
Canvas.Brush.Color := clWindow;
Canvas.Brush.Style := bsSolid;
Canvas.FillRect(Rect(0, 0, Self.Width, Self.Height));
end;
procedure DrawNamesBackground;
begin
Canvas.Brush.Color := clWindow;
Canvas.Brush.Style := bsSolid;
Canvas.FillRect(Rect(0, 0, Self.Width div 2, Self.Height));
end;
procedure DrawNamesSelection;
begin
if (FRowCount > -1) and (RowExists(MousePositionToRowIndex)) then
begin
Canvas.Brush.Color := $00E0E0E0;
Canvas.Brush.Style := bsSolid;
Canvas.FillRect(Rect(0, RowIndexToMousePosition(FSelectedRow),
Self.Width div 2, RowIndexToMousePosition(FSelectedRow) + GetRowHeight));
end;
end;
procedure DrawNamesText;
var
I: Integer;
Y: Integer;
begin
FRowCount := FInspectorItems.FPropertyNames.Count;
Canvas.Brush.Style := bsClear;
Canvas.Font.Color := FNamesFont.Color;
Canvas.Font.Name := FNamesFont.Name;
Canvas.Font.Size := FNamesFont.Size;
Y := 0;
for I := 0 to FInspectorItems.FPropertyNames.Count -1 do
begin
Canvas.TextOut(2, Y, FInspectorItems.FPropertyNames.Strings[I]);
Inc(Y, GetRowHeight);
end;
end;
procedure DrawValuesBackground;
begin
Canvas.Brush.Color := clWindow;
Canvas.Brush.Style := bsSolid;
Canvas.FillRect(Rect(Self.Width div 2, 0, Self.Width, Self.Height));
end;
procedure DrawValuesSelection;
begin
if (FRowCount > -1) and (RowExists(MousePositionToRowIndex)) then
begin
Canvas.DrawFocusRect(Rect(Self.Width div 2, RowIndexToMousePosition(FSelectedRow),
Self.Width, RowIndexToMousePosition(FSelectedRow) + GetRowHeight));
end;
end;
procedure DrawValues;
var
I, Y: Integer;
begin
FRowCount := FInspectorItems.FPropertyValues.Count;
Y := 0;
for I := 0 to FInspectorItems.FPropertyValues.Count -1 do
begin
Canvas.Brush.Style := bsClear;
Canvas.Font.Color := FValuesFont.Color;
Canvas.Font.Name := FValuesFont.Name;
Canvas.Font.Size := FValuesFont.Size;
Canvas.TextOut(Self.Width div 2 + 2, Y + 1, FInspectorItems.FPropertyValues.Strings[I]);
Inc(Y, GetRowHeight);
end;
end;
begin
DrawNamesBackground;
DrawNamesSelection;
DrawNamesText;
DrawValuesBackground;
DrawValuesSelection;
DrawValues;
end;
procedure TMyCustomInspector.WMKeyDown(var Message: TMessage);
begin
inherited;
case Message.WParam of
VK_DOWN:
begin
end;
end;
end;
procedure TMyCustomInspector.WMMouseDown(var Message: TMessage);
begin
inherited;
Parent.SetFocus;
FSelectedRow := MousePositionToRowIndex;
if FSelectedRow <> -1 then
begin
if Assigned(FOnSelectRow) then
begin
FOnSelectRow(Self, FInspectorItems.FPropertyNames.Strings[FSelectedRow],
FInspectorItems.FPropertyValues.Strings[FSelectedRow], FSelectedRow);
end;
end;
Invalidate;
end;
procedure TMyCustomInspector.WMMouseMove(var Message: TMessage);
begin
inherited;
if Assigned(FOnMouseMove) then
begin
FOnMouseMove(Self, GetMousePosition.X, GetMousePosition.Y);
end;
end;
procedure TMyCustomInspector.WMMouseUp(var Message: TMessage);
begin
inherited;
end;
procedure TMyCustomInspector.SetNamesFont(const AValue: TFont);
begin
FNamesFont.Assign(AValue);
Invalidate;
end;
procedure TMyCustomInspector.SetValuesFont(const AValue: TFont);
begin
FValuesFont.Assign(AValue);
Invalidate;
end;
procedure TMyCustomInspector.CalculateInspectorHeight;
var
I, Y: Integer;
begin
FRowCount := FInspectorItems.FPropertyNames.Count;
Y := GetRowHeight;
for I := 0 to FRowCount -1 do
begin
Inc(Y, GetRowHeight);
end;
if Self.Height <> Y then
Self.Height := Y;
end;
function TMyCustomInspector.GetMousePosition: TPoint;
var
Pt: TPoint;
begin
Pt := Mouse.CursorPos;
Pt := ScreenToClient(Pt);
Result := Pt;
end;
function TMyCustomInspector.MousePositionToRowIndex: Integer;
begin
Result := GetMousePosition.Y div GetRowHeight;
end;
function TMyCustomInspector.RowIndexToMousePosition(
ARowIndex: Integer): Integer;
begin
Result := ARowIndex * GetRowHeight;
end;
function TMyCustomInspector.GetRowHeight: Integer;
begin
Result := FNamesFont.Size * 2 + 1;
end;
function TMyCustomInspector.GetValueRowWidth: Integer;
begin
Result := Self.Width div 2;
end;
function TMyCustomInspector.RowCount: Integer;
begin
Result := FRowCount;
end;
function TMyCustomInspector.RowExists(ARowIndex: Integer): Boolean;
begin
Result := MousePositionToRowIndex < RowCount;
end;
function TMyCustomInspector.IsRowSelected: Boolean;
begin
Result := FSelectedRow <> -1;
end;
{ TMyPropertyInspector }
constructor TMyPropertyInspector.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
Self.DoubleBuffered := True;
Self.Height := 150;
Self.HorzScrollBar.Visible := False;
Self.TabStop := True; // needed to receive focus
Self.Width := 250;
FInspector := TMyCustomInspector.Create(Self);
FInspector.Parent := Self;
FInspector.Align := alTop;
FInspector.Height := 0;
FInspector.OnSelectRow := SelectRow;
FInplaceStringEditor := TEdit.Create(Self);
FInplaceStringEditor.Parent := Self;
FInplaceStringEditor.BorderStyle := bsNone;
FInplaceStringEditor.Color := clWindow;
FInplaceStringEditor.Height := 0;
FInplaceStringEditor.Left := 0;
FInplaceStringEditor.Name := 'MyPropInspectorInplaceStringEditor';
FInplaceStringEditor.Top := 0;
FInplaceStringEditor.Visible := False;
FInplaceStringEditor.Width := 0;
FInplaceStringEditor.Font.Assign(FInspector.FValuesFont);
FInplaceStringEditor.OnEnter := InplaceStringEditorEnter;
FInplaceStringEditor.OnExit := InplaceStringEditorExit;
FInplaceStringEditor.OnKeyPress := InplaceStringEditorKeyPress;
FCanSelect := True;
end;
destructor TMyPropertyInspector.Destroy;
begin
FInspector.Free;
FInplaceStringEditor.Free;
inherited Destroy;
end;
procedure TMyPropertyInspector.Loaded;
begin
inherited Loaded;
end;
procedure TMyPropertyInspector.WMSize(var Message: TMessage);
begin
FInspector.Width := Self.Width;
Invalidate;
end;
procedure TMyPropertyInspector.AddItem(APropName, APropValue: string);
begin
FInspector.CalculateInspectorHeight;
FInspector.Items.AddItem(APropName, APropValue);
FInspector.Invalidate;
Self.Invalidate;
end;
function TMyPropertyInspector.GetSelectedPropertyName: string;
begin
Result := '';
if FInspector.FSelectedRow <> -1 then
begin
Result := FInspector.FInspectorItems.FPropertyNames.Strings[FInspector.FSelectedRow];
end;
end;
function TMyPropertyInspector.GetSelectedPropertyValue: string;
begin
Result := '';
if FInspector.FSelectedRow <> -1 then
begin
Result := FInspector.FInspectorItems.FPropertyValues.Strings[FInspector.FSelectedRow];
end;
end;
function TMyPropertyInspector.RowCount: Integer;
begin
Result := FInspector.RowCount;
end;
procedure TMyPropertyInspector.InplaceStringEditorEnter(Sender: TObject);
begin
FCanSelect := False;
FLastSelectedRow := FInplaceStringEditor.Tag;
end;
procedure TMyPropertyInspector.InplaceStringEditorExit(Sender: TObject);
begin
if SetPropertyValue(True) then
begin
FCanSelect := True;
end;
end;
procedure TMyPropertyInspector.InplaceStringEditorKeyPress(Sender: TObject;
var Key: Char);
begin
if Key = Chr(VK_RETURN) then
begin
Key := #0;
FInplaceStringEditor.SelectAll;
end;
end;
procedure TMyPropertyInspector.SelectRow(Sender: TObject; PropName, PropValue: string; RowIndex: Integer);
begin
FSelectedRowName := PropName;
FLastSelectedRowName := PropName;
FInplaceStringEditor.Height := FInspector.GetRowHeight - 2;
FInplaceStringEditor.Left := Self.Width div 2;
FInplaceStringEditor.Tag := RowIndex;
FInplaceStringEditor.Text := GetSelectedPropertyValue;
FInplaceStringEditor.Top := FInspector.RowIndexToMousePosition(FInspector.FSelectedRow) + 1 - Self.VertScrollBar.Position;
FInplaceStringEditor.Visible := True;
FInplaceStringEditor.Width := FInspector.GetValueRowWidth - 3;
FInplaceStringEditor.SetFocus;
FInplaceStringEditor.SelectAll;
end;
function TMyPropertyInspector.SetPropertyValue(
RevertToPreviousValueOnFail: Boolean): Boolean;
var
S: string;
begin
Result := False;
S := FInplaceStringEditor.Text;
if ValidateStringValue(S) then
begin
Result := True;
end
else
begin
ShowMessage('"' + S + '"' + 'is not a valid value.');
Result := False;
end;
end;
function TMyPropertyInspector.ValidateStringValue(Value: string): Boolean;
begin
// a quick and dirty way of testing for a valid string value, here we just
// look for strings that are not zero length.
Result := Length(Value) > 0;
end;
end.
Problem (detailed)
The confusion I have all comes down to who receives focus first and how to handle and respond to it correctly. Because I am custom drawing my rows I determine where the mouse is when clicking on the inspector control and then I draw the selected row to show this. When handling the inplace editors however, especially the OnEnter and OnExit event I have been facing all kinds of funky problems where in some cases I have been stuck in a cycle of the validate error message repeatedly showing for example (because focus is switching from my inspector to the inplace editor and back and forth).
To populate my inspector at runtime you can do the following:
procedure TForm1.Button1Click(Sender: TObject);
begin
MyPropertyInspector1.AddItem('A', 'Some Text');
MyPropertyInspector1.AddItem('B', 'Hello World');
MyPropertyInspector1.AddItem('C', 'Blah Blah');
MyPropertyInspector1.AddItem('D', 'The Sky is Blue');
MyPropertyInspector1.AddItem('E', 'Another String');
end;
A little something you may try:
Click on a row
Delete the contents from the inplace editor
Select another row
The validate error message box appears (don't close it yet)
With the message box still visible, move your mouse over another row
Now press Enter to close the message box
You will notice the selected row has now moved to where the mouse was
What I need is after the validate message box has shown and closed, I need to set the focus back to the row that was been validated in the first place. It gets confusing because it seems (or so I think) that the inplace editors OnExit is been called after the WMMouseDown(var Message: TMessage); code of my inspector.
To put it as simple as I can if the question remains unclear, the behavior of the Delphi Object Inspector is what I am trying to implement into my component. You enter a value into the inplace editors, if it fails the validation then display a messagebox and then focus back to the row that was last selected. The inplace editor validation should occur as soon as focus is switched away from the inplace editor.
I just can't seem to figure out what is been called first and what is blocking events been fired, it becomes confusing because the way I draw my selected row is determined by where the mouse was when clicking on the inspector control.
This is your flow of events:
TMyCustomInspector.WMMouseDown is called
Therein, Parent.SetFocus is called
The focus is removed from the Edit control and TMyPropertyInspector.InplaceStringEditorExit is called
The message dialog is shown by SetPropertyValue
FSelectedRow is being reset
TMyPropertyInspector.SelectRow is called (via TMyCustomInspector.FOnSelectRow) which resets the focus to the replaced Edit control.
What you need to is to prevent FSelectedRow being reset in case of validation did not succeed. All needed ingredients are already there, just add this one condition:
if FCanSelect then
FSelectedRow := MousePositionToRowIndex;
A few remarks:
Make FCanSelect a protected or private field of TMyCustomInspector,
You need to check for limits in TMyCustomInspector.MousePositionToRowIndex in order to return -1.
Your problem is very interesting. From what I gather, you want a way to reset the focus to the invalid row, when the evaluation is false. Where I see you do this is in your SetPropertyValue function. I believe if you do the following, you will be able to reset the focus after the user clicks "OK" in the message:
function TMyPropertyInspector.SetPropertyValue(
RevertToPreviousValueOnFail: Boolean): Boolean;
var
S: string;
begin
Result := False;
S := FInplaceStringEditor.Text;
if ValidateStringValue(S) then
begin
Result := True;
end
else
begin
if (MessageDlg('"' + S + '"' + 'is not a valid value.', mtError, [mbOK], 0)) = mrOK then
begin
SelectRow(nil, FSelectedRowName, FInplaceStringEditor.Text, FInplaceStringEditor.Tag);
end;
Result := False;
end;
end;
Changing the ShowMessage to MessageDlg will allow for an action to occur when the button is pressed. Then calling your SelectRow function with (what I believe are) global variables representing the information about the last row will set that focus to the bad cell.

OnClick event handler for control in custom component not working (Lazarus)

Using: Lazarus 1.2.0; Windows 32-bit application
I have written a custom component derived from TPanel and it conains 4 TEdit controls. I've written the OnClick event handler code for the TEdits. However its not working at runtime ie the events are not firing. I'm not sure what I missed. Please can you tell me what I'm doing wrong?
Component code follows:
unit uEditPanel;
{$mode objfpc}{$H+}
interface
uses
Classes, SysUtils, LResources, Forms, Controls, Graphics, Dialogs, ExtCtrls, StdCtrls;
type
{ TEditPanel }
TEditPanel = class(TCustomPanel)
Edit0: TEdit;
Edit1: TEdit;
Edit2: TEdit;
Edit3: TEdit;
Edit4: TEdit;
procedure SetEdit1OnClick(const AEvent: TNotifyEvent);
procedure SetEdit2OnClick(const AEvent: TNotifyEvent);
procedure SetEdit3OnClick(const AEvent: TNotifyEvent);
procedure SetEdit4OnClick(const AEvent: TNotifyEvent);
private
{ Private declarations }
FEdit1OnClick: TNotifyEvent;
FEdit2OnClick: TNotifyEvent;
FEdit3OnClick: TNotifyEvent;
FEdit4OnClick: TNotifyEvent;
function GetEdit0Text: string;
procedure SetEdit0Text(AText: string);
function GetEdit1Text: string;
procedure SetEdit1Text(AText: string);
function GetEdit2Text: string;
procedure SetEdit2Text(AText: string);
function GetEdit3Text: string;
procedure SetEdit3Text(AText: string);
function GetEdit4Text: string;
procedure SetEdit4Text(AText: string);
protected
{ Protected declarations }
procedure DoEdit1Click;
procedure DoEdit2Click;
procedure DoEdit3Click;
procedure DoEdit4Click;
public
{ Public declarations }
constructor Create(AOwner: TComponent); override;
published
{ Published declarations }
property Edit0Text: string read GetEdit0Text write SetEdit0Text;
property Edit1Text: string read GetEdit1Text write SetEdit1Text;
property Edit2Text: string read GetEdit2Text write SetEdit2Text;
property Edit3Text: string read GetEdit3Text write SetEdit3Text;
property Edit4Text: string read GetEdit4Text write SetEdit4Text;
property OnEdit1Click: TNotifyEvent read FEdit1OnClick write SetEdit1OnClick;
property OnEdit2Click: TNotifyEvent read FEdit2OnClick write SetEdit2OnClick;
property OnEdit3Click: TNotifyEvent read FEdit3OnClick write SetEdit3OnClick;
property OnEdit4Click: TNotifyEvent read FEdit4OnClick write SetEdit4OnClick;
end;
procedure Register;
implementation
procedure Register;
begin
RegisterComponents('Standard', [TEditPanel]);
end;
{ TEditPanel }
procedure TEditPanel.SetEdit1OnClick(const AEvent: TNotifyEvent);
begin
FEdit1OnClick := AEvent;
end;
procedure TEditPanel.SetEdit2OnClick(const AEvent: TNotifyEvent);
begin
FEdit2OnClick := AEvent;
end;
procedure TEditPanel.SetEdit3OnClick(const AEvent: TNotifyEvent);
begin
FEdit3OnClick := AEvent;
end;
procedure TEditPanel.SetEdit4OnClick(const AEvent: TNotifyEvent);
begin
FEdit4OnClick := AEvent;
end;
function TEditPanel.GetEdit0Text: string;
begin
Result := Edit0.Text;
end;
procedure TEditPanel.SetEdit0Text(AText: string);
begin
Edit0.Text := AText;
end;
function TEditPanel.GetEdit1Text: string;
begin
Result := Edit1.Text;
end;
procedure TEditPanel.SetEdit1Text(AText: string);
begin
Edit1.Text := AText;
end;
function TEditPanel.GetEdit2Text: string;
begin
Result := Edit2.Text;
end;
procedure TEditPanel.SetEdit2Text(AText: string);
begin
Edit2.Text := AText;
end;
function TEditPanel.GetEdit3Text: string;
begin
Result := Edit3.Text;
end;
procedure TEditPanel.SetEdit3Text(AText: string);
begin
Edit3.Text := AText;
end;
function TEditPanel.GetEdit4Text: string;
begin
Result := Edit4.Text;
end;
procedure TEditPanel.SetEdit4Text(AText: string);
begin
Edit4.Text := AText;
end;
procedure TEditPanel.DoEdit1Click;
begin
if Assigned(FEdit1OnClick) then
FEdit1OnClick(Self);
end;
procedure TEditPanel.DoEdit2Click;
begin
if Assigned(FEdit2OnClick) then
FEdit2OnClick(Self);
end;
procedure TEditPanel.DoEdit3Click;
begin
if Assigned(FEdit3OnClick) then
FEdit3OnClick(Self);
end;
procedure TEditPanel.DoEdit4Click;
begin
if Assigned(FEdit4OnClick) then
FEdit4OnClick(Self);
end;
constructor TEditPanel.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
Edit0 := TEdit.Create(Self);
Edit1 := TEdit.Create(Self);
Edit2 := TEdit.Create(Self);
Edit3 := TEdit.Create(Self);
Edit4 := TEdit.Create(Self);
Edit0.Parent := Self;
Edit1.Parent := Self;
Edit2.Parent := Self;
Edit3.Parent := Self;
Edit4.Parent := Self;
Edit0.SetSubComponent(True);
Edit1.SetSubComponent(True);
Edit2.SetSubComponent(True);
Edit3.SetSubComponent(True);
Edit4.SetSubComponent(True);
Edit1.ReadOnly := True;
Edit2.ReadOnly := True;
Edit3.ReadOnly := True;
Edit4.ReadOnly := True;
Edit1.OnClick := FEdit1OnClick;
Edit2.OnClick := FEdit2OnClick;
Edit3.OnClick := FEdit3OnClick;
Edit4.OnClick := FEdit4OnClick;
Caption := EmptyStr;
Height := 117;
Width := 289;
BevelOuter := bvNone;
ClientHeight := 117;
ClientWidth := 289;
Edit0.Left := 0;
Edit0.Height := 21;
Edit0.Top := 0;
Edit0.Width := 288;
Edit0.BorderStyle := bsNone;
Edit0.TabOrder := 0;
Edit1.Left := 0;
Edit1.Height := 21;
Edit1.Top := 24;
Edit1.Width := 288;
Edit1.BorderStyle := bsNone;
Edit1.TabOrder := 1;
Edit1.Font.Color := clGray;
Edit2.Left := 0;
Edit2.Height := 21;
Edit2.Top := 48;
Edit2.Width := 288;
Edit2.BorderStyle := bsNone;
Edit2.TabOrder := 2;
Edit2.Font.Color := clGray;
Edit3.Left := 0;
Edit3.Height := 21;
Edit3.Top := 72;
Edit3.Width := 288;
Edit3.BorderStyle := bsNone;
Edit3.TabOrder := 3;
Edit3.Font.Color := clGray;
Edit4.Left := 0;
Edit4.Height := 21;
Edit4.Top := 96;
Edit4.Width := 288;
Edit4.BorderStyle := bsNone;
Edit4.TabOrder := 4;
Edit4.Font.Color := clGray;
end;
end.
At runtime my component looks like this:
I would suggest the following approach instead. Not only does it ensure the various OnClick events work correctly, but it also consolidates all of the duplicate code for easier management:
type
TEditPanel = class(TCustomPanel)
private
{ Private declarations }
FEdits: array[0..4] of TEdit;
FEditOnClick: array[1..4] of TNotifyEvent;
function GetEditOnClick(Index: Index): TNotifyEvent;
procedure SetEditOnClick(Index: Index; const AEvent: TNotifyEvent);
function GetEditText(Index: Integer): string;
procedure SetEditText(Index: Integer; const AText: string);
protected
{ Protected declarations }
procedure DoEditClick(Sender: TObject);
public
{ Public declarations }
constructor Create(AOwner: TComponent); override;
published
{ Published declarations }
property Edit0Text: string read GetEditText write SetEditText index 0;
property Edit1Text: string read GetEditText write SetEditText index 1;
property Edit2Text: string read GetEditText write SetEditText index 2;
property Edit3Text: string read GetEditText write SetEditText index 3;
property Edit4Text: string read GetEditText write SetEditText index 4;
property OnEdit1Click: TNotifyEvent read GetEditOnClick write SetEditOnClick index 1;
property OnEdit2Click: TNotifyEvent read GetEditOnClick write SetEditOnClick index 2;
property OnEdit3Click: TNotifyEvent read GetEditOnClick write SetEditOnClick index 3;
property OnEdit4Click: TNotifyEvent read GetEditOnClick write SetEditOnClick index 4;
end;
procedure TEditPanel.GetEditOnClick(Index: Integer): TNotifyEvent;
begin
Result := FEditOnClick[Index];
end;
procedure TEditPanel.SetEditOnClick(Index: Integer; const AEvent: TNotifyEvent);
begin
FEditOnClick[Index] := AEvent;
end;
function TEditPanel.GetEditText(Index: Integer): string;
begin
Result := FEdits[Index].Text;
end;
procedure TEditPanel.SetEditText(Index: Integer; const AText: string);
begin
FEdits[Index].Text := AText;
end;
procedure TEditPanel.DoEditClick(Sender: TObject);
var
Evt: TNotifyEvent;
begin
Evt := FEditOnClick[TEdit(Sender).Tag];
if Assigned(Evt) then
Evt(Self);
end;
constructor TEditPanel.Create(AOwner: TComponent);
var
I: Integer;
begin
inherited Create(AOwner);
Caption := EmptyStr;
Height := 117;
Width := 289;
BevelOuter := bvNone;
ClientHeight := 117;
ClientWidth := 289;
for I := 0 To 4 do
begin
FEdits[I] := TEdit.Create(Self);
FEdits[I].Parent := Self;
FEdits[I].SetSubComponent(True);
FEdits[I].ReadOnly := True;
FEdits[I].Left := 0;
FEdits[I].Height := 21;
FEdits[I].Top := 24 * I;
FEdits[I].Width := 288;
FEdits[I].BorderStyle := bsNone;
FEdits[I].TabOrder := I;
if I > 0 then
begin
FEdits[I].Tag := I;
FEdits[I].Font.Color := clGray;
FEdits[I].OnClick := DoEditClick;
end;
end;
end;
You store the events into a field but you do not set the controls event.
constructor TEditPanel.Create(AOwner: TComponent);
begin
...
// Assign the current value, but is nil at this moment
Edit1.OnClick := FEdit1OnClick;
...
end;
procedure TEditPanel.SetEdit1OnClick(const AEvent: TNotifyEvent);
begin
// set a new value only to the field
FEdit1OnClick := AEvent;
end;
There is no magic value transport from the field and the edit control event handler. You have to set the value also the the control event.
But you can access the event properties direct from the controls as you do with the Text properties.
And you should have a look at repeating code to simplify/shorten it.
unit uEditPanel;
{$mode objfpc}{$H+}
interface
uses
Classes, SysUtils, LResources, Forms, Controls, Graphics, Dialogs, ExtCtrls, StdCtrls;
type
{ TEditPanel }
TEditPanel = class( TCustomPanel )
private
Edit0 : TEdit;
Edit1 : TEdit;
Edit2 : TEdit;
Edit3 : TEdit;
Edit4 : TEdit;
{ Private declarations }
function GetEditOnClick( const Index : Integer ) : TNotifyEvent;
function GetEditText( const Index : Integer ) : string;
procedure SetEditOnClick( const Index : Integer; const Value : TNotifyEvent );
procedure SetEditText( const Index : Integer; const Value : string );
procedure InitEdit( AEdit : TEdit; ATop, ATabOrder : Integer; AReadOnly : Boolean );
protected
{ Protected declarations }
public
{ Public declarations }
constructor Create( AOwner : TComponent ); override;
published
{ Published declarations }
property Edit0Text : string index 0 read GetEditText write SetEditText;
property Edit1Text : string index 1 read GetEditText write SetEditText;
property Edit2Text : string index 2 read GetEditText write SetEditText;
property Edit3Text : string index 3 read GetEditText write SetEditText;
property Edit4Text : string index 4 read GetEditText write SetEditText;
property OnEdit1Click : TNotifyEvent index 1 read GetEditOnClick write SetEditOnClick;
property OnEdit2Click : TNotifyEvent index 2 read GetEditOnClick write SetEditOnClick;
property OnEdit3Click : TNotifyEvent index 3 read GetEditOnClick write SetEditOnClick;
property OnEdit4Click : TNotifyEvent index 4 read GetEditOnClick write SetEditOnClick;
end;
procedure Register;
implementation
procedure Register;
begin
RegisterComponents( 'Standard', [TEditPanel] );
end;
{ TEditPanel }
function TEditPanel.GetEditOnClick( const Index : Integer ) : TNotifyEvent;
begin
case index of
0 :
Result := Edit0.OnClick;
1 :
Result := Edit1.OnClick;
2 :
Result := Edit2.OnClick;
3 :
Result := Edit3.OnClick;
4 :
Result := Edit4.OnClick;
end;
end;
function TEditPanel.GetEditText( const Index : Integer ) : string;
begin
case index of
0 :
Result := Edit0.Text;
1 :
Result := Edit1.Text;
2 :
Result := Edit2.Text;
3 :
Result := Edit3.Text;
4 :
Result := Edit4.Text;
end;
end;
procedure TEditPanel.SetEditOnClick( const Index : Integer; const Value : TNotifyEvent );
begin
case index of
0 :
Edit0.OnClick := Value;
1 :
Edit1.OnClick := Value;
2 :
Edit2.OnClick := Value;
3 :
Edit3.OnClick := Value;
4 :
Edit4.OnClick := Value;
end;
end;
procedure TEditPanel.SetEditText( const Index : Integer; const Value : string );
begin
case index of
0 :
Edit0.Text := Value;
1 :
Edit1.Text := Value;
2 :
Edit2.Text := Value;
3 :
Edit3.Text := Value;
4 :
Edit4.Text := Value;
end;
end;
procedure TEditPanel.InitEdit( AEdit : TEdit; ATop, ATabOrder : Integer; AReadOnly : Boolean );
begin
AEdit.Parent := Self;
AEdit.SetSubComponent( True );
AEdit.ReadOnly := AReadOnly;
AEdit.Left := 0;
AEdit.Top := ATop;
AEdit.Height := 21;
AEdit.Width := 288;
AEdit.BorderStyle := bsNone;
AEdit.TabOrder := ATabOrder;
if AReadOnly
then
AEdit.Color := clGray;
end;
constructor TEditPanel.Create( AOwner : TComponent );
begin
inherited Create( AOwner );
Caption := EmptyStr;
Height := 117;
Width := 289;
BevelOuter := bvNone;
ClientHeight := 117;
ClientWidth := 289;
Edit0 := TEdit.Create( Self );
Edit1 := TEdit.Create( Self );
Edit2 := TEdit.Create( Self );
Edit3 := TEdit.Create( Self );
Edit4 := TEdit.Create( Self );
InitEdit( Edit0, 0, 0, False );
InitEdit( Edit1, 24, 1, True );
InitEdit( Edit2, 48, 2, True );
InitEdit( Edit3, 72, 3, True );
InitEdit( Edit4, 96, 4, True );
end;
end.
Your code can be written very short and will even become shorter if you manage the edit controls with an array.

Performance issues re-sizing large amount of components on form resize

I feel my failure so far lies in search terms as information on this has to be pretty common. Basically I am looking for common solutions and best practices when performing resizes on several components while a form is resized.
I have a form with a component that is based upon TScrollBox. The ScrollBox contains rows which are added dynamically at run time. They are basically a subcomponent. Each one has an image on the left and a memo on the right. The height is set based upon the width and aspect ratio of the image. Upon the resize of the scroll box a loop sets the width of the rows triggering the rows own internal resize. The loop also sets the relative top position if the heights have changed.
Screen shot:
Around 16 rows performs fine. My goal is closer to 32 rows which is very choppy and can peg a core at 100% usage.
I have tried:
Added a check to prevent a new resize starting while the previous has yet to complete. It answered if it occured and it does sometimes.
I tried preventing it resizing more often than every 30 ms which would allow for 30 frame per second drawing. Mixed results.
Changed the rows base component from TPanel to TWinControl. Not sure if there is a performance penalty using the Panel but its an old habit.
With and without double buffering.
I would like to allow row resizing to occur during a resize as a preview to how large the image will be in the row. That eliminates one obvious solution that in some applications is an acceptable loss.
Right now the resize code internally for the row is completely dynamic and based upon the dimensions of each image. Next thing I plan to try is to basically specify the Aspect Ratio, Max Width/Height based on the largest image in the collection. This should reduce the amount of math per row. But it seems like the issues are more the resize event and the loop itself?
Full unit code for the components:
unit rPBSSVIEW;
interface
uses
Classes, Controls, Forms, ExtCtrls, StdCtrls, Graphics, SysUtils, rPBSSROW, Windows, Messages;
type
TPBSSView = class(TScrollBox)
private
public
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
procedure ResizeRows(Sender: TObject);
procedure AddRow(FileName: String);
procedure FillRow(Row: Integer; ImageStream: TMemoryStream);
end;
var
PBSSrow: Array of TPBSSRow;
Resizingn: Boolean;
procedure Register;
implementation
procedure Register;
begin
RegisterComponents('Standard', [TScrollBox]);
end;
procedure TPBSSView.AddRow(FileName: String);
begin
SetLength(PBSSrow,(Length(PBSSrow) + 1));
PBSSrow[Length(PBSSrow)-1] := TPBSSRow.create(self);
With PBSSrow[Length(PBSSrow)-1] do
begin
Left := 2;
if (Length(PBSSrow)-1) = 0 then Top := 2 else Top := ((PBSSRow[Length(PBSSRow) - 2].Top + PBSSRow[Length(PBSSRow) - 2].Height) + 2);
Width := (inherited ClientWidth - 4);
Visible := True;
Parent := Self;
PanelLeft.Caption := FileName;
end;
end;
procedure TPBSSView.FillRow(Row: Integer; ImageStream: TMemoryStream);
begin
PBSSRow[Row].LoadImageFromStream(ImageStream);
end;
procedure TPBSSView.ResizeRows(Sender: TObject);
var
I, X: Integer;
begin
if Resizingn then exit
else
begin
Resizingn := True;
HorzScrollBar.Visible := False;
X := (inherited ClientWidth - 4);
if Length(PBSSrow) > 0 then
for I := 0 to Length(PBSSrow) - 1 do
Begin
PBSSRow[I].Width := X; //Set Width
if not (I = 0) then //Move all next ones down.
begin
PBSSRow[I].Top := (PBSSRow[(I - 1)].Top + PBSSRow[(I - 1)].Height) + 2;
end;
Application.ProcessMessages;
End;
HorzScrollBar.Visible := True;
Resizingn := False;
end;
end;
constructor TPBSSView.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
OnResize := ResizeRows;
DoubleBuffered := True;
VertScrollBar.Tracking := True;
Resizingn := False;
end;
destructor TPBSSView.Destroy;
begin
inherited;
end;
end.
Row Code:
unit rPBSSROW;
interface
uses
Classes, Controls, Forms, ExtCtrls, StdCtrls, Graphics, pngimage, SysUtils;
type
TPBSSRow = class(TWinControl)
private
FImage: TImage;
FPanel: TPanel;
FMemo: TMemo;
FPanelLeft: TPanel;
FPanelRight: TPanel;
FImageWidth: Integer;
FImageHeight: Integer;
public
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
procedure MyPanelResize(Sender: TObject);
procedure LeftPanelResize(Sender: TObject);
published
procedure LoadImageFromStream(ImageStream: TMemoryStream);
property Image: TImage read FImage;
property Panel: TPanel read FPanel;
property PanelLeft: TPanel read FPanelLeft;
property PanelRight: TPanel read FPanelRight;
end;
procedure Register;
implementation
procedure Register;
begin
RegisterComponents('Standard', [TWinControl]);
end;
procedure TPBSSRow.MyPanelResize(Sender: TObject);
begin
if (Width - 466) <= FImageWidth then FPanelLeft.Width := (Width - 466)
else FPanelLeft.Width := FImageWidth;
FPanelRight.Width := (Width - FPanelLeft.Width);
end;
procedure TPBSSRow.LeftPanelResize(Sender: TObject);
var
AspectRatio: Extended;
begin
FPanelRight.Left := (FPanelLeft.Width);
//Enforce Info Minimum Height or set Height
if FImageHeight > 0 then AspectRatio := (FImageHeight/FImageWidth) else
AspectRatio := 0.4;
if (Round(AspectRatio * FPanelLeft.Width)) >= 212 then
begin
Height := (Round(AspectRatio * FPanelLeft.Width));
FPanelLeft.Height := Height;
FPanelRight.Height := Height;
end
else
begin
Height :=212;
FPanelLeft.Height := Height;
FPanelRight.Height := Height;
end;
if Fimage.Height >= FImageHeight then FImage.Stretch := False else Fimage.Stretch := True;
if Fimage.Width >= FImageWidth then FImage.Stretch := False else Fimage.Stretch := True;
end;
procedure TPBSSRow.LoadImageFromStream(ImageStream: TMemoryStream);
var
P: TPNGImage;
n: Integer;
begin
P := TPNGImage.Create;
ImageStream.Position := 0;
P.LoadFromStream(ImageStream);
FImage.Picture.Assign(P);
FImageWidth := P.Width;
FImageHeight := P.Height;
end;
constructor TPBSSRow.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
BevelInner := bvNone;
BevelOuter := bvNone;
BevelKind := bkNone;
Color := clWhite;
OnResize := MyPanelResize;
DoubleBuffered := True;
//Left Panel for Image
FPanelLeft := TPanel.Create(Self);
with FPanelLeft do
begin
SetSubComponent(true);
Align := alLeft;
Parent := Self;
//SetBounds(0,0,100,100);
ParentBackground := False;
Color := clBlack;
Font.Color := clLtGray;
Constraints.MinWidth := 300;
BevelInner := bvNone;
BevelOuter := bvNone;
BevelKind := bkNone;
BorderStyle := bsNone;
OnResize := LeftPanelResize;
end;
//Image for left panel
FImage := TImage.Create(Self);
FImage.SetSubComponent(true);
FImage.Align := alClient;
FImage.Parent := FPanelLeft;
FImage.Center := True;
FImage.Stretch := True;
FImage.Proportional := True;
//Right Panel for Info
FPanelRight := TPanel.Create(Self);
with FPanelRight do
begin
SetSubComponent(true);
Parent := Self;
Padding.SetBounds(2,5,5,2);
BevelInner := bvNone;
BevelOuter := bvNone;
BevelKind := bkNone;
BorderStyle := bsNone;
Color := clLtGray;
end;
//Create Memo in Right Panels
FMemo := TMemo.create(self);
with FMemo do
begin
SetSubComponent(true);
Parent := FPanelRight;
Align := alClient;
BevelOuter := bvNone;
BevelInner := bvNone;
BorderStyle := bsNone;
Color := clLtGray;
end;
end;
destructor TPBSSRow.Destroy;
begin
inherited;
end;
end.
A few tips:
TWinControl already ís a container, you do not need another panel inside it to add controls
You do not need an TImage component to view a graphic, that can also with TPaintBox, or as in my example control below, a TCustomControl,
Since all of your other panels are not recognizable (borders and bevels are disabled), loose them altogether and place the TMemo directly on your row control,
SetSubComponent is only for design time usage. You do not need it. Nor the Register procedures for that matter.
Put the global rows array inside your class definition, otherwise multiple TPBSSView controls will use the same array!
TWinControl already tracks all its child controls, so you won't need the array anyway, see my example below,
Make use of the Align property to save you from realigning manually,
If the memo control is just for showing text, then remove it and paint the text yourself.
Try this one for starters:
unit PBSSView;
interface
uses
Windows, Messages, Classes, Controls, SysUtils, Graphics, ExtCtrls, StdCtrls,
Forms, PngImage;
type
TPBSSRow = class(TCustomControl)
private
FGraphic: TPngImage;
FStrings: TStringList;
function ImageHeight: Integer; overload;
function ImageHeight(ControlWidth: Integer): Integer; overload;
function ImageWidth: Integer; overload;
function ImageWidth(ControlWidth: Integer): Integer; overload;
procedure WMEraseBkgnd(var Message: TWmEraseBkgnd); message WM_ERASEBKGND;
procedure WMWindowPosChanging(var Message: TWMWindowPosChanging);
message WM_WINDOWPOSCHANGING;
protected
procedure Paint; override;
procedure RequestAlign; override;
public
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
procedure LoadImageFromStream(Stream: TMemoryStream);
property Strings: TStringList read FStrings;
end;
TPBSSView = class(TScrollBox)
private
function GetRow(Index: Integer): TPBSSRow;
procedure WMEnterSizeMove(var Message: TMessage); message WM_ENTERSIZEMOVE;
procedure WMEraseBkgnd(var Message: TWmEraseBkgnd); message WM_ERASEBKGND;
procedure WMExitSizeMove(var Message: TMessage); message WM_EXITSIZEMOVE;
protected
procedure PaintWindow(DC: HDC); override;
public
constructor Create(AOwner: TComponent); override;
procedure AddRow(const FileName: TFileName);
procedure FillRow(Index: Integer; ImageStream: TMemoryStream);
property Rows[Index: Integer]: TPBSSRow read GetRow;
end;
implementation
{ TPBSSRow }
constructor TPBSSRow.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
Width := 300;
Height := 50;
FStrings := TStringList.Create;
end;
destructor TPBSSRow.Destroy;
begin
FStrings.Free;
FGraphic.Free;
inherited Destroy;
end;
function TPBSSRow.ImageHeight: Integer;
begin
Result := ImageHeight(Width);
end;
function TPBSSRow.ImageHeight(ControlWidth: Integer): Integer;
begin
if (FGraphic <> nil) and not FGraphic.Empty then
Result := Round(ImageWidth(ControlWidth) * FGraphic.Height / FGraphic.Width)
else
Result := Height;
end;
function TPBSSRow.ImageWidth: Integer;
begin
Result := ImageWidth(Width);
end;
function TPBSSRow.ImageWidth(ControlWidth: Integer): Integer;
begin
Result := ControlWidth div 2;
end;
procedure TPBSSRow.LoadImageFromStream(Stream: TMemoryStream);
begin
FGraphic.Free;
FGraphic := TPngImage.Create;
Stream.Position := 0;
FGraphic.LoadFromStream(Stream);
Height := ImageHeight + Padding.Bottom;
end;
procedure TPBSSRow.Paint;
var
R: TRect;
begin
Canvas.StretchDraw(Rect(0, 0, ImageWidth, ImageHeight), FGraphic);
SetRect(R, ImageWidth, 0, Width, ImageHeight);
Canvas.FillRect(R);
Inc(R.Left, 10);
DrawText(Canvas.Handle, FStrings.Text, -1, R, DT_EDITCONTROL or
DT_END_ELLIPSIS or DT_NOFULLWIDTHCHARBREAK or DT_NOPREFIX or DT_WORDBREAK);
Canvas.FillRect(Rect(0, ImageHeight, Width, Height));
end;
procedure TPBSSRow.RequestAlign;
begin
{eat inherited}
end;
procedure TPBSSRow.WMEraseBkgnd(var Message: TWmEraseBkgnd);
begin
Message.Result := 1;
end;
procedure TPBSSRow.WMWindowPosChanging(var Message: TWMWindowPosChanging);
begin
inherited;
if (FGraphic <> nil) and not FGraphic.Empty then
Message.WindowPos.cy := ImageHeight(Message.WindowPos.cx) + Padding.Bottom;
end;
{ TPBSSView }
procedure TPBSSView.AddRow(const FileName: TFileName);
var
Row: TPBSSRow;
begin
Row := TPBSSRow.Create(Self);
Row.Align := alTop;
Row.Padding.Bottom := 2;
Row.Parent := Self;
end;
constructor TPBSSView.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
VertScrollBar.Tracking := True;
end;
procedure TPBSSView.FillRow(Index: Integer; ImageStream: TMemoryStream);
begin
Rows[Index].LoadImageFromStream(ImageStream);
end;
function TPBSSView.GetRow(Index: Integer): TPBSSRow;
begin
Result := TPBSSRow(Controls[Index]);
end;
procedure TPBSSView.PaintWindow(DC: HDC);
begin
{eat inherited}
end;
procedure TPBSSView.WMEnterSizeMove(var Message: TMessage);
begin
if not AlignDisabled then
DisableAlign;
inherited;
end;
procedure TPBSSView.WMEraseBkgnd(var Message: TWmEraseBkgnd);
var
DC: HDC;
begin
DC := GetDC(Handle);
try
FillRect(DC, Rect(0, VertScrollBar.Range, Width, Height), Brush.Handle);
finally
ReleaseDC(Handle, DC);
end;
Message.Result := 1;
end;
procedure TPBSSView.WMExitSizeMove(var Message: TMessage);
begin
inherited;
if AlignDisabled then
EnableAlign;
end;
end.
If this still performs badly, then there are multiple other enhancements possible.
Update:
Flickering eliminated by overriding/intercepting WM_ERASEBKGND (and intercepting PaintWindow for versions < XE2),
Better performance by making use of DisableAlign and EnableAlign.
I don't know if this will make a significant difference, but instead setting PBSSRow[I].Width and PBSSRow[I].Top separately, make one call to PBSSRow[I].SetBounds instead. This will save you one Resize event for that SubComponent.

Why doesn't my size-changing control work when it shares a form with a TSplitter?

I'm writing a panel control that allows the user to mimimize the panel and to hide the components on this panel.
A single THidePanel seems to work as expected, but not when I put two of them on a form separated by a splitter. The first panel is aligned alLeft; the second panel alClient:
When the second panel's button is clicked, it does not react to minimize or maximize. Here is all of my code. Why doesn't it work?
const
BoarderSize = 20;
type
TButtonPosition = (topleft, topright, buttomleft, buttomright);
///
/// a panel with a smaller panel inside and a button on the side
///
THidePanel = class(TPanel)
private
{ Private-Deklarationen }
///
/// a smaller working panel
WorkingPanel: TPanel;
FLargeHight: Integer;
FLargeWidth: Integer;
FActivateButton: TButton;
FExpandState: Boolean;
FButtonPosition: TButtonPosition;
FOnActivateBtnClick: TNotifyEvent;
procedure SetButtonPosition(const Value: TButtonPosition);
protected
{ Protected-Deklarationen }
public
{ Public-Deklarationen }
constructor create(aOwner: TComponent); override;
procedure WMSize(var Msg: TWMSize); message WM_SIZE;
procedure HideComponents;
procedure H_ActivateButtonClick(Sender: TObject);
procedure SetState(astate: Boolean);
procedure free;
destructor destroy; override;
published
{ Published-Deklarationen }
property OnActivateButtonClick: TNotifyEvent read FOnActivateBtnClick
write FOnActivateBtnClick;
property ButtonPosition: TButtonPosition read FButtonPosition
write SetButtonPosition;
end;
procedure Register;
implementation
procedure Register;
begin
RegisterComponents('Samples', [THidePanel]);
end;
{ THidePanel }
constructor THidePanel.create(aOwner: TComponent);
begin
inherited;
WorkingPanel := TPanel.create(self);
WorkingPanel.Caption := 'V01';
FActivateButton := TButton.create(self);
FActivateButton.Parent := self;
FActivateButton.Caption := '<';
FActivateButton.OnClick := H_ActivateButtonClick;
FActivateButton.Width := BoarderSize;
FActivateButton.Height := BoarderSize;
WorkingPanel.Caption := '';
FLargeWidth := self.Width;
SetButtonPosition(topright);
end;
destructor THidePanel.destroy;
begin
inherited;
end;
procedure THidePanel.free;
begin
inherited;
WorkingPanel.free;
FActivateButton.free;
end;
procedure THidePanel.HideComponents;
var
i: Integer;
begin
for i := 0 to WorkingPanel.ControlCount - 1 do
WorkingPanel.Controls[i].Visible := False;
end;
procedure THidePanel.WMSize(var Msg: TWMSize);
begin
/// set inner panel size
WorkingPanel.Top := self.Top + BoarderSize;
WorkingPanel.Left := self.Left + BoarderSize;
WorkingPanel.Width := self.Width - 2 * BoarderSize;
WorkingPanel.Height := self.Height - 2 * BoarderSize;
/// move button
SetButtonPosition(FButtonPosition);
end;
procedure THidePanel.H_ActivateButtonClick(Sender: TObject);
begin
/// button is clicked!
///
FExpandState := not FExpandState;
SetState( FExpandState );
///
if (Assigned(FOnActivateBtnClick)) then
FOnActivateBtnClick(self);
end;
procedure THidePanel.SetButtonPosition(const Value: TButtonPosition);
begin
FButtonPosition := Value;
case FButtonPosition of
topleft:
begin
FActivateButton.Left := 0;
FActivateButton.Top := 0;
end;
topright:
begin
FActivateButton.Left := self.Width - BoarderSize;
FActivateButton.Top := 0;
end;
buttomleft:
begin
FActivateButton.Left := 0;
FActivateButton.Top := self.ClientWidth - BoarderSize;
end;
buttomright:
begin
FActivateButton.Top := self.ClientWidth - BoarderSize;
FActivateButton.Left := self.Width - BoarderSize;
end;
else
/// never go here
end;
end;
procedure THidePanel.SetState(astate: Boolean);
begin
if astate then
begin
/// ...
FActivateButton.Caption := '>';
self.Width := BoarderSize;
end
else
begin
/// ...
FActivateButton.Caption := '<';
self.Width := FLargeWidth;
end;
end;
When Control's Anchors set to alClient, you can not change the size . Set second panel align to alLeft or alRight . if you want fill form with this control, set AutoSize of form True or manually set max size of your control on resize it .
Like MohsenB already explained (+1ed), you cannot change the size of a control with Align = alClient. But since you are making this a component, I would choose to change the Align setting of the component temporarily, instead of dealing with this in the designer code: i.e. make it a feature of the component to be able to set its Align property to alClient and let it behave accordingly when situation requires.
I think you are looking for the following enhancements:
unit Unit2;
interface
uses
Messages, Classes, Controls, StdCtrls, ExtCtrls;
const
BorderSize = 20;
type
TButtonPosition = (bpTopLeft, bpTopRight, bpBottomLeft, bpBottomRight);
THidePanel = class(TPanel)
private
FActivateButton: TButton;
FButtonPosition: TButtonPosition;
FExpandState: Boolean;
FOldAlign: TAlign;
FOldWidth: Integer;
FOnActivateBtnClick: TNotifyEvent;
FWorkingPanel: TPanel;
procedure ActivateButtonClick(Sender: TObject);
procedure SetButtonPosition(Value: TButtonPosition);
protected
function CanResize(var NewWidth, NewHeight: Integer): Boolean; override;
procedure Resize; override;
public
constructor Create(AOwner: TComponent); override;
procedure SetState(AState: Boolean);
published
property ButtonPosition: TButtonPosition read FButtonPosition
write SetButtonPosition default bpTopRight;
property OnActivateButtonClick: TNotifyEvent read FOnActivateBtnClick
write FOnActivateBtnClick;
end;
procedure Register;
implementation
procedure Register;
begin
RegisterComponents('Samples', [THidePanel]);
end;
{ THidePanel }
constructor THidePanel.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
FWorkingPanel := TPanel.Create(Self);
FWorkingPanel.Caption := '';
FWorkingPanel.SetBounds(BorderSize, BorderSize, Width - 2 * BorderSize,
Height - 2 * BorderSize);
FWorkingPanel.Anchors := [akLeft, akTop, akRight, akBottom];
FWorkingPanel.Parent := Self;
FActivateButton := TButton.Create(Self);
FActivateButton.Caption := '<';
FActivateButton.OnClick := ActivateButtonClick;
FActivateButton.Width := BorderSize;
FActivateButton.Height := BorderSize;
FActivateButton.Parent := Self;
SetButtonPosition(bpTopRight);
end;
procedure THidePanel.ActivateButtonClick(Sender: TObject);
begin
FExpandState := not FExpandState;
SetState(FExpandState);
if Assigned(FOnActivateBtnClick) then
FOnActivateBtnClick(Self);
end;
procedure THidePanel.SetButtonPosition(Value: TButtonPosition);
begin
if FButtonPosition <> Value then
begin
FButtonPosition := Value;
case FButtonPosition of
bpTopLeft:
begin
FActivateButton.Left := 0;
FActivateButton.Top := 0;
FActivateButton.Anchors := [akLeft, akTop];
end;
bpTopRight:
begin
FActivateButton.Left := Width - BorderSize;
FActivateButton.Top := 0;
FActivateButton.Anchors := [akRight, akTop];
end;
bpBottomLeft:
begin
FActivateButton.Left := 0;
FActivateButton.Top := ClientWidth - BorderSize;
FActivateButton.Anchors := [akLeft, akBottom];
end;
bpBottomRight:
begin
FActivateButton.Top := ClientWidth - BorderSize;
FActivateButton.Left := Width - BorderSize;
FActivateButton.Anchors := [akRight, akBottom];
end;
end;
end;
end;
procedure THidePanel.SetState(AState: Boolean);
begin
if AState then
begin
FActivateButton.Caption := '>';
FOldAlign := Align;
if FOldAlign = alClient then
Align := alLeft;
Width := BorderSize;
end
else
begin
FActivateButton.Caption := '<';
if FOldAlign = alClient then
Align := FOldAlign
else
Width := FOldWidth;
end;
end;
procedure THidePanel.Resize;
begin
if not FExpandState then
FOldWidth := Width;
inherited Resize;
end;
function THidePanel.CanResize(var NewWidth, NewHeight: Integer): Boolean;
begin
Result := inherited CanResize(NewWidth, NewHeight);
if FExpandState then
NewWidth := BorderSize;
end;
end.
Testing code:
unit Unit1;
interface
uses
Controls, Forms, Unit2, ExtCtrls;
type
TForm1 = class(TForm)
procedure FormClick(Sender: TObject);
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
procedure TForm1.FormClick(Sender: TObject);
begin
with THidePanel.Create(Self) do
begin
Align := alLeft;
Parent := Self;
end;
with TSplitter.Create(Self) do
begin
Left := 200;
Parent := Self;
end;
with THidePanel.Create(Self) do
begin
Align := alClient;
Parent := Self;
end;
end;
end.

Resources