Simple Delphi DBcharting - delphi

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).

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?

Removing controls from a TGridPanel

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

Delphi label values sorting

Im trying to sort Label's values. I have lots of labels with an integer value. Labels are called like Label1, Label2, [...], which Im accessing through FindComponent. I have no problem in sorting the integer values Ive stored in an array, but the problem is, after sorting, I have no idea which label had what value. My goal is to like, sort those labels by their value, so I'd get like an array with Labels sorted by their value. Im stuck at this point :(
Eg:
Label1.Caption := 10;
Label2.Caption := 4;
Label3.Caption := 7;
for i := 1 to 3
do some_array[i] := StrToInt(TLabel(FindComponent('Label' + IntToStr(i))).Caption);
sortarray(some_array);
Now, I have sorted array, but Im lacking some sort procedure that would also store label number in the corresponding place. Can someone point me out?
Instead of creating an array of integers, create an array of TLabel controls. This one you can sort the same way as the array of integers. Indeed, given a MyLabel: TLabel, you can easily get the associated integer as StrToInt(MyLabel.Caption).
In addition, the FindComponent approach is not very efficient. I'd do
const
ALLOC_BY = 100;
MAGIC_TAG = 871226;
var
i: Integer;
ActualLength: integer;
FLabels: array of TLabel;
begin
SetLength(FLabels, ALLOC_BY);
ActualLength := 0;
for i := 0 to ControlCount - 1 do
if Controls[i] is TLabel then
with TLabel(Controls[i]) do
if Tag = MAGIC_TAG then
begin
if ActualLength = length(FLabels) then
SetLength(FLabels, length(FLabels) + ALLOC_BY);
FLabels[ActualLength] := Controls[i];
inc(ActualLength);
end;
SetLength(FLabels, ActualLength);
SortArray(FLabels) // with respect to the StrToInt(CurLabel.Caption) of each
// CurLabel: TLabel.
Of course, you can skip the chunk allocating if you know the number of labels in advance.
Make sure that each of the labels that are to be included in the array have the Tag set to MAGIC_TAG.
Another option would be to create an array
FLabelDataArray: array of TLabelData;
of
type
TLabelData = record
Control: TLabel;
Value: integer;
end;
where
FLabelDataArray[i].Value := StrToInt(FLabelDataArray[i].Control.Caption);
is computed only once.
A quick-n-dirty solution that also works in old Delphi versions, is to use TStringList, which has a Sort method and an Objects property that allow you to associate one object to each entry in the list.
Note that the list is sorted in lexicographic order, so the integers must be left padded with zeroes when converted to strings.
var
list: TStringList;
i: integer;
lab: TLabel;
begin
Label1.Caption := '10';
Label2.Caption := '4';
Label3.Caption := '7';
list := TStringList.Create;
try
for i := 1 to 3 do begin
lab := TLabel(FindComponent('Label' + IntToStr(i)));
list.AddObject(Format('%10.10d', [StrToInt(lab.Caption)]), lab);
end;
list.Sort;
for i := 0 to list.Count-1 do
Memo1.Lines.Add(list[i] + #9 + TLabel(list.Objects[i]).Name);
finally
list.Free;
end;
end;
The output would be:
0000000004 Label2
0000000007 Label3
0000000010 Label1
Also, if instead of list.Sort you use list.Sorted := true, you get binary search on list as a bonus (using list.IndexOf or list.Find).
Edit: As Rudy says, visual components such as TLabel should only be used for displaying data, and not for storing and manipulating it. It is recommended to use appropiate data structures for this and to separate the logic of the program from its user interface.
As Andreas says, put the labels into a dynamic array, rather than sorting the values. Once you have them in such an array, sort them like this:
uses
Generics.Defaults, Generics.Collections;
procedure SortLabels(var Labels: array of TLabel);
var
Comparison: TComparison<TLabel>;
Comparer: IComparer<TLabel>;
begin
Comparison := function(const Left, Right: TLabel): Integer
begin
Result := StrToInt(Left.Caption)-StrToInt(Right.Caption);
end;
Comparer := TDelegatedComparer<TLabel>.Create(Comparison);
TArray.Sort<TLabel>(Labels, Comparer);
end;
As others have said, I don't think you're taking the right approach for this task. But, based on your question, a simple hack would be to use the tag property on each label to store its caption's value :
Label1.Caption := '10';
Label1.Tag:=10;
Label2.Caption := '4';
Label2.Tag:=4;
Label3.Caption := '7';
Label3.Tag := 7;
Then you can find the appropriate label by matching the 'array of integer' entry with the label's tag property.
Again: As Rudy and others have commented, your approach to this task is far from desirable, and my solution only conforms to your approach. The tag property itself is a hack built into Delphi, an artifact from ancient times, like 'label' and 'goTo' and really should not be used except out of sheer desperation - like when trying to re-work and retro-fit ancient, poorly written code or if you've got something in prod and you must get in a quick fix for an emergency situation.

Showing accumulated messages to the user

I want to show the user a summary of all the relevant messages that have occurred during a code execution (e.g. parsing, algorithm, conversion, validation etc). The messages should be showed together when the process is done.
A similar incident might occur none, one or several times. The user should be notified if the incident occurred. There might be several types of incidents.
I'm not sure if the scenario is clear, but maybe some code will help:
PSEUDO-CODE:
begin
//Execute computing process//
repeat
Set a flag if an incident occurs
Set another flag if another incident occurs
until done
//Show message to user//
if AnyFlagIsSet then
ShowPrettyMessageToUser
end;
EXECUTABLE DELPHI CODE:
program Test;
{$APPTYPE CONSOLE}
uses
SysUtils, StrUtils;
var
i: Integer;
tmpFlags: Array[1..4] of Boolean;
tmpMessage: String;
tmpChar: Char;
begin
Randomize;
repeat
//Initialization//
for i := 1 to 4 do
tmpFlags[i] := False;
//Will insident occur?//
for i := 0 to 5 do
begin
if (Random(10) = 0) then tmpFlags[1] := True;
if (Random(10) = 0) then tmpFlags[2] := True;
if (Random(10) = 0) then tmpFlags[3] := True;
if (Random(10) = 0) then tmpFlags[4] := True;
end;
//Show message//
tmpMessage := '';
if tmpFlags[1] then tmpMessage := tmpMessage + IfThen(tmpMessage <> '', #13#10+#13#10) + 'Incident 1';
if tmpFlags[2] then tmpMessage := tmpMessage + IfThen(tmpMessage <> '', #13#10+#13#10) + 'Incident 2';
if tmpFlags[3] then tmpMessage := tmpMessage + IfThen(tmpMessage <> '', #13#10+#13#10) + 'Incident 3';
if tmpFlags[4] then tmpMessage := tmpMessage + IfThen(tmpMessage <> '', #13#10+#13#10) + 'Incident 4';
Writeln('----------');
Writeln(tmpMessage);
Writeln('----------');
Writeln;
Write('Again? (Y/N) ');
Readln(tmpChar);
until tmpChar <> 'y';
end.
The code in the iteration is quiet complex in real life, of course.
And the messages are also more informative, and may even be formatted and multi-lined.
So...
Is there a best practice or pattern that can be used for this?
Any Delphi-component that handles this?
An easy solution would be to use a TStringList to collect all messages. You can then either display the strings in a listbox or concatenate the strings (in this case all messages should be valid sentences).
Pseudocode:
procedure DoSomething(Log : TStrings);
begin
//...
Log.Add ('Some hint.');
//...
Log.Add ('Some error happened.');
//...
end;
DoSomething (Log);
if (Log.Count > 0) then
LogListBox.Items.AddStrings (Log);
For formatted or multi-lined messages you could store HTML strings in the stringlist and use a component that can display HTML formatted text to display the messages.
Edit: If you want no duplicate messages, just do
Log.Duplicates := dupIgnore;
I would (where the memory requirements are not a factor) create a class which uses a hashed stringlist as a catalog, where X number of "logitems" could be inserted as objects. This allows some control over how the items are grouped. By accessing the stringlist using a normal index - you get a linear timeline. But accessing the items through it's hash-keys, you get the items grouped by content. Using a hashed stringlist is much faster to work with since it employes a lookup-table.
For an overall, application-wide solution, I have used the midas library's TClientDataset which has been with Delphi for ages. Just remember to declare "midaslib" as the first unit in your project and it's linked directly into your binary (no need to ship midaslib.dll). This gives you the benefit of fields and sorting by message/error types. As long as the record count dont get above 10k it's both fast and stable to work with.

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