Add stringgrid column at runtime - delphi

I have found many solutions online but they aren't working becaue the StringGrid1.ColumnCount peoperty is read only. I am using Delphi 10 Seattle.
I have a StringGrid1 and I need to add columns at runtime. To be specific I have to add columns according to the size of a TList. In particular:
var a: TList<double>;
begin
//fill the TList...
for i := 0 to a.Count - 1 do
begin
StringGrid1.AddColumn(); //how can I do this?
end;
end;
I find this very easy on Lazarus (but it has FPC of course) but on Delphi I really don't know what to do. I am working on Firemonkey.

Use the grid's AddObject() or InsertObject() method to add an object instance of the desired TColumn-derived class, like TStringColumn. The column object will get added to the grid's Columns array. The ColumnCount property simply returns the number of columns in the array, that is why it is read-only.

Related

Number of rows in DBCtrlGrid on Delphi

When I use DBCtrlGrid Delphi component, setting the RowCount property to 5, for example, it will always display 5 lines on the component, even if the table has only 3 records. I need to know how do I hide the additional lines and show only the lines that my table has records.
You can use the AfterScroll event of the dataset connected to the DBCtrlGrid to set its RowCount:
procedure TForm1.qApplsAfterScroll(DataSet: TDataSet);
begin
DBCtrlGrid1.RowCount := DataSet.RecordCount;
end;
Be aware, though, that not all types of Delphi dataset return meaningful numbers for their RecordCounts. If yours doesn't, you'll need to do something like running a "SELECT COUNT(*) ..." query in the AfterScroll event to get the value you need to set RowCount to.
Btw, the main use of datasets' AfterScroll event is to allow you to do things like this, where some action needs to be taken when the dataset's cursor moves.

Avoiding Duplicate Column Name Additions in a TDataSet

