Removing controls from a TGridPanel - delphi

I'm using TGridPanel to manage a number of panels. I create the panels and add them to the GridPanel using code like the following:
var
pnl: TPanel;
begin
pnl := TPanel.Create(GridPanel2);
pnl.Caption := 'Panel One';
pnl.Tag := 1;
pnl.Parent := GridPanel2;
pnl.Name := 'pnlOne';
GridPanel2.ControlCollection.AddControl(pnl);
pnl := TPanel.Create(GridPanel2);
pnl.Caption := 'Panel Two';
pnl.Tag := 2;
pnl.Parent := GridPanel2;
pnl.Name := 'pnlTwo';
GridPanel2.ControlCollection.AddControl(pnl);
pnl := TPanel.Create(GridPanel2);
pnl.Caption := 'Panel Three';
pnl.Tag := 3;
pnl.Parent := GridPanel2;
pnl.Name := 'pnlThree';
GridPanel2.ControlCollection.AddControl(pnl);
end;
You will notice that each panel has a different tag value.
I'd like to remove a panel from the GridPanel based on the value in the panel's tag property. I have tried the following code:
var
ii: integer ;
pnl: TPanel;
begin
for ii := 0 to GridPanel2.ControlCollection.Count -1 do begin
if GridPanel2.ControlCollection[ii].Control.Tag = 1 then begin
pnl := GridPanel2.ControlCollection[ii].Control as TPanel;
GridPanel2.ControlCollection.RemoveControl(pnl);
freeandnil(pnl);
end;
end;
gridpanel2.Refresh();
end;
This works well providing the panel is the last panel in the collection. If I try to remove the panel with tag = 1 or tag = 2 I get an out of range error. Clicking "continue" in the debugger leaves a space where the removed panel was, so does remove the panel.
What I'd like to see is, say panel 2 removed and subsequent panels shuffled down one place to leave no gaps.
How do I do this?
I'm using Delphi 10.1 Berlin if that matters.

As always when deleting an item from a list or collection you need to take precaution when the count changes. A for loop count is determined at the beginning of the loop. Now, if you delete an item from the list you will hit a non-existing index when the for loop continues to the end.
You can avoid this in many ways, f.ex. by breaking out of the loop, once you have found and deleted the item.
freeandnil(pnl);
break;
Another way, is to run the for loop backwards
for ii := GridPanel2.ControlCollection.Count -1 downto 0 do begin
Or you can use Repeat Until or Whileloops which checks the condition to continue on every turn of the loop.
To update the grid panel after deleting items call either or both of
gridpanel2.UpdateControlsRow();
gridPanel2.UpdateControlsColumn();
However, it feels quite tricky to get the order right

Related

Different Results of TScrollBox Population in Delphi and Lazarus

I populate ScrollBoxin the alike way:
procedure TForm1.FormCreate(Sender: TObject);
var
i: word;
begin
for i := 1 to 3 do
begin
with TLabel.Create(ScrollBox1) do
begin
Parent := ScrollBox1;
Top := 1000;
AutoSize := False;
Align := alTop;
Height := 25;
Caption := 'Label' + IntToStr(i);
end;
end;
ScrollBox1.Realign;
end;
When the code is run under Delphi I get the follwong result:
The order of items is proper.
But when I call the same code under Lazarus I get:
The order of items is reverse. I can solve the issue by reverse creation of ScrollBox children and/or adding {IFDEF ...} but I suspect this is not reliable. Adding compiler switches will double the volume of code making it bulky and difficult to read.
Is there a way to do unified reliable Delphi-Lazarus code for this purpose?
APPENDED
explanation on comment of #TomBrunberg
If I create chidren in reverse order (for instance for i := 3 downto 1) I get the opposite result: Delphi produces reverse and Lazarus - direct order. That is why I was saying about doubling of code.
APPENDED 2
on note of Tom Brunberg
When the same code is called from a Button onClick event handler the code behaviour becomes opposite (and again different in Lazarus and in Delphi).
APPENDED 3
Can I trust for i := 1 to 3... Top := 1000 + i; as it gives the expected result?

