TStrings vs TStringList in TCheckListBox - delphi

In Delphi 7, I'm using a TCheckListBox. I want it to use a TStringList rather than a TStrings, so I can set Duplicates to dupIgnore, and Sorted to TRUE.
Can I just do this:
Form1 = class(TObject
CheckListBox1: TCheckListBox; // created by the IDE
end;
procedure TForm1.FormCreate
begin
CheckListBox1.Items.Free;
CheckListBox1.Items := TStringList.Create;
CheckListBox1.Items.Sorted := TRUE;
CheckListBox1.Items.Duplicates := dupIgnore;
end;
Is this safe? Any caveats or suggestions?
EDIT: Removed declaration for MyStringList and added .Items to the last two assignment lines.
EDIT 2: Trying to compile the above, it looks like I'd have to cast the two final lines like this:
TStringList(CheckListBox1.Items).Sorted := TRUE;
TStringList(CheckListBox1.Items).Duplicates := dupIgnore;
Although I might be able to get this to run, I'm asking the question because just getting it to run doesn't mean it will always run or is safe.

You don't control what class TCheckListBox uses to store its items. Assigning the Items property a value only assigns its items to the internal storage.
Also, you shouldn't call Items.Free;. TCheckListBox depends on its internal instance of TListBoxStrings.
To answer your edits in your question: Don't hard-cast the Items property to TStringList, either. The typecast is wrong (the instance exposed by Items is not a TStringList) and will only cause problems.
Edit, to suggest a workaround for what you seem to try to achieve: To keep the checklistbox sorted, you can set its Sorted property to True. To avoid duplicates, you can check the list before adding an item in code.

Related

Memory Leaks with TList<T>.Pack

Consider a simple class TMyObject that contains an array[0..9] of string (Description) and some other properties (omitted for this example). At a certain point, I want to remove all "empty" objects (i.e. where all 10 string are empty) from FMyList (TList<TMyObject>). For this purpose I am using the Pack method with the IsEmpty function:
FMyList.Pack(
function(const MyObject1, MyObject2: TMyObject): boolean
var
ii: Integer;
begin
result := true;
for ii := 0 to 9 do
if (MyObject1.Description[ii] <> '') then
begin
result := false;
break;
end;
end);
Initially I (wrongfully) assumed Pack would also free (release) the objects. This is not the case, so the above code causes memory leaks, leaving the removed TMyObjects "dangling".
Unfortunately, the documentation is a bit sparse on Pack. How to correctly use this method and ensure that after a Pack, not only are the objects removed from the TList but also correctly freed?
Pack is specifically to remove empty items from the list with empty being related to the data type. That is why it does not trigger any OnChange event as it technically does not remove items but just empty slots in the backing array.
The overload with IsEmpty func is not meant for "remove all items from the list where this and that applies".
That being said for your task using Pack is not the solution. You will have to use a backwards loop and Delete. When using a TObjectList<T> with OwnsObjects = True it will automatically destroy the object. Otherwise you need to do that yourself.
Edit: Just to mention this for completeness sake - but I advise against doing this: you can still .Free the item inside the IsEmpty func you provide when you return True. But imho that code is kinda smelly.

Insert existing TLabels into a dynamically created array of TLabels

I'm trying to create a dynamic array of TLabels, then insert already existing TLabels created in the IDE into it so I can then use the array in the code.
My purpose is to use this method for several similar processes.
I would like to make it by using loops.
I saw this thread and it was very helpful to understand about creating arrays of TLabels and populating them with TLabels for the desired purposes, but I couldnt find the particular solution for the case when the labels are already created.
Use variables for object name in Delphi
Basically, what I'm trying to automate is this:
var
LabelArray : array of TLabel;
SetLength(LabelArray, 17);
LabelArray[0] := M2;
LabelArray[1] := M3;
.
.
LabelArray[16] := M18
M2 to M18 are 17 labels that are the TLabels already created and positioned on the Form.
You can use the form's FindComponent method to add the labels to your array.
The code below depends on your having declared LabelArray as a private variable at the form level, so that it exists and is visible when the form is created and the code executes. It also hard-codes in 17 as the number of labels, so if you delete or rename a label you'll have an empty spot in the array - I did not include any error checking to make sure a label is found before putting it into the array and the element could end up nil if a label is missing.
procedure TForm1.FormCreate(Sender: TObject);
var
i: Integer;
begin
SetLength(LabelArray, 17);
{
I use Low() and High() here to avoid having multiple places where
you would have to change the code if you end up adding or removing
labels in the future.
Also note that the index into the array starts at 0, so the code that
calls Format() adjusts that index by 2 to start at M2 instead of M0.
}
for i := Low(LabelArray) to High(LabelArray) do
LabelArray[i] := TLabel(FindComponent(Format('M%d', [i + 2])));
end;
If your labels cannot be found using a common name scheme, but you simply want to get all labels on the same panel / group box etc. into the array, you can use the Controls[] property of the parent control:
SetLength(LabelArray, ParentCtrl.ControlCount);
cnt := 0;
for i := 0 to ParentCtrl.ControlCount - 1 do begin
if ParentCtrl.Controls[i] is TLabel then begin
LabelArray[cnt] := TLabel(ParentCtrl.Controls[i]);
Inc(cnt);
end;
end;
SetLength(LabelArray, cnt);
ParentCtrl is the panel / groupbox or even the form itself that is the common parent of all the labels you are interested in.
If you simply want all labels on a form, even if they are located on other controls, you can use the Components[] property of the form in the same way.

What has happened to ComboBox.Sorted := True; in Delphi 10.2?

Having recently received a 'Tumbleweed' badge for my last question, I am not sure whether I should be asking any more questions, but here goes.
I am populating a TComboBox with items from a sqlite table and this works fine. In my previous version of Delphi I was able to use ComboBox1.Sorted := True; to sort the items, but this seems to have disappeared in Delphi 10.2. I can sort the items in the table by applying a query and then populate the TComboBox from the sorted table. However, for curiosities sake I would like to find out how one now sorts items in a TComboBox. I have found some references to TComboBox(Sort:Compare) but have not succeeded in getting this to work to as of yet.
Can somebody please shed some light on this - many thanks
In Firemonkey you can populate a TComboBox instance either simply with the Items property of type TStrings or you add TListBoxItem instances with the form designer. But internally always TListBoxItem for the elements is used.
To use the TComboBox.Sort you need to provide an anonymous compare-function.
This is a simple example usage of TComboBox.Sort
cbxItems.Sort(
function (pLeft, pRight: TFMXObject): Integer
var
lLeft, lRight: TListBoxItem;
begin
lLeft := TListBoxItem(pLeft);
lRight := TListBoxItem(pRight);
Result := String.Compare(lLeft.Text, lRight.Text);
end
);

Delphi. How to Disable/Enable controls without triggering controls events

I have a DataSet (TZQuery), which has several boolean fields, that have TDBCheckBoxes assigned to them.
These CheckBoxes have "OnClick" events assigned to them and they are triggered whenever I change field values (which are assigned to checkboxes).
The problem is that I do not need these events triggerred, during many operations i do with the dataset.
I've tried calling DataSet.DisableControls, but then events are called right after i call DataSet.EnableControls.
So my question is - is there a way to disable triggering Data-aware controls events.
Edit (bigger picture):
If an exception happens while let's say saving data, i have to load the default values (or the values i've had before saving it). Now while loading that data, all these events (TDBCheckBoxes and other data-aware controls) are triggered, which do all sorts of operations which create lag and sometimes even unwanted changes of data, i'm looking for an universal solution of disabling them all for a short period of time.
Building on Guillem's post:
Turn off everything:
Traverse each component on the form with the for-loop, shown below, changing the properties to the desired value.
If you want to later revert back to the original property values, then you must save the original value (as OldEvent is used below.)
Edit: The code below shows the key concept being discussed. If components are being added or deleted at run-time, or if you'd like to use the absolutely least amount of memory, then use a dynamic array, and as Pieter suggests, store pointers to the components rather than indexing to them.
const
MAX_COMPONENTS_ON_PAGE = 100; // arbitrarily larger than what you'd expect. (Use a dynamic array if this worries you.
var
OldEvent: Array[0.. MAX_COMPONENTS_ON_PAGE - 1] of TNotifyEvent; // save original values here
i: Integer;
begin
for i := 0 to ComponentCount - 1 do
begin
if (Components[i] is TCheckBox) then
begin
OldEvent[i] := TCheckBox(Components[i]).OnClick; // remember old state
TCheckBox(Components[i]).OnClick := nil;
end
else if (Components[i] is TEdit) then
begin
OldEvent[i] := TEdit(Components[i]).OnClick; // remember old state
TEdit(Components[i]).OnClick := nil;
end;
end;
Revert to former values
for i := 0 to ComponentCount - 1 do
begin
if (Components[i] is TCheckBox) then
TCheckBox(Components[i]).OnClick := OldEvent[i]
else if (Components[i] is TEdit) then
TEdit(Components[i]).OnClick := OldEvent[i];
end;
There may be a way to fold all of the if-statements into one generic test that answers "Does this component have an OnClickEvent" -- but I don't know what it is.
Hopefully someone will constructively criticize my answer (rather than just down voting it.) But, hopefully what I've shown above will be workable.
One way to do this is following:
var
Event : TNotifyEvent;
begin
Event := myCheckbox.OnClick;
try
myCheckbox.OnClick := nil;
//your code here
finally
myCheckbox.OnClick := Event;
end;
end;
HTH
The internal design of the TCustomCheckBox is that it triggers the Click method every time the Checked property if changed. Be it by actually clicking it or setting it in code. And this is happening here when you call EnableControls because the control gets updated to display the value of the linked field in your dataset.
TButtonControl (which is what TCustomCheckBox inherits from) has the property ClicksDisabled. Use this instead of (or in addition to) the DisableControls/EnableControls call. Unfortunately it is protected and not made public by TCustomCheckBox but you can use a small hack to access it:
type
TButtonControlAccess = class(TButtonControl)
public
property ClicksDisabled;
end;
...
TButtonControlAccess(MyCheckBox1).ClicksDisabled := True;
// do some dataset stuff
TButtonControlAccess(MyCheckBox1).ClicksDisabled := False;
Of course you can put this into a method that checks all components and sets this property if the control inherits from TCustomCheckBox or some other criteria.