I am dynamically adding fields to a TDataSet using the following code:
while not ibSQL.Eof do
fieldname := Trim(ibSql.FieldByName('columnnameofchange').AsString);
TDataSet.FieldDefs.Add(fieldname , ftString, 255);
end
Problem is that I might get duplicate names so what is the easiest way to screen for duplicates and not add the duplicates that are already added.
I hope not to traverse through the TDataSet.FieldDefList for each column added as this would be tedious for every single column addition. And there can be many additions.
Please supply another solution if possible. If not then I am stuck using the FieldDefList iteration.
I will also add that screening out duplicates on the SQL query is an option but not a desired option.
Thanks
TFieldDefs has a method IndexOf that returns -1 when a field with the given name does not exist.
If I understand you correctly, the easiest way would probably be to put all of the existing field names in a TStringList. You could then check for the existence before adding a new field, and if you add it you simply add the name to the list:
var
FldList: TStringList;
i: Integer;
begin
FldList := TStringList.Create;
try
for i := 0 to DataSet.FieldCount - 1 do
FldList.Add(DataSet.Fields[i].FieldName);
while not ibSQL.Eof do
begin
fieldname := Trim(ibSql.FieldByName('columnnameofchange').AsString);
if FldList.IndexOf(fieldName) = -1 then
begin
FldList.Add(fieldName);
DataSet.FieldDefs.Add(fieldname , ftString, 255);
end;
ibSQL.Next;
end;
finally
FldList.Free;
end;
end;
I'm posting this anyway as I finished writing it, but clearly screening on the query was my preference for this problem.
I'm having a bit of trouble understanding what you're aiming for so forgive me if I'm not answering your question. Also, it has been years since I used Delphi regularly so this is definitely not a specific answer.
If you're using the TADOQuery (or whatever TDataSet you're using) in the way I expect my workaround was to do something like:
//SQL
SELECT
a.field1,
a.... ,
a.fieldN,
b.field1 as "AlternateName"
FROM
Table a INNER JOIN Table b
WHERE ...
As which point it automatically used AlternateName instead of field1 (thus the collision where you're forced to work by index or rename the columns.
Obviously if you're opening a table for writing this isn't a great solution. In my experience with Delphi most of the hardship could be stripped out with simple SQL tricks so that you did not need to waste time playing with the fields.
Essentially this is just doing what you're doing at the source instead of the destination and it is a heck of a lot easier to update.
What I'd do is keep a TStringList with Sorted := true and Duplicates := dupError set. For each field, do myStringList.Add(UpperCase(FieldName)); inside a try block, and if it throws an exception, you know it's a duplicate.
TStringList is really an incredibly versatile class. It's always a bit surprising all the uses you can find for it...

Delphi reaching a DBGrid's rows

So i have a TDBGrid, my purpose is searching DBGrid's Fieldname and comparing it with my Edit's Text property and if they are equal then,
i want to write the whole column which i've found the match, to a ListBox.
With a for loop with fieldcount, i can compare FieldName, though since there is no rows or rowcount property i can use, i don't know how i would get the index of this whole column.
for i:=0 to DBGrid1.FieldCount-1 do
begin
if DBGrid1.Fields[i].FieldName=Edit1.Text then
for j:=1 to DBGrid1.RowCount-1 do
ListBox1.Items.Add(DBGrid1.Rows.Fields[i].Index.AsString);
end;
This is an imaginary code of what im trying to do...
P.S.:I'm still using Delphi 7, (educational reasons)
You can't get the row values directly from the DbGrid. Instead, you have to navigate through the dataset that's used to feed the DbGrid.
This example assumes you are using a TClientDataSet.
for i := 0 to DBGrid1.FieldCount - 1 do
begin
if DBGrid1.Fields[i].FieldName = Edit1.Text then
begin
ClientDataSet1.DisableControls;
try
ClientDataSet1.First();
while (not ClientDataSet1.Eof) do
begin
ListBox1.Items.Add(ClientDataSet1.FieldByName(Edit1.Text).AsString);
ClientDataSet1.Next();
end;
finally
ClientDataSet1.EnableControls;
end;
end;
end;
As far as DBGrid only displays an excerpt of the data, IMHO you should
get a bookmark of your dataset
disable Controls
use first, while not eof with your dataset, adding
Dataset.FieldbyName(Edit1.text).asString to your list
goto bookmark
enable controls

How best to present my Delphi database table to FastReport for lists and aggregates

I have spent several days so far laying the ground work to use FastReport in my Application. The Application stores device test result data in the form of a DBF file comprising several fixed fields(DeviceID, Passed etc) plus a variable number of result fields, each of which correspond to the type of measurement data available. There can be as few as one of these fields and as many as 100. Each field has a letter code name such as OV and RV. Total record counts can be from zero up to some 10's of thousands.
A specific report template will have already included in its design the field names that it will display. Missing fields will be empty on the report.
My question involves the best way of designing the report and the data supplied to the report so that the report construction is as simple as possible - I'm going to allow my users to generate their own reports - and I need two kinds of report output - list of results and aggregates. It is the aggregates that are giving me the headache. I need not only MIN, MAX, COUNT etc (as provided internally in FastReport) but Standard Deviation as well. Further, I would like to use the FastReport 'drill down' feature where you can click on a group header and the data table is revealed or hidden. My aggregates should ideally be in the header, not the footer so that they appear all the time.
I have found that SQL in a TQuery gives me a lot of flexibility since it provides the 'StDev' aggregrate (FastREport does not) but as far as I can see I would need a fixed TQuery for each of my fields. So far, the nicest solution that I can come up with involves using a filter on the main table for 'Passed' (so that the user can view passe, failed or all) and then to build a separate 'stats' table with the same field name columns, but with MIN, MAX, COUNT, MEAN, STDEV as individual records. I would then use a TfrxDBDataSet to expose this table to FastReport. I see that I can also use FastReport's own ADODatabase and ADOQuery to directly access my DBF file. This works well but again I did not want to expose this access layer to my user in the report if possible.
This just seems so messy when aggregate functions must be a fundamental database requirement. Am I missing a much easier way of doing this? I've worked my way through the (excellent) demos supplied with FastReport (professional) and I'm using XE2. I'm also aware of the useful functions in the MATH unit if I need to calculate StDev myself.
I would appreciate any guidance, thanks.
For anything you could calculate in code, lists of array values, aggregate or functional calculation results, I prefer to use the TfrxUserDataSet and implement the TfrxReport.OnGetvalue event.
Although it might initially be confusing, the user datasets simply declare a data set name, and the list of fields available through that data set name and use events which fire to let you "navigate" (first, next record) and declare when you've reached the end of your calculated data. This allows you to build a "generator" or else, just a normal virtual-data-provider set of logic for your calculations.
Here's what my OnGetValue events look like:
procedure TfrmReport.frxReportGetValue(const VarName: string; var Value: Variant);
begin
Value := GetReportValue(VarName);
end;
// INPUT: VarName = '(<GlobalArea."hdReportTitle">)'
// OUTPUT: tableName = 'GlobalArea', fieldName = 'hdReportTitle'
function ParseVar(const VarName:String; var tableName,fieldName:String; var ParenFlag:Boolean):Boolean;
var
paVarName:String;
angleBracketFlag:Boolean;
dotPos:Integer;
fieldQuoteFlag:Boolean;
procedure RemoveOuter(var str:String; initialChar,finalChar:Char; var flag);
var
n:Integer;
begin
n := Length(str);
if n>2 then begin
ParenFlag := (str[1]=initialChar) and (str[n]=finalChar);
if ParenFlag then begin
str := Copy(str,2,n-2);
end;
end;
end;
begin
result := false;
fieldQuoteFlag := false;
paVarName := SysUtils.Trim(VarName);
ParenFlag := false;
tableName := '';
fieldName := '';
RemoveOuter(paVarName, '(',')',parenFlag);
RemoveOuter(paVarName,'<','>',angleBracketFlag);
dotPos := Pos('.',paVarName);
if dotPos >0 then begin
tableName := Copy(paVarName,1,dotPos-1);
fieldName := Copy(paVarName,dotPos+1,Length(paVarName));
RemoveOuter(fieldName, '"','"',fieldQuoteFlag);
result := true;
end else begin
tableName := '';
fieldName := paVarName;
end;
end;
function TfrmProfitAnalysisReport.GetReportValue(const VarName:String):Variant;
var
tableName:String;
fieldName:String;
parenFlag:Boolean;
begin
ParseVar(VarName,tableName,fieldName,parenFlag);
result := NULL;
{ Global Area - Header Values }
if sameText(tableName,'GlobalArea') then begin
if fieldName='hdReportTitle' then
result := GetTitle; { A function that calculates a title for the report }
else if fieldName='hdReportSubtitle' then
result := 'Report for Customer XYZ'
else if fieldName='....' then begin
...
end;
if Variants.VarIsNull( result) then
result := '?'+fieldName+'?';
end;
Well, a lot of questions with a lot of possible answers:
1) About the datasets, I really recommend put them in your application (DataModule or Form) instead of using them inside the report. It will give you more flexibility;
2) You can have one query for each aggregation, but this will affect performance if your data tables grows in tons of records. Some alternatives:
2.1) calculate the values in your FastReport script, but this will also expose the logic to the report;
2.2) Iterate through the record on the Delphi code, and pass the results as variables to your report. Example:
frxReport.Variables['MIN'] := YourMinVariableOrMethod;
frxReport.Variables['MAX'] := YourMaxVariableOrMethod;
2.3) Using a ClientDataSet associated with your query and implement TAggregateFields on the ClientDataSet.
I, personally, prefer the 2.2 approach, with all logic in the Delphi code, which is simple and powerful.