write specific line in tmemo according to line number

I am using the following to insert text from a text file into TMemo.
procedure TForm1.Button1Click(Sender: TObject);
var
SL: TStringList;
begin
SL := TStringList.Create;
try
SL.LoadFromFile('c:\testimeng\keyfil.txt');
Memo1.Lines.Assign(SL);
finally
SL.Free;
end;
end;
What i want to know is how to add a single line according to row number to TMemo when i choose the specific row number.
Example output:
During this time he has distinguished himself in the academic, sporting and cultural spheres of school life.
During this time he has distinguished himself in the academic and sporting spheres of school life.
During this time he has distinguished himself in the academic and cultural spheres of school life.
During this time he has distinguished himself in the academic aspect of school life.
During this time he has distinguished himself in both the sporting and cultural aspects of school life.
Any help appreciated.
I think you're asking about putting a single line from the TStringList into the TMemo when you specify which item (index, or line number) from the TStringList. If that's the case, you can use something like this:
Memo1.Lines.Add(SL[Index]);
So if the first line in your keyfile.txt is
During this time he has distinguished himself in the academic, sporting and cultural spheres of school life.
You would use
Memo1.Lines.Add(SL[0]); // Desired line number - 1
Ok, after your comment to your question, I think I know what you're wanting to do. Here's one way to do it:
Drop a TListBox, a TButton, and a TMemo on your form. I arranged mine with the ListBox on the left, the button next to it (at the top right corner), and then the memo just to the right of the button.
In the FormCreate event, populate the TListBox with your text file and clear the existing memo content:
procedure TForm1.FormCreate(Sender: TObject);
begin
Memo1.Clear;
ListBox1.Items.LoadFromFile('c:\testimeng\keyfil.txt');
end;
Double-click the button to add an OnClick handler:
procedure TForm1.Button1Click(Sender: TObject);
var
s: string;
begin
// If there's an item selected in the listbox...
if ListBox1.ItemIndex <> -1 then
begin
// Get the selected item
s := ListBox1.Items[ListBox1.ItemIndex];
// See if it's already in the memo. If it's not, add it at the end.
if Memo1.Lines.IndexOf(s) = -1 then
Memo1.Lines.Add(s);
end;
end;
Now run the app. Click on an item in the listbox, and then click the button. If the item is not already present in the memo, it will be added as a new last line. If it's already there, it won't be added (to prevent duplicates).
If you're wanting to add it to the end of the current last line (extending the paragraph, perhaps), then you'd do it like this:
// Add selected sentence to the end of the last line of the memo,
// separating it with a space from the content that's there.
Memo1.Lines[Memo1.Lines.Count - 1] := Memo1.Lines[Memo1.Lines.Count - 1] + #32 + s;
So, it should be clear by now that to add to the end of a specific line, you just grab the content that's already
there and add to it. For instance, if the user types 3 into a TEdit:
procedure TForm1.FormCreate(Sender: TObject);
begin
SL := TStringList.Create;
SL.LoadFromFile('c:\testimeng\keyfil.txt');
end;
procedure TForm1.ButtonAddTextClick(Sender: TObject);
var
TheLine: Integer;
begin
// SL is the TStringList from the FormCreate code above
TheLine := StrToIntDef(Edit1.Text, -1);
if (TheLine > -1) and (TheLine < Memo1.Lines.Count) then
if TheLine < SL.Count then
Memo1.Lines[TheLine] := Memo1.Lines[TheLine] + SL[TheLine];
end;
Write a specific line with String by Mouse Clicking on TMemo
Procedure TForm1.Button1Click(Sender: TObject);
Var SL: TStringList;
LineNumber : Integer;
Begin
LineNumber := Memo1.Perform(EM_LINEFROMCHAR, Memo1.SelStart, 0);
Memo1.SelStart := Memo1.Perform(EM_LINEINDEX, LineNumber, 0);
Memo1.SelLength := Length(Memo1.Lines[LineNumber]) ;
Memo1.SetFocus;
SL := TStringList.Create;
try
SL.LoadFromFile('c:\testimeng\keyfil.txt');
Memo1.SelText := SL.Strings[0];
finally
SL.Free;
end;
End;

Simple Delphi DBcharting

So, the problem I'm having is that I'm displaying two bars on the graph for each student, I just want one of them. They're the correct height though, so that's good.
This is my Delphi source code;
strlstField := TStringList.Create();
ADOQGetResults.SQL.clear;
ADOQGetResults.SQL.Add(
'SELECT Results.StudentID, SUM(Results.Rawmark) as TRM, StudentInfo.Fname '+
'FROM (StudentInfo INNER JOIN Results ON StudentInfo.StudentID = Results.StudentID) '+
'WHERE (((StudentInfo.StudentID)=Results.StudentID)) AND Results.TestID =12 '+
'GROUP BY StudentInfo.Fname, Results.StudentID'
);
ADOQGetResults.Active := True;
ADOQGetResults.Open;
DBChart1.Title.Text.Clear;
DBChart1.Title.Text.Add('Class leaderboard');
DBChart1.Title.Font.Size := 15;
DBChart1.LeftAxis.Title.Font.Size := 12;
DBChart1.LeftAxis.Title.Caption := 'Total marks';
DBChart1.BottomAxis.Title.Font.Size := 12;
DBChart1.BottomAxis.Title.Caption := 'Student';
//Charting Series
//To Remove Old Series
for intCnt := DBChart1.SeriesCount -1 downto 0 do
DBChart1.Series[intCnt].Free;
//To Add New Series
for intCnt := 1 to ADOQGetResults.FieldCount - 1 do
begin
strlstField.Add(ADOQGetResults.FieldList[intCnt].FieldName);
DBChart1.AddSeries(TBarSeries.Create(nil));
end;
//To set source for Series
for intCnt:= 0 to DBChart1.SeriesCount -1 do
begin
with DBChart1 do begin
Series[intCnt].Clear;
Series[intCnt].Title := strlstField[intCnt];
Series[intCnt].ParentChart := DBChart1;
Series[intCnt].DataSource := ADOQGetResults;
Series[intCnt].XLabelsSource := 'Fname';
Series[intCnt].YValues.ValueSource := 'TRM';
end;
end;
I've been trying to work-out whats going wrong all day, so if anyone can help at all I'd be very grateful!
Here is what the graph looks like right now;
http://oi48.tinypic.com/6qelba.jpg
Why are you looping over EVERY FIELD in the result (you return 3 fields in your query) and adding one series PER field in the result? It's almost like you think that the field count equals your row count or something. Secondly I would venture to guess that something in your query plus your data (that we can't see) could result in you getting more rows in your query result than you were expecting.
Why are you destroying and re-adding series when your query always returns 3 fields, 1 field is not charted, 1 field is the series label source and 1 field is the series value source? Just statically create one series at designtime in your dfm and forget all this crazy runtime stuff. Have you tried double clicking dbchart and adding ONE BarChart series there?
This works and is much less code. You don't need to open a dataset twice, by the way. Note that I'm using the DBDEMOS.mdb database that comes with Delphi here so that everyone can play along. Add a db chart and at DESIGNTIME add ONE barchart series to it. Configure as desired. Use this code. dataset below is a TADODataset.
-
dataset.CommandText := 'select EmpNo,FirstName,Salary from employee';
dataset.Active := True;
DBChart1.Title.Text.Clear;
DBChart1.Title.Text.Add('Class leaderboard');
DBChart1.Title.Font.Size := 15;
DBChart1.LeftAxis.Title.Font.Size := 12;
DBChart1.LeftAxis.Title.Caption := 'Total marks';
DBChart1.BottomAxis.Title.Font.Size := 12;
DBChart1.BottomAxis.Title.Caption := 'Student';
if DBChart1.SeriesCount<1 then
begin
raise Exception.Create('Add series to your chart in the dfm ONCE.');
end;
//To set source for Series
with DBChart1 do begin
Series[0].Title := 'Test';
Series[0].DataSource := dataset;
Series[0].XLabelsSource := 'FirstName';
Series[0].YValues.ValueSource := 'Salary';
end;
Note that this is still more code than you absolutely have to write. You could do most of this if not all in dfm (form designer).