Default boolean value in a array of record - Delphi

I am helping out my company with some old delphi 7 code.
There is a record declared at the start that is used throughout to store all the data we want outputted.
type
TOutput_Type = record
result: String;
resultoffset: String;
selected: boolean;
resultcategory: integer;
end;
and then an array of this is declared
Output: array of TOutput_Type;
The length is set at the start to a large value, as actual length is unknown.
This array is used all over the place, but unfortunately the value selected is not always set when used.
My problem is I am adding in a summary of the data, but because selected is not set, delphi seems to give it a random true or false status.
Is there a way of setting all instances of selected as true at the start? Seems like a simple enough thing to do, but I'm not a delphi programmer so am unsure if its possible? I know I can go through and add in selected := true every time a new record is made, but I'd like to do it cleanly at the start if possible....
Thanks in advance
After calling SetLengt for Output variable you must first initiate the new record parts (because new allocated memory isn't defined) in for loop.
Something like:
OldLength := Length(Output);
SetLength(Output, NewLength);
for n := OldLength to NewLength -1 do
Output[n].selected := True;
Records, unlike objects, aren't initialized upon creation, so you need to initialize them yourself. Since you're on Delphi 7, you can't use records with methods, so what I'd do is make an initialization function, something like this:
type
TOutputArray: array of TOutput_Type;
function CreateOutputArray(length: integer): TOutputArray;
var
i: integer;
begin
SetLength(result, MyArbitraryItemCount);
FillChar(result[0], Length(Output)*SizeOf(TOutput_Type), 0);
for i := 0 to high(result) do
result[i].selected := true;
end;
I'd go for the factory method like in the question dcp linked to. Parameterless constructors aren't allowed for records, so you would always have to specify some parameters, which might be annoying if you don't really need them.
If this is all about initializing the content of the large array once at the start you could also use this:
SetLength(Output, MyArbitraryItemCount);
FillChar(Output[0], Length(Output)*SizeOf(TOutput_Type), 1);
Then everything is 1. Including selected :) Of course you could also use a for-loop...

Resources