How do I enumerate JvMemoryData...Or, how do I create a hash with a single key and multiple values?

I am using JvMemoryData to populate a JvDBUltimGrid. I'm primarily using this JvMemoryData as a data structure, because I am not aware of anything else that meets my needs.
I'm not working with a lot of data, but I do need a way to enumerate the records I am adding to JvMemoryData. Has anyone done this before? Would it be possible to somehow "query" this data using TSQLQuery?
Or, is there a better way to do this? I'm a bit naive when it comes to data structures, so maybe someone can point me in the right direction. What I really need is like a Dictionary/Hash, that allows for 1 key, and many values. Like so:
KEY1: val1;val2;val3;val4;val5;etc...
KEY2: val1;val2;val3;val4;val5;etc...
I considered using THashedStringList in the IniFiles unit, but it still suffers from the same problem in that it allows only 1 key to be associated with a value.
One way would be to create a TStringList, and have each item's object point to another TList (or TStringList) which would contain all of your values. If the topmost string list is sorted, then retrieval is just a binary search away.
To add items to your topmost list, use something like the following (SList = TStringList):
Id := SList.AddObject( Key1, tStringList.Create );
InnerList := tStringList(SList.Objects[id]);
// for each child in list
InnerList.add( value );
When its time to dispose the list, make sure you free each of the inner lists also.
for i := 0 to SList.count-1 do
begin
if Assigned(SList.Objects[i]) then
SList.Objects[i].free;
SList.Objects[i] := nil;
end;
FreeAndNil(SList);
I'm not a Delphi programmer but couldn't you just use a list or array as the value for each hash entry? In Java terminology:
Map<String,List>
You already seem to be using Jedi. Jedi contains classes that allow you to map anything with anything.
Take a look at this related question.
I have been using an array of any arbitrarily complex user defined record types as a cache in conjunction with a TStringList or THashedStringList. I access each record using a key. First I check the string list for a match. If no match, then I get the record from the database and put it in the array. I put its array index into the string list. Using the records I am working with, this is what my code looks like:
function TEmployeeCache.Read(sCode: String): TEmployeeData;
var iRecNo: Integer;
oEmployee: TEmployee;
begin
iRecNo := CInt(CodeList.Values[sCode]);
if iRecNo = 0 then begin
iRecNo := FNextRec;
inc(FNextRec);
if FNextRec > High(Cache) then SetLength(Cache, FNextRec * 2);
oEmployee := TEmployee.Create;
oEmployee.Read(sCode);
Cache[iRecNo] := oEmployee.Data;
oEmployee.Free;
KeyList.Add(Format('%s=%s', [CStr(Cache[iRecNo].hKey), IntToStr(iRecNo)]));
CodeList.Add(Format('%s=%s', [sCode, IntToStr(iRecNo)]));
end;
Result := Cache[iRecNo];
end;
I have been getting seemingly instant access this way.
Jack

Resources