Remove duplicates from combobox

Say i have a combobox with
apples
apples
pears
oranges
oranges
i would like to have it show
apples
pears
oranges
how can i do this?
for iter := combobox.Items.Count - 1 downto 0 do
begin
index := combobox.Items.IndexOf(combobox.Items[iter]);
if index < iter then
combobox.Items.Delete(iter);
end;
I suggest that you simply refill the combo box each time. That makes the logic simpler:
ComboBox.Items.BeginUpdate;
try
ComboBox.Clear;
for Str in Values do
begin
if ComboBox.Items.IndexOf (Str) = -1 then
ComboBox.Items.Add (Str);
end;
finally
ComboBox.Items.EndUpdate;
end;
Just to put methods against eachother: one keeps the order but is increasingly slow with larger number of items. The other stays relatively faster but doesn't keep order:
procedure SortStringlist;
var
i,index,itimer: integer;
sl : TStringlist;
const
numberofitems = 10000;
begin
sl := TStringlist.Create;
for i := 0 to numberofitems-1 do begin
sl.Add(IntToStr(random(2000)));
end;
Showmessage(IntToStr(sl.Count));
itimer := GetTickCount;
sl.Sort;
for I := sl.Count-1 downto 1 do begin
if sl[i]=sl[i-1] then sl.Delete(i);
end;
Showmessage(IntToStr(sl.Count)+' Time taken in ms: '+IntToStr(GetTickCount-itimer));
sl.free;
sl := TStringlist.Create;
for i := 0 to numberofitems-1 do begin
sl.Add(IntToStr(random(2000)));
end;
Showmessage(IntToStr(sl.Count));
itimer := GetTickCount;
for i := sl.Count - 1 downto 0 do
begin
index := sl.IndexOf(sl[i]);
if index < i then
sl.Delete(i);
end;
Showmessage(IntToStr(sl.Count)+' Time taken in ms: '+IntToStr(GetTickCount-itimer));
end;
If you don't care if the items get reordered (or they're sorted already), TStrings can do the work for you - it eliminates all of the looping, deletion, and other work. (Of course, it requires the creation/destruction of a temporary TStringList, so if that's an issue for you it won't work.)
var
SL: TStringList;
begin
ComboBox1.Items.BeginUpdate;
try
SL := TStringList.Create;
try
SL.Sorted := True; // Required for Duplicates to work
SL.Duplicates := dupIgnore;
SL.AddStrings(ComboBox1.Items);
ComboBox1.Items.Assign(SL);
finally
SL.Free;
end;
finally
ComboBox1.Items.EndUpdate;
end;
end;
To properly compare with Igor's answer (which includes no BeginUpdate/EndUpdate), remove those things:
var
SL: TStringList;
begin
SL := TStringList.Create;
try
SL.Sorted := True; // Required for Duplicates to work
SL.Duplicates := dupIgnore;
SL.AddStrings(ComboBox1.Items);
ComboBox1.Items.Assign(SL);
finally
SL.Free;
end;
end;
You have to remove duplicates from the source data.
In most scenarios, a ComboBox is filled with data in run-time, which means, data is coming from some source. There are basically 2 scenarios here: a dataset from database and a collection of strings from any other source. In both cases you filter out duplicates before inserting anything into the ComboBox.
If source is a dataset from database, simply use the SQL DISTINCT keyword.
If source is any collection of strings, use a peace of code provided in the answer by #Smasher.
I faced this problem several times before, and i used all the previous approaches and I'm still using them, but do you know : i think the best approach , though not mentioned here, is to subclass TComboBox, creating a new method (say AddUnique ) that add the string to the combo ONLY if it does not exist previously , otherwise it will drop it.
This solution may cost some extra time in the beginning , but it will solve the problem once and for all.

