In this code :
unit MSEC;
interface
uses
Winapi.Windows, Vcl.Dialogs, Vcl.ExtCtrls, System.SysUtils, System.Classes, Vcl.Controls, Vcl.StdCtrls;
type
TMSEC = class(TWinControl)
private
FOpr :TComboBox;
public
constructor Create(AOwner: TComponent); override;
end;
implementation
const
DEF_OPERATIONS :array[0..3] of Char = ('+', '-', '*', '/');
constructor TMSEC.Create(AOwner: TComponent);
var i :Integer;
begin
inherited;
FOpr:= TComboBox.Create(Self);
with FOpr do begin
Parent:= Self;
Align:= alLeft;
Width:= DEF_OPERATIONS_WIDTH;
Style:= csDropDownList;
//error in next lines :
Items.Clear;
for i := Low(DEF_OPERATIONS) to High(DEF_OPERATIONS) do Items.Add(DEF_OPERATIONS[i]);
ItemIndex:= 0;
end;
end;
end.
When I change ComboBox items, the program breaks with the message :
'Control' has no parent.
How can I fix this error or initialize ComboBox items in another way?
TComboBox requires an allocated HWND in order to store strings in its Items property. In order for TComboBox to get an HWND, its Parent needs an HWND first, and its Parent needs an HWND, and so on. The problem is that your TMSEC object does not have a Parent assigned yet when its constructor runs, so it is not possible for the TComboBox to get an HWND, hense the error.
Try this instead:
type
TMSEC = class(TWinControl)
private
FOpr: TComboBox;
protected
procedure CreateWnd; override;
public
constructor Create(AOwner: TComponent); override;
end;
constructor TMSEC.Create(AOwner: TComponent);
begin
inherited;
FOpr := TComboBox.Create(Self);
with FOpr do begin
Parent := Self;
Align := alLeft;
Width := DEF_OPERATIONS_WIDTH;
Style := csDropDownList;
Tag := 1;
end;
end;
procedure TMSEC.CreateWnd;
var
i :Integer;
begin
inherited;
if FOpr.Tag = 1 then
begin
FOpr.Tag := 0;
for i := Low(DEF_OPERATIONS) to High(DEF_OPERATIONS) do
FOpr.Items.Add(DEF_OPERATIONS[i]);
FOpr.ItemIndex := 0;
end;
end;
Remy explained the problem well, but for a more general solution, you could create a descendant of TComboBox, for example:
type
TComboBoxSafe = class(TComboBox)
strict private
FSafeItemIndex: Integer;
FSafeItems: TArray<string>;
function GetSafeItemIndex: Integer;
function GetSafeItems: TArray<string>;
procedure SetSafeItemIndex(const AValue: Integer);
procedure SetSafeItems(const AValue: TArray<string>);
strict protected
procedure CreateWnd; override;
procedure DestroyWnd; override;
public
constructor Create(AOwner: TComponent); override;
property SafeItemIndex: Integer read GetSafeItemIndex write SetSafeItemIndex;
property SafeItems: TArray<string> read GetSafeItems write SetSafeItems;
end;
{ TComboBoxSafe }
constructor TComboBoxSafe.Create(AOwner: TComponent);
begin
inherited;
FSafeItemIndex := -1;
end;
procedure TComboBoxSafe.CreateWnd;
var
LOnChange: TNotifyEvent;
begin
inherited;
LOnChange := OnChange;
OnChange := nil;
try
Items.Text := string.Join(sLineBreak, FSafeItems);
ItemIndex := FSafeItemIndex;
finally
OnChange := LOnChange;
end;
end;
procedure TComboBoxSafe.DestroyWnd;
begin
FSafeItemIndex := ItemIndex;
FSafeItems := Items.ToStringArray;
inherited;
end;
function TComboBoxSafe.GetSafeItemIndex: Integer;
begin
if WindowHandle <> 0 then
Result := ItemIndex
else
Result := FSafeItemIndex;
end;
function TComboBoxSafe.GetSafeItems: TArray<string>;
begin
if WindowHandle <> 0 then
Result := Items.ToStringArray
else
Result := FSafeItems;
end;
procedure TComboBoxSafe.SetSafeItemIndex(const AValue: Integer);
begin
if WindowHandle <> 0 then
ItemIndex := AValue
else
FSafeItemIndex := AValue;
end;
procedure TComboBoxSafe.SetSafeItems(const AValue: TArray<string>);
begin
if WindowHandle <> 0 then
Items.Text := string.Join(sLineBreak, AValue)
else
FSafeItems := AValue;
end;
Related
I created a unit with my own class to manage my application settings. One of the members of the class is of type TIniFile which should be used to store the settings in the ini file.
In the constructor of the class, I call TIniFile.Create(SettingsFile) but it raises an exception (I tested it with try...except).
Why does TIniFile creation fail?
Here is my code:
unit SettingsUnit;
interface
{$M+}
uses
System.Classes, System.IniFiles, System.SysUtils;
type
TSettingsManager = class(TObject)
private
FSettingsFile: TIniFile;
FSettingsFileName: String;
FSize: Byte;
FBatteryTop: Integer;
FBatteryLeft: Integer;
FTransparentValue: Byte;
FMinBatteryPercent: Byte;
FShowPercentLabel: Boolean;
FShowPercentOnACPower: Boolean;
FStartWithWindows: Boolean;
public
constructor Create(const SettingsFile: String);
destructor Destroy; override;
procedure StartWithWindows(Value: Boolean);
procedure LoadSettings;
procedure RestoreDefaults;
procedure Apply;
published
property Size: Byte read FSize write FSize;
property BatteryTop: Integer read FBatteryTop write FBatteryTop;
property BatteryLeft: Integer read FBatteryLeft write FBatteryLeft;
property Transparent: Byte read FTransparentValue write FTransparentValue;
property MinBatteryPercent: Byte read FMinBatteryPercent write FMinBatteryPercent;
property ShowPercentLabel: Boolean read FShowPercentLabel write FShowPercentLabel;
property ShowPercentOnACPower: Boolean read FShowPercentOnACPower write FShowPercentOnACPower;
property StartingWithWindows: Boolean read FStartWithWindows;
end;
var
Settings: TSettingsManager;
implementation
uses
System.Win.Registry;
const
SettingsSection: String = 'BatteryFormSettings';
constructor TSettingsManager.Create(const SettingsFile: String);
begin
try
FSettingsFile := TIniFile.Create(SettingsFile);
except
RaiseLastOSError;
Exit;
end;
FSettingsFileName := SettingsFile;
LoadSettings;
inherited Create;
end;
procedure TSettingsManager.LoadSettings;
begin
with FSettingsFile do
begin
FSize := ReadInteger('BatteryFormSettings', 'FormSize', 1);
FBatteryTop := ReadInteger(SettingsSection, 'Top', 20);
FBatteryLeft := ReadInteger(SettingsSection, 'Left', 20);
FTransparentValue := ReadInteger(SettingsSection, 'TransparentValue', 255);
FMinBatteryPercent := ReadInteger(SettingsSection, 'MinBatteryPercent', 80);
FShowPercentLabel := ReadBool(SettingsSection, 'ShowPercentLabel', True);
FShowPercentOnACPower := ReadBool(SettingsSection, 'ShowPercentOnACPower', True);
FStartWithWindows := ReadBool(SettingsSection, 'StartWithWindows', False);
end;
end;
procedure TSettingsManager.Apply;
begin
with FSettingsFile do
begin
WriteInteger(SettingsSection, 'FormSize', FSize);
WriteInteger(SettingsSection, 'Top', FBatteryTop);
WriteInteger(SettingsSection, 'Left', FBatteryLeft);
WriteInteger(SettingsSection, 'TransparentValue', FTransparentValue);
WriteInteger(SettingsSection, 'MinBatteryPercent', FMinBatteryPercent);
WriteBool(SettingsSection, 'ShowPercentLabel', FShowPercentLabel);
WriteBool(SettingsSection, 'ShowPercentOnACPower', FShowPercentOnACPower);
end;
end;
procedure TSettingsManager.StartWithWindows(Value: Boolean);
const
KEY_SET_VALUE = $0002;
var
WinReg: TRegistry;
begin
try
WinReg := TRegistry.Create(KEY_SET_VALUE);
WinReg.OpenKey('Software\Microsoft\Windows\CurrentVersion\Run', True);
if Value then
begin
if not WinReg.KeyExists('BatteryScreen') then
WinReg.WriteString('BatteryScreen', ChangeFileExt(FSettingsFileName, '.exe'));
FStartWithWindows := True;
end
else
begin
if WinReg.KeyExists('BatteryScreen') then
WinReg.DeleteKey('BatteryScreen');
FStartWithWindows := False;
end;
finally
WinReg.CloseKey;
WinReg.Free;
end;
end;
procedure TSettingsManager.RestoreDefaults;
begin
Size := 1;
BatteryTop := 20;
BatteryLeft := 20;
Transparent := 255;
MinBatteryPercent := 80;
ShowPercentLabel := True;
ShowPercentOnACPower := True;
StartWithWindows(False);
end;
destructor TSettingsManager.Destroy;
begin
FSettingsFile.Free;
inherited Destroy;
end;
end.
And I call him in my form unit like this:
implementation
uses SettingsUnit;
...
procedure TMyForm.FormCreate(Sender: TObject);
begin
Settings.Create(ChangeFileExt(Application.ExeName, '.ini'));
...
end;
I have an application where an invisible "Host" application object creates main form and main form creates temporarily a data monitoring dialog form.
There is an asynchronous data receiver in "Host" that has a trace output event. This event should be temporarily bound with data monitoring dialog form's method when dialog form exists and unbound when it is about to be destroyed.
I made a minimal equivalent to this application below. Could you check whether it is the right way to do so? Please pay attention to "Attention" comments.
program BindToTempObject;
uses
Vcl.Forms, System.Classes, Vcl.StdCtrls, Vcl.ExtCtrls, System.SysUtils;
type
TBoolEvent = procedure(b: Boolean) of object;
TDialogForm = class(TForm)
public
constructor Create(AOwner: TComponent); override;
procedure BoolEventHandler(b: Boolean);
end;
TMainForm = class(TForm)
private
_btn: TButton;
_onBoolEventRelay: TBoolEvent; //Attention
procedure _btnClick(Sender: TObject);
public
constructor Create(AOwner: TComponent); override;
procedure BoolEventRelay(b: Boolean); //Attention
end;
TAppObject = class
private
_mainForm: TMainForm;
_eventSource: TTimer;
_boolState: Boolean;
procedure _eventSourceTick(Sender: TObject);
public
OnBoolEvent: TBoolEvent;
constructor Create();
destructor Destroy(); override;
end;
var
app: TAppObject;
{ TAppObject }
constructor TAppObject.Create();
begin
Application.CreateForm(TMainForm, _mainForm);
OnBoolEvent := _mainForm.BoolEventRelay; //Attention
_eventSource := TTimer.Create(nil);
_eventSource.OnTimer := _eventSourceTick;
_eventSource.Enabled := True;
end;
destructor TAppObject.Destroy();
begin
_eventSource.OnTimer := nil;
_eventSource.Free();
inherited;
end;
procedure TAppObject._eventSourceTick(Sender: TObject);
begin
_boolState := not _boolState;
if Assigned(OnBoolEvent) then OnBoolEvent(_boolState);
end;
{ TMainForm }
constructor TMainForm.Create(AOwner: TComponent);
begin
inherited CreateNew(AOwner);
Caption := 'Main form';
_btn := TButton.Create(Self);
_btn.Parent := Self;
_btn.Caption := 'Click me';
_btn.OnClick := _btnClick;
end;
procedure TMainForm.BoolEventRelay(b: Boolean);
begin
if Assigned(_onBoolEventRelay) then _onBoolEventRelay(b); //Attention
end;
procedure TMainForm._btnClick(Sender: TObject);
var
dlg: TDialogForm;
begin
dlg := TDialogForm.Create(Self);
try
_onBoolEventRelay := dlg.BoolEventHandler; //Attention
dlg.ShowModal();
finally
_onBoolEventRelay := nil; //Attention
dlg.Free();
end;
end;
{ TDialogForm }
procedure TDialogForm.BoolEventHandler(b: Boolean);
begin
Caption := BoolToStr(b, True);
end;
constructor TDialogForm.Create(AOwner: TComponent);
begin
inherited CreateNew(AOwner);
Caption := 'Dialog form';
end;
begin
Application.Initialize();
app := TAppObject.Create();
try
Application.Run();
finally
app.Free();
end;
end.
You could do it that way, sure. A decent separation of responsibilities between classes, so they don't have to know about each other.
However, in your particular example, since everything is in a single unit, and the app object is globally accessible, you could simplifly the code a little bit by assigning the TDialogForm.BoolEventHandler() method directly to the TAppObject.OnBoolEvent event and get rid of TMainForm as a middle man:
program BindToTempObject;
uses
Vcl.Forms, System.Classes, Vcl.StdCtrls, Vcl.ExtCtrls, System.SysUtils;
type
TBoolEvent = procedure(b: Boolean) of object;
TDialogForm = class(TForm)
public
constructor Create(AOwner: TComponent); override;
destructor Destroy(); override;
procedure BoolEventHandler(b: Boolean);
end;
TMainForm = class(TForm)
private
_btn: TButton;
procedure _btnClick(Sender: TObject);
public
constructor Create(AOwner: TComponent); override;
end;
TAppObject = class
private
_mainForm: TMainForm;
_eventSource: TTimer;
_boolState: Boolean;
procedure _eventSourceTick(Sender: TObject);
public
OnBoolEvent: TBoolEvent;
constructor Create();
destructor Destroy(); override;
end;
var
app: TAppObject;
{ TAppObject }
constructor TAppObject.Create();
begin
Application.CreateForm(TMainForm, _mainForm);
_eventSource := TTimer.Create(nil);
_eventSource.OnTimer := _eventSourceTick;
_eventSource.Enabled := True;
end;
destructor TAppObject.Destroy();
begin
_eventSource.OnTimer := nil;
_eventSource.Free();
inherited;
end;
procedure TAppObject._eventSourceTick(Sender: TObject);
begin
_boolState := not _boolState;
if Assigned(OnBoolEvent) then OnBoolEvent(_boolState);
end;
{ TMainForm }
constructor TMainForm.Create(AOwner: TComponent);
begin
inherited CreateNew(AOwner);
Caption := 'Main form';
_btn := TButton.Create(Self);
_btn.Parent := Self;
_btn.Caption := 'Click me';
_btn.OnClick := _btnClick;
end;
procedure TMainForm._btnClick(Sender: TObject);
var
dlg: TDialogForm;
begin
dlg := TDialogForm.Create(Self);
try
dlg.ShowModal();
finally
dlg.Free();
end;
end;
{ TDialogForm }
procedure TDialogForm.BoolEventHandler(b: Boolean);
begin
Caption := BoolToStr(b, True);
end;
constructor TDialogForm.Create(AOwner: TComponent);
begin
inherited CreateNew(AOwner);
Caption := 'Dialog form';
app.OnBoolEvent := BoolEventHandler;
end;
destructor TDialogForm.Destroy();
begin
app.OnBoolEvent := nil;
inherited;
end;
begin
Application.Initialize();
app := TAppObject.Create();
try
Application.Run();
finally
app.Free();
end;
end.
I'm trying to create a new component named CheckEdit as the following:
unit UnitName;
interface
uses
System.Classes, Vcl.ExtCtrls, Vcl.StdCtrls, Vcl.Controls;
type
TCheckEdit = class (TCustomControl)
private
FCheckBox : TCheckBox;
FEdit : TEdit;
FEnableCaption: TCaption;
FDisbleCaption: TCaption;
procedure SetIsActive(const Value: Boolean);
function GetIsActive : Boolean;
procedure ChBoxOnClick (Sender : TObject);
procedure SetDisbleCaption(const Value: TCaption);
procedure SetEnableCaption(const Value: TCaption);
protected
public
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
published
property IsActive : Boolean read GetIsActive write SetIsActive default False;
property EnableCaption : TCaption read FEnableCaption write SetEnableCaption;
property DisbleCaption : TCaption read FDisbleCaption write SetDisbleCaption;
property OnClick;
end;
procedure register;
implementation
procedure register;
begin
RegisterComponents('Standard', [TCheckEdit]);
end;
{ TCheckEdit }
procedure TCheckEdit.ChBoxOnClick(Sender: TObject);
begin
if FCheckBox.Checked then
IsActive := True
else
IsActive:= False;
end;
constructor TCheckEdit.Create(AOwner: TComponent);
begin
inherited;
FCheckBox := TCheckBox.Create(Self);
FCheckBox.Parent := Self;
FCheckBox.Align := alTop;
FCheckBox.Caption := Self.Name;
FCheckBox.OnClick := ChBoxOnClick;
FDisbleCaption := 'Disabled';
FEnableCaption := 'Enabled';
FCheckBox.Caption := FDisbleCaption;
FEdit := TEdit.Create(Self);
FEdit.Parent := Self;
FEdit.Align := alTop;
FEdit.Enabled := False;
Self.Height := 40;
Self.Width := 185;
Self.AutoSize := True;
end;
destructor TCheckEdit.Destroy;
begin
FEdit.Free;
FCheckBox.Free;
inherited;
end;
function TCheckEdit.GetIsActive: Boolean;
begin
if FCheckBox.Checked then
Result := True
else
Result := False;
end;
procedure TCheckEdit.SetDisbleCaption(const Value: TCaption);
begin
FDisbleCaption := Value;
end;
procedure TCheckEdit.SetEnableCaption(const Value: TCaption);
begin
FEnableCaption := Value;
end;
procedure TCheckEdit.SetIsActive(const Value: Boolean);
begin
FCheckBox.Checked := Value;
case Value of
True :
begin
FEdit.Enabled := True;
FCheckBox.Caption := FEnableCaption;
end;
False :
begin
FEdit.Enabled := False;
FCheckBox.Caption := FDisbleCaption;
end;
end;
end;
end.
Everything is working fine, but I want to make EnableCaption and DisableCaption in one node as TToggleSwitch have StateCaptions property, and when I change the caption it will change it in the CheckBox too.
I try to call Invalidate; in SetEnableCaption and SetDisbleCaption procedures, but that does not work.
How can I do that?
To be honest with you, I did not want to answer this question at first because you already have the answer in one of your questions here on SO
Create a button that accepts .PNG images as Glyph
the first class to be exact it is a TPersistent that exposed the Glyph coordinates in the TNCRSpeedButton.
I'm writing this because I said I hope you benefit well from it. and I can see you did not.
So this is your solution to your problem and you are more than welcome to ask any thing about how it is implemented.
unit UnitName;;
interface
uses
System.Classes, Vcl.ExtCtrls, Vcl.StdCtrls, Vcl.Controls;
type
TCheckEditCaptions = class(TPersistent)
private
FDisableCaption: TCaption;
FEnableCaption: TCaption;
FOnChange: TNotifyEvent;
function GetDisableCaption: TCaption;
function GetEnableCaption: TCaption;
procedure SetDisableCaption(const Value: TCaption);
procedure SetEnableCaption(const Value: TCaption);
public
procedure Assign(aValue: TPersistent); override;
published
property EnableCaption: TCaption read GetEnableCaption write SetEnableCaption;
property DisableCaption: TCaption read GetDisableCaption write SetDisableCaption;
property OnChange: TNotifyEvent read FOnChange write FOnChange;
end;
TCheckEdit = class (TCustomControl)
private
FCheckBox : TCheckBox;
FEdit : TEdit;
FCheckEditCaptions: TCheckEditCaptions;
procedure SetIsActive(const Value: Boolean);
function GetIsActive : Boolean;
procedure ChBoxOnClick (Sender : TObject);
procedure CheckEditCaptionsChanged(Sender : TObject);
procedure SetCheckEditCaptions(const Value: TCheckEditCaptions);
protected
public
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
published
property IsActive : Boolean read GetIsActive write SetIsActive default False;
property CheckEditCaptions : TCheckEditCaptions read FCheckEditCaptions write SetCheckEditCaptions;
property OnClick;
end;
procedure register;
implementation
procedure register;
begin
RegisterComponents('Samples', [TCheckEdit]);
end;
{ TCheckEdit }
procedure TCheckEdit.ChBoxOnClick(Sender: TObject);
begin
IsActive := FCheckBox.Checked;
end;
procedure TCheckEdit.CheckEditCaptionsChanged(Sender: TObject);
begin
SetIsActive(GetIsActive);
end;
constructor TCheckEdit.Create(AOwner: TComponent);
begin
inherited;
FCheckBox := TCheckBox.Create(Self);
FCheckBox.Parent := Self;
FCheckBox.Align := alTop;
FCheckBox.OnClick := ChBoxOnClick;
FCheckEditCaptions := TCheckEditCaptions.Create;
FCheckEditCaptions.FDisableCaption := 'Disabled';
FCheckEditCaptions.FEnableCaption := 'Enabled';
FCheckEditCaptions.OnChange := CheckEditCaptionsChanged;
FCheckBox.Caption := CheckEditCaptions.DisableCaption;
FEdit := TEdit.Create(Self);
FEdit.Parent := Self;
FEdit.Align := alTop;
FEdit.Enabled := False;
Self.Height := 40;
Self.Width := 185;
Self.AutoSize := True;
end;
destructor TCheckEdit.Destroy;
begin
FEdit.Free;
FCheckBox.Free;
FCheckEditCaptions.Free;
inherited;
end;
function TCheckEdit.GetIsActive: Boolean;
begin
Result := FCheckBox.Checked ;
end;
procedure TCheckEdit.SetCheckEditCaptions(const Value: TCheckEditCaptions);
begin
FCheckEditCaptions.Assign(Value);
end;
procedure TCheckEdit.SetIsActive(const Value: Boolean);
begin
FCheckBox.Checked := Value;
FEdit.Enabled := Value;
if Value then
FCheckBox.Caption := CheckEditCaptions.EnableCaption
else
FCheckBox.Caption := CheckEditCaptions.DisableCaption;
end;
{ TCheckEditCaptions }
procedure TCheckEditCaptions.Assign(aValue: TPersistent);
begin
if aValue is TCheckEditCaptions then begin
FEnableCaption := TCheckEditCaptions(aValue).FEnableCaption;
FEnableCaption := TCheckEditCaptions(aValue).FDisableCaption;
if Assigned(FOnChange) then
FOnChange(self);
end else
inherited;
end;
function TCheckEditCaptions.GetDisableCaption: TCaption;
begin
result := FDisableCaption;
end;
function TCheckEditCaptions.GetEnableCaption: TCaption;
begin
result := FEnableCaption;
end;
procedure TCheckEditCaptions.SetDisableCaption(const Value: TCaption);
begin
FDisableCaption := Value;
if Assigned(FOnChange) then
FOnChange(self);
end;
procedure TCheckEditCaptions.SetEnableCaption(const Value: TCaption);
begin
FEnableCaption := Value;
if Assigned(FOnChange) then
FOnChange(self);
end;
end.
Using Delphi XE6
I am trying to create an FMX RadioGroup control using a TGroupBox and TRadioButton. I can see both my TTestRadioGroup and TTestGroupButton controls in my IDE control pallette. I can drop a TTestRadioGroup on my form and set the items property and it will create the Radio buttons. However, when I run the application and call the form with the TTestRadioGroup control with its radio buttons, on it, I get a "cant find TTestGroupButton" message.
What have I done wrong?
My first test seems to work ok, as long as I am in design. When
unit TestComponent;
interface
uses {$IFDEF MSWINDOWS}Windows, {$ENDIF}
System.Classes, FMX.Edit, System.UITypes, System.Character, FMX.DateTimeCtrls,
System.SysUtils, FMX.Types, System.DateUtils, System.SysConst, FMX.Controls,
FMX.Pickers, FMX.Platform, FMX.Text, math, FMX.Consts, FMX.Forms, FMX.StdCtrls;
type
TTestRadioGroup = class;
TTestGroupButton = class(TRadioButton)
private
protected
public
constructor InternalCreate(RadioGroup: TTestRadioGroup);
destructor Destroy; override;
end;
TTestRadioGroup = class(TGroupBox)
private
FReading: Boolean;
FButtons: TList;
FItems: TStrings;
FItemIndex: Integer;
FColumns: Integer;
FUpdating: Boolean;
FButtonMargins: Integer;
fButtonSpacing: Integer;
function GetButtons(Index: Integer): TRadioButton;
procedure SetButtonMargins(Value: Integer);
procedure SetButtonSpacing(Value: Integer);
procedure SetColumns(Value: Integer);
procedure SetItemIndex(Value: Integer);
procedure SetItems(Value: TStrings);
procedure ItemsChange(Sender: TObject);
procedure SetButtonCount(Value: Integer);
procedure ButtonClick(Sender: TObject);
procedure UpdateButtons; //updates buttons list from Items list
procedure ArrangeButtons; //rearranges buttons on Groupbox based on new properties
protected
public
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
property Buttons[Index: Integer]: TRadioButton read GetButtons;
published
property ItemIndex: Integer read FItemIndex write SetItemIndex default -1;
property Items: TStrings read FItems write SetItems;
property Columns: Integer read FColumns write SetColumns default 1;
property ButtonMargins: Integer read fButtonMargins write SetButtonMargins default 0;
property ButtonSpacing: Integer read fButtonSpacing write SetButtonSpacing default 0;
end;
procedure Register;
implementation
procedure Register;
begin
RegisterComponents('Test', [TTestRadioGroup, TTestGroupButton]);
end;
{ TTestGroupButton }
constructor TTestGroupButton.InternalCreate(RadioGroup: TTestRadioGroup);
begin
inherited Create(RadioGroup);
RadioGroup.FButtons.Add(Self);
Visible := False;
Enabled := RadioGroup.Enabled;
OnClick := RadioGroup.ButtonClick;
Parent := RadioGroup;
end;
destructor TTestGroupButton.Destroy;
begin
TTestRadioGroup(Owner).FButtons.Remove(Self);
inherited Destroy;
end;
{ TTestRadioGroup }
constructor TTestRadioGroup.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
FButtons := TList.Create;
FItems := TStringList.Create;
TStringList(FItems).OnChange := ItemsChange;
FItemIndex := -1;
FColumns := 1;
end;
destructor TTestRadioGroup.Destroy;
begin
SetButtonCount(0);
TStringList(FItems).OnChange := nil;
FItems.Free;
FButtons.Free;
inherited Destroy;
end;
procedure TTestRadioGroup.ArrangeButtons;
var
I,Y: Integer ;
begin
if (FButtons.Count <> 0) and not FReading then
begin
try
Y:= 10;
for I := 0 to FButtons.Count - 1 do
with TTestGroupButton(FButtons[I]) do
begin
Position.X:= 10;
Position.Y:= Y;
Y:= Y + 10;
Visible := True;
end;
finally
end;
end;
end;
procedure TTestRadioGroup.UpdateButtons;
var
I: Integer;
begin
SetButtonCount(FItems.Count);
for I := 0 to FButtons.Count - 1 do
TRadioButton(FButtons[I]).Text := FItems[I];
if FItemIndex >= 0 then
begin
FUpdating := True;
TRadioButton(FButtons[FItemIndex]).isChecked := True;
FUpdating := False;
end;
ArrangeButtons;
Repaint;
end;
procedure TTestRadioGroup.ButtonClick(Sender: TObject);
begin
if not FUpdating then
begin
FItemIndex := FButtons.IndexOf(Sender);
Change;
Click;
end;
end;
procedure TTestRadioGroup.ItemsChange(Sender: TObject);
begin
if not FReading then
begin
if FItemIndex >= FItems.Count then
FItemIndex := FItems.Count - 1;
UpdateButtons;
end;
end;
procedure TTestRadioGroup.SetColumns(Value: Integer);
begin
if Value < 1 then Value := 1;
if Value > 16 then Value := 16;
if FColumns <> Value then
begin
FColumns := Value;
ArrangeButtons;
Repaint;
end;
end;
procedure TTestRadioGroup.SetItemIndex(Value: Integer);
begin
if FReading then FItemIndex := Value else
begin
if Value < -1 then Value := -1;
if Value >= FButtons.Count then Value := FButtons.Count - 1;
if FItemIndex <> Value then
begin
if FItemIndex >= 0 then
TRadioButton(FButtons[FItemIndex]).isChecked := False;
FItemIndex := Value;
if FItemIndex >= 0 then
TRadioButton(FButtons[FItemIndex]).isChecked := True;
end;
end;
end;
procedure TTestRadioGroup.SetItems(Value: TStrings);
begin
FItems.Assign(Value);
end;
procedure TTestRadioGroup.SetButtonCount(Value: Integer);
begin
while FButtons.Count < Value do
TTestGroupButton.InternalCreate(Self);
while FButtons.Count > Value do
TTestGroupButton(FButtons.Last).Free;
end;
procedure TTestRadioGroup.SetButtonMargins(Value: Integer);
begin
if fButtonMargins <> Value then
fButtonMargins:= Value;
ArrangeButtons;
end;
procedure TTestRadioGroup.SetButtonSpacing(Value: Integer);
begin
if fButtonSpacing <> Value then
fButtonSpacing:= Value;
ArrangeButtons;
end;
function TTestRadioGroup.GetButtons(Index: Integer): TRadioButton;
begin
Result := TRadioButton(FButtons[Index]);
end;
end.
What I think the first problem is, is that when you run the program it will attempt to load the design state of the objects using a copy of the FMX file. The problem with this is that it expects TTestGroupButton to have a standard Create constructor, which effectively yours does not, so it uses TRadioButton.Create instead, which means that at run time your InternalCreate never gets called.
There is a second problem too, to do with creating buttons on the fly, and indeed it is probably this that causes your first problem.
One way to attempt to address this might be to define an additional create. like this:
TTestGroupButton = class(TRadioButton)
private
protected
public
constructor InternalCreate(RadioGroup: TTestRadioGroup);
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
end;
...
constructor TTestGroupButton.Create(AOwner: TComponent); override;
begin
if AOwner is TTestRadioGroup then
begin
InternalCreate( AOwner as TTestRadioGroup );
end
else
begin
inherited;
end;
end;
However it may be better to address the underlying problem which is that the buttons that you create are created on the fly both at design and at run time, so either do not create them at design time, or make sure that they are not saved at design time like this by setting the Stored value to FALSE.
constructor TTestGroupButton.InternalCreate(RadioGroup: TTestRadioGroup);
begin
inherited Create(RadioGroup);
RadioGroup.FButtons.Add(Self);
Visible := False;
Enabled := RadioGroup.Enabled;
OnClick := RadioGroup.ButtonClick;
Parent := RadioGroup;
Stored := FALSE; //////////// Make sure not saved in FMX file
end;
I am modifying the edit control with autocomplete from here:
Auto append/complete from text file to an edit box delphi
I want to load autocomplete strings from DB. I declared new properties on autocomplete control descendant:
FACDataSource : TDataSource;
FACFieldName : string;
I call this to load autocomplete strings:
procedure TAutoCompleteEdit.ReadSuggestions;
begin
FAutoCompleteSourceList.Clear;
if (not Assigned(FACDataSource)) or (not Assigned(FACDataSource.DataSet)) or (not ACEnabled) then
exit;
with FACDataSource.DataSet do
begin
if Active and (RecordCount > 0) and (FACFieldName <> '') then
begin
First;
while not EOF do
begin
FAutoCompleteSourceList.Add(FACDataSource.DataSet.FieldByName(FACFieldName).AsString);
Next;
end;
if FAutoCompleteSourceList.Count > 0 then
ACStrings := FAutoCompleteSourceList;
end;
end;
end;
However, I get AccessViolation when assigning FAutoCompleteSourceList to ACStrings. The setter for ACStrings is:
procedure TAutoCompleteEdit.SetACStrings(const Value: TStringList);
begin
if Value <> FACList.FStrings then
FACList.FStrings.Assign(Value);
end;
I get AccessViolation in the line: FACList.FStrings.Assign(Value); (READ of address XXXYYY). Value is defined and not garbage at that point (e.g. in I can view the string list in the debugger). 'FStrings' is an empty stringlist.
It works fine when the control is dropped on the form. But doesn't if I place it within a custom inplace editor shown when user enters a DBGridEH cell.
The inplace editor is like this:
unit UInplaceAutoCompleteEditor;
interface
uses UDBAutoComplete, UMyInplaceEditor, classes, windows, Controls, Buttons, DB;
type TInplaceAutoCompleteEditor = class(TMyInplaceEditor)
private
FEditor : TAutoCompleteEdit;
FButton : TSpeedButton;
FShowButton : boolean;
procedure SetShowButton(value : boolean);
public
constructor Create(AOwner : TComponent); override;
procedure SetFocus; override;
destructor Destroy; override;
protected
procedure EditorKeyDown(Sender : TObject; var Key : Word; Shift : TShiftState);
function GetACDataSource : TDataSource;
procedure SetACDataSource(value : TDataSource);
function GetACFieldName : string;
procedure SetACFieldName(value : string);
procedure SetACEnabled(value : boolean);
function GetACEnabled : boolean;
published
property Editor : TAutoCompleteEdit read FEditor;
property ACDataSource : TDataSource read GetACDataSource write SetACDataSource;
property ACFieldName : string read GetACFieldName write SetACFieldName;
property ACEnabled : boolean read GetACEnabled write SetACEnabled;
property Button : TSpeedButton read FButton;
property ShowButton : boolean read FShowButton write SetShowButton;
end;
procedure Register;
implementation
procedure Register;
begin
RegisterComponents('nikolaev', [ TInplaceAutoCompleteEditor ]);
end;
{ TInplaceAutoCompleteEditor }
constructor TInplaceAutoCompleteEditor.Create(AOwner: TComponent);
begin
inherited;
FEditor := TAutoCompleteEdit.Create(self);
FEditor.Parent := self;
FEditor.Align := alClient;
FEditor.Visible := true;
FEditor.WantTabs := true;
FEditor.OnKeyDown := EditorKeyDown;
FButton := TSpeedButton.Create(self);
FButton.Parent := self;
FButton.Align := alRight;
self.FOwnHeight := -1;
self.FOwnWidth := -1;
SetShowButton(false);
end;
destructor TInplaceAutoCompleteEditor.Destroy;
begin
Feditor.Destroy;
FButton.Destroy;
inherited;
end;
procedure TInplaceAutoCompleteEditor.EditorKeyDown(Sender: TObject;
var Key: Word; Shift: TShiftState);
begin
if Key in [ VK_Return, VK_Tab ] then
begin
self.Value := FEditor.Text;
Key := 0;
ConfirmValue;
end;
if Key = VK_Escape then
begin
Key := 0;
CancelValue;
end;
inherited;
end;
function TInplaceAutoCompleteEditor.GetACDataSource: TDataSource;
begin
Result := FEditor.ACDataSource;
end;
function TInplaceAutoCompleteEditor.GetACEnabled: boolean;
begin
Result := FEditor.ACEnabled;
end;
function TInplaceAutoCompleteEditor.GetACFieldName: string;
begin
Result := FEditor.ACFieldName
end;
procedure TInplaceAutoCompleteEditor.SetACDataSource(value: TDataSource);
begin
FEditor.ACDataSource := value;
end;
procedure TInplaceAutoCompleteEditor.SetACEnabled(value: boolean);
begin
FEditor.ACEnabled := value;
end;
procedure TInplaceAutoCompleteEditor.SetACFieldName(value: string);
begin
FEditor.acfieldname := value;
end;
procedure TInplaceAutoCompleteEditor.SetFocus;
begin
inherited;
FEditor.SetFocus;
end;
procedure TInplaceAutoCompleteEditor.SetShowButton(value: boolean);
begin
if value <> FShowButton then
begin
FShowButton := value;
FButton.Visible := value;
end;
end;
end.
This inplace editor inherits from an abstract class like this:
unit UMyInplaceEditor;
interface
uses Windows, classes, types, dbGridEh, ExtCtrls, Controls;
type TMyInplaceEditor = class (TWinControl)
private
FOnValueConfirmed : TNotifyEvent;
FOnCanceled : TNotifyEvent;
FWantTabs : boolean;
procedure AdjustPosition;
protected
FOwnHeight, FOwnWidth : integer;
FValue : Variant;
function GetIsEditing : boolean;
procedure SetIsEditing(value : boolean); virtual;
procedure ConfirmValue;
procedure CancelValue;
procedure SetValue(val : Variant); virtual;
public
property OnValueConfirmed : TNotifyEvent read FOnValueConfirmed write FOnValueConfirmed;
property OnCanceled : TNotifyEvent read FOnCanceled write FOnCanceled;
property Value : Variant read FValue write SetValue;
property IsEditing : boolean read GetIsEditing write SetIsEditing;
procedure SetPosition(parentControl : TWinControl; rect : TRect); virtual;
function ColumnEditable(column : TColumnEH) : boolean; virtual;
constructor Create(AOwner : TComponent); override;
property WantTabs : boolean read FWantTabs write FWantTabs;
end;
procedure Register;
implementation
procedure Register;
begin
RegisterComponents('nikolaev', [TMyInplaceEditor]);
end;
constructor TMyInplaceEditor.Create(AOwner : TComponent);
begin
inherited Create(AOwner);
self.AutoSize := false;
self.Visible := false;
self.FOwnHeight := -1;
self.FOwnWidth := -1;
end;
procedure TMyInplaceEditor.AdjustPosition;
var xOffset, yOffset : integer;
begin
xoffset := self.Left + self.Width - self.Parent.Width;
if xOffset > 0 then
self.Left := self.Left - xOffset;
yOffset := self.Top + self.Height - self.Parent.height;
if yOffset > 0 then
self.Top := self.Top - yOffset;
end;
function TMyInplaceEditor.GetIsEditing : boolean;
begin
Result := self.Visible;
end;
procedure TMyInplaceEditor.SetIsEditing(value: Boolean);
begin
self.Visible := value;
self.BringToFront;
{if Visible then
self.SetFocus;}
end;
procedure TMyInplaceEditor.SetPosition(parentControl : TWinControl; rect: TRect);
begin
self.Parent := parentControl;
self.Top := rect.Top;//parentControl.Top;
self.Left := rect.Left;//parentControl.left;
if self.FOwnWidth = -1 then
self.Width := rect.Right - rect.Left
else
self.Width := self.FOwnWidth;
if self.FOwnHeight = -1 then
self.Height := rect.Bottom - rect.Top
else
self.Height := self.FOwnHeight;
AdjustPosition;
end;
function TMyInplaceEditor.ColumnEditable(column : TColumnEH) : boolean;
begin
Result := true;
end;
procedure TMyInplaceEditor.ConfirmValue;
begin
if Assigned(FOnValueConfirmed) then
FOnValueConfirmed(self);
end;
procedure TMyInplaceEditor.CancelValue;
begin
if Assigned(FOnCanceled) then
FOnCanceled(self);
end;
procedure TMyInplaceEditor.SetValue(val : Variant);
begin
FValue := val;
end;
end.
The InplaceEditor is used in a descendant from DBGridEH. I override ShowEditor and HideEditor to show / hide my editor in certain cases.
Again, the autocomplete control only throws exception when embedded in the inplaceeditor control.
What causes access violation?
The problem is that the code you are using mis-handles interface reference counting. Here are the relevant extracts:
type
TEnumString = class(TInterfacedObject, IEnumString)
....
Note that this class is derived from TInterfacedObject and so it manages its lifetime using reference counting.
Then the code goes on like this:
type
TAutoCompleteEdit = class(TEdit)
private
FACList: TEnumString;
....
So we are going to hold a reference to the object rather than the interface. That looks dubious already.
Then we do this:
constructor TAutoCompleteEdit.Create(AOwner: TComponent);
begin
inherited;
FACList := TEnumString.Create;
....
end;
destructor TAutoCompleteEdit.Destroy;
begin
FACList := nil;
inherited;
end;
There's nothing here to keep the object alive. At other points in the code we take a reference to the IEnumString interface. But then as soon as that reference is released, the object thinks that there are no references left. And so it is deleted. Then, later on, the code refers to FACList which now points at an object that has been destroyed.
A simple way to fix this would be to make sure that the TAutoCompleteEdit control always holds a reference to the interface:
type
TAutoCompleteEdit = class(TEdit)
private
FACList: TEnumString;
FEnumString: IEnumString;
....
constructor TAutoCompleteEdit.Create(AOwner: TComponent);
begin
inherited;
FACList := TEnumString.Create;
FEnumString := FACList;
....
end;
And with this change you can then remove the destructor for TAutoCompleteEdit since the object behind FEnumString will get destroyed by the reference counting mechanism.
Another way to fix this would be to change TEnumString to disable automatic reference counting. That would look like this:
type
TEnumString = class(TObject, IInterface, IEnumString)
private
function QueryInterface(const IID: TGUID; out Obj): HResult; stdcall;
function _AddRef: Integer; stdcall;
function _Release: Integer; stdcall;
....
end;
function TEnumString.QueryInterface(const IID: TGUID; out Obj): HResult;
begin
if GetInterface(IID, Obj) then
Result := 0
else
Result := E_NOINTERFACE;
end;
function TEnumString._AddRef: Integer;
begin
Result := -1;
end;
function TEnumString._Release: Integer;
begin
Result := -1;
end;
And then you'd need the TAutoCompleteEdit destructor to look like this:
destructor TAutoCompleteEdit.Destroy;
begin
FACList.Free;
inherited;
end;
And a final option would be to avoid holding a TEnumString at all and instead only hold an IEnumString reference. Let the reference counting manage lifetime as in the first solution. But then you'd need to implement another interface that allowed the TAutoCompleteEdit to obtain the TStrings object.