Why is my code so slow?

Top-posted (sorry) answer, for those who don't have time to get into it but may have similar problems.
Rule #1, as always, move as much as you can out of loops.
2, moving TField var := ADODataSet.FieldByname() out of the loop
3, ADODataSet.DisableControls(); and ADODataSet.EnableControls(); around the loop
4, stringGrid.Rows[r].BeginUpdate() and EndUpdate() on each row (cannot do on teh whle control)
each of these shaved off a few seconds, but I got it down to "faster than the eye can see" by changing
loop
stringGrid.RowCount := stringGrid.RowCount + 1;
end loop
to putting stringGrid.RowCount := ADODataSet.RecordCount; before the loop
+1 and heartfelt thanks to all who helped.
(now I will go and see what I can do to optimize drawing a TChart, which is also slow ;-)
with about 3,600 rows in the table this takes 45 seconds to populate the string grid. What am I doing wrong?
ADODataSet := TADODataSet.Create(Nil);
ADODataSet.Connection := AdoConnection;
ADODataSet.CommandText := 'SELECT * FROM measurements';
ADODataSet.CommandType := cmdText;
ADODataSet.Open();
while not ADODataSet.eof do
begin
TestRunDataStringGrid.RowCount := TestRunDataStringGrid.RowCount + 1;
measurementDateTime := UnixToDateTime(ADODataSet.FieldByname('time_stamp').AsInteger);
DoSQlCommandWithResultSet('SELECT * FROM start_time_stamp', AdoConnection, resultSet);
startDateTime := UnixToDateTime(StrToInt64(resultSet.Strings[0]));
elapsedTime := measurementDateTime - startDateTime;
TestRunDataStringGrid.Cells[0, Pred(TestRunDataStringGrid.RowCount)] := FormatDateTime('hh:mm:ss', elapsedTime);
TestRunDataStringGrid.Cells[1, Pred(TestRunDataStringGrid.RowCount)] := FloatToStrWithPrecision(ADODataSet.FieldByname('inputTemperature').AsFloat);
TestRunDataStringGrid.Cells[2, Pred(TestRunDataStringGrid.RowCount)] := FloatToStrWithPrecision(ADODataSet.FieldByname('outputTemperature').AsFloat);
TestRunDataStringGrid.Cells[3, Pred(TestRunDataStringGrid.RowCount)] := FloatToStrWithPrecision(ADODataSet.FieldByname('flowRate').AsFloat);
TestRunDataStringGrid.Cells[4, Pred(TestRunDataStringGrid.RowCount)] := FloatToStrWithPrecision(ADODataSet.FieldByname('waterPressure').AsFloat * convert);
TestRunDataStringGrid.Cells[5, Pred(TestRunDataStringGrid.RowCount)] := FloatToStrWithPrecision(ADODataSet.FieldByname('waterLevel').AsFloat);
TestRunDataStringGrid.Cells[6, Pred(TestRunDataStringGrid.RowCount)] := FloatToStrWithPrecision(ADODataSet.FieldByname('cod').AsFloat);
ADODataSet.Next;
end;
ADODataSet.Close();
ADODataSet.Free();
update:
Function DoSQlCommandWithResultSet(const command : String; AdoConnection : TADOConnection; resultSet : TStringList): Boolean;
var
i : Integer;
AdoQuery : TADOQuery;
begin
Result := True;
resultSet.Clear();
AdoQuery := TADOQuery.Create(nil);
try
AdoQuery.Connection := AdoConnection;
AdoQuery.SQL.Add(command);
AdoQuery.Open();
i := 0;
while not AdoQuery.eof do
begin
resultSet.Add(ADOQuery.Fields[i].Value);
i := i + 1;
AdoQuery.Next;
end;
finally
AdoQuery.Close();
AdoQuery.Free();
end;
end;
You are executing the command SELECT * FROM start_time_stamp 3,600 times, but it does not appear to me that it is correlated with your outer loop in any way. Why not execute it once before the loop?
That SELECT command appears to return only a single column of a single record, yet you use "*" to load all columns, and no WHERE clause to limit the results to a single row (if there's more than one row in the table).
You use only a limited number of columns from Measurements, but you retrieve all columns with "*".
You don't show the contents of DoSQlCommandWithResultSet, so it's not clear if there's a problem in that routine.
It's not clear whether the problem is in your database access or the string grid. Comment out all the lines pertaining to the string grid and run the program. How long does the database access alone take?
Additionally to Larry Lustig points:
In general, FieldByName is comparably slow method. You are calling it in loop for the same fields. Move the getting of field references out of the loop and store references in the variables. Like: InputTempField := ADODataSet.FieldByname('inputTemperature');
You are resizing the grid in the loop TestRunDataStringGrid.RowCount := TestRunDataStringGrid.RowCount + 1. That is the case, when you should use ADODataSet.RecordCount before the loop: TestRunDataStringGrid.RowCount := ADODataSet.RecordCount.
That is a good practice to call ADODataSet.DisableControls before loop and ADODataSet.EnableControls after loop. Even more actual that is for ADO dataset, which has not optimal implementation and those calls help.
Depending on a DBMS you are using, you can improve the fetching performance by setting a larger "rowset size". Not sure, how it control in ADO, probably setting ADODataSet.CacheSize to a greater value will help. Also, there are cursor settings :)
instead of calling ADODataSet.FieldByname('Fieldname') inside the loop you should declare local variables of type TField for each field, assign ADODataset.FindField('Fieldname') to the variables and use the variables inside the loop. FindFieldByName searches a list with every call.
Update:
procedure TForm1.Button1Click(Sender: TObject);
var
InputTemp, OutputTemp: TField;
begin
ADODataSet := TADODataSet.Create(Nil);
try
ADODataSet.Connection := ADOConnection;
ADODataSet.CommandText := 'SELECT * FROM measurements';
ADODataSet.Open;
InputTemp := ADODataSet.FindField('inputTemperature');
OutputTemp := ADODataSet.FindField('outputTemperature');
// assign more fields here
while not ADODataSet.Eof do begin
// do something with the fields, for example:
// GridCell := Format ('%3.2f', [InputTemp.AsFloat]);
// GridCell := InputTemp.AsString;
ADODataSet.Next;
end;
finally
ADODataSet.Free;
end;
end;
Another option would be to drop the TADODataset Componont on the form (or use a TDataModule) and define the fields at designtime.
Additional to the Larry Lustig answer, consider using data-aware controls instead, like the TDbGrid component.
If you aren't using data-aware controls you should use TestRunDataStringGrid.BeginUpdate before and TestRunDataStringGrid.EndUpdate after loop. Without this is your grid constantly redrawing after each modification (adding new row, cell update).
Another tip is set AdoQuery.LockType := ltReadOnly before opening query.
You could also try an instrumenting profiler instead of a sampling profiler to get better results (sampling profilers miss lot of detail info, and most time they have less then 1000 samples per second, and 1000 is already low: only good to get a quick overview).
Instrumenting profilers:
AQTime (commercial)
AsmProfiler (open source)
http://code.google.com/p/asmprofiler/wiki/AsmProfilerInstrumentingMode

Resources