How to clone a dataset structure (TFDQuery) in Delphi 10? - delphi

Can anyone help me to clone a TFDQuery in run-time? I'm coding in Delphi Tokyo , I have a Datamodule with a TFDQuery in which I defined all fields properties using Fields Editor at design time, in this way my DBGrid1 that points to this a Datamodule of this dataset, has all columns properly formated (dislay names, width, format, order). During run-time I need to create new instances of TFDQuery, TDatamodule and link these new objects with the Dbgrid1. I need this new TFDQuery be identiical to the existing one defined at design-time in order to keep DBgrid1 with same display names, display width and display formats as the design-time!
I tried the following approaches to copy dataset field definitions :
**1st Approach : Method Assign for TFDQuery (didn't work) **
type
TFormDados = class(TForm)
Edit1: TEdit;
Button1: TButton;
DBGrid1: TDBGrid;
Edit2: TEdit;
Label1: TLabel;
Label2: TLabel;
procedure Button1Click(Sender: TObject);
private
{ Private declarations }
vconnection : TFDConnection;
vdataset : TFDQuery;
vdatasource : Tdatasource;
public
{ Public declarations }
end;
var
FormDados: TFormDados;
implementation
{$R *.dfm}
Uses
unitdata;
procedure TFormDados.Button1Click(Sender: TObject);
var
i : integer;
begin
vconnection := TFDConnection.Create(nil);
vconnection.Assign(Dtmodule.FDConGrafico);
vdataset := TFDQuery.Create(nil);
vdataset.Connection := vconnection;
vdataset.Assign(Dtmodule.FDQueryDados); // Runtime Error : Cannot assign a TFDQuery to a TFDQuery
2nd Approach : Assign FieldDefs from the existing Dataset to the new one - didn't work !
...
vdataset.FieldDefs.Assign(Dtmodule.FDQueryDados.FieldDefs);
vdataset.sql := Dtmodule.FDQueryDados.sql;
vdataset.params := Dtmodule.FDQueryDados.Params;
vdataset.FieldDefs.Update;
vdataset.CreateDataSet;
vdatasource := Tdatasource.create(nil);
vdatasource.DataSet := vdataset;
dbgrid1.DataSource := vdatasource;
vdataset.close;
vdataset.Params[0].Asinteger := strtoint(edit1.Text);
vdataset.Params[1].Asinteger := strtoint(edit2.Text);
vdataset.Open;
Althought Assign method had run, vdataset didn't receive the fields definitions of the existing FDQquery . After open the vdataset , DBGrid1 did not show the columns sequence, labels and formats fro the source dataset , WHY ?
3rd Approach - Copy fields definition, one by one - didn't work
for i:=0 to Dtmodule.FDQueryDados.Fields.Count -1 do
begin
with vdataset.FieldDefs.AddFieldDef do
begin
Name := Dtmodule.FDQueryDados.FieldDefs[i].Name;
Datatype := Dtmodule.FDQueryDados.FieldDefs[i].DataType;
Displayname := Dtmodule.FDQueryDados.FieldDefs[i].Displayname;
Fieldno := Dtmodule.FDQueryDados.FieldDefs[i].FieldNo;
end;
end;
vdataset.FieldDefs.Update;
vdataset.CreateDataSet;
vdatasource := Tdatasource.create(nil);
vdatasource.DataSet := vdataset;
dbgrid1.DataSource := vdatasource;
...
This code lead to the same result as the approach 2nd, i.e., it run but after opened vdataset , DBGrid1 did not show the columns sequence, labels and formats fro the source dataset.
I appreciate your help to fix the above code OR to implement the right method to copy dataset fields definitions from one existing dataset to a new one.
Thank you all in advance !

When you use the Fields editor for queries you are creating Fields not FieldDefs. From what I can tell the FieldDefs are kept in sync with the FieldsCollection when the component is created (or maybe opened not 100% sure). The Display* properties are not available on the FieldDef object - they only exist on the Field object. When you go to copy the structure you need to iterate the fields. The the method we use is below.
Note that the loop and the items created are "Fields", but we use a temporary FieldDef object to make the code simpler. The TFieldDef.CreatField serves as a class factory method to get the correct type of field i.e. TIntegerField vs TStringField. Also if you are using calculated fields you will need to hookup the OnCalcField event. This method does not do that.
procedure CopyFieldStructure(Source: TDataSet; Target: TDataset);
{^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^}
var
Field: TField;
NewField: TField;
FieldDef: TFieldDef;
begin
Target.Fields.Clear;
Target.FieldDefs.Clear;
// Cannot perform the next operation on an opened dataset
if Target.State <> dsInactive then
Target.Close;
for Field in Source.Fields do
begin
// We are going to setup the first part in a FieldDef
// that will set us use the CreateField Call in order to
// get the correct subclass of TField created.
FieldDef := Target.FieldDefs.AddFieldDef;
FieldDef.DataType := Field.DataType;
FieldDef.Size := Field.Size;
FieldDef.Name := Field.FieldName;
NewField := FieldDef.CreateField(Target);
NewField.Visible := Field.Visible;
NewField.DisplayLabel := Field.DisplayLabel;
NewField.DisplayWidth := Field.DisplayWidth;
NewField.EditMask := Field.EditMask;
NewField.Calculated := Field.Calculated;
end;
end;
Here is a similar StackOverflow question. I think this is where I originally took my code from: Is there some better way to copy all DataSet Fields and their properties to another DataSet?
And here is one other blog post that uses a similar approach: How to: Clone TField and TDataset fields structure
Also don't get fooled by the TDataSet.CopyField method. The help makes it seem like it could copy the field structure. When really it copies the current field "values" for any matching field names.

Related

How can i add fields to TfrxDBDataset

if Length(idStrArray)>0 then
begin
with DataModule4.ADQueryTemp do
begin
Close;
SQL.Clear;
SQL.Add('SELECT id, pato, ftest, res FROM tbl ');
SQL.Add('WHERE id IN ('+idStrArray+')');
Open;
(rprMasterDataFish as Tfrxmasterdata).DataSet := frxDst_Multi;
(rprMasterDataFish as Tfrxmasterdata).DataSetName := 'Multi';
end;
end;
Hello,
I have TfrxDBDataset component. I can add fields from table like above. But i also want to add fields and values manually at runtime.
I have text file like this :
id note
1 sample
2 sample
I want to read this text file and insert note to frxDst_Multi. Is this possible ?
I dont want to create a new column as note in tbl. Because, i have too many mysql server.
Thanks in advice,
You can't add fields to a dataset while it is open, so you have to do it before
it is opened, either in code or using the TDataSet fields editor. If you are
doing it in code, you can add the field in the dataset's BeforeOpen
event.
The next problem is that is you don't want to field to be bound to the table the
dataset accesses, you need to add it as a calculated field and set its value
in the dataset's`OnCalcFields' event - see example below.
Ideally, the added field would be a TMemoField, but unfortunately a TMemoField
can't be a calculated field (FieldKind = ftMemo). So probably the best thing you can do is to make it a String field, but then you will need to
give it a fixed maximum size and truncate the field's value at that size when you calculate its value.
Btw, I don't know whether your TfrxDBDataset supports the fkInternalCalc fieldkind, but if it does, then you could try adding the note field as a TMemoField instead of a TStringField one.
The one thing I haven't been able to do is to load the field's value from
an external file because you haven't said in your q how to determine the
name of the file which is to be read.
Obviously, the IsNull check in the frxDst_MultiCalcFields event is to avoid the overhead of reloading the file if its contents have already been read.
const
NoteFieldSize = 4096;
procedure TForm1.AddNoteField;
var
NoteField : TField;
begin
if frxDst_Multi.FindField('Note') = Nil then begin
NoteField := TStringField.Create(frxDst_Multi);
NoteField.FieldName := 'Note';
NoteField.Size := NoteFieldSize;
NoteField.FieldKind := fkCalculated;
NoteField.DataSet := frxDst_Multi;
end;
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
frxDst_Multi.Open;
end;
procedure TForm1.frxDst_MultiCalcFields(DataSet: TDataSet);
var
S : String;
begin
if DataSet.FieldByName('Note').IsNull then begin
S := 'a note'; // replace by code to set the field value
DataSet.FieldByName('Note').AsString := Copy(S, 1, NoteFieldSize);
end;
end;
procedure TForm1.frxDst_MultiBeforeOpen(DataSet: TDataSet);
begin
AddNoteField;
end;

Dynamically creating a new component without predeclared variable limitation?

I am working on a school project. I need the user to click a button and each time the button is pressed a new component with a new name is created.
The idea I had looked something like this
procedure TForm1.btnClick (Sender: TObject);
Var
pnlTest1, pnlTest2 : TPanel;
Begin
If iCount = 1
then
Begin
pnlTest1 := TPanel.Create(Self);
pnlTest1.Parent := Self;
pnlTest.Top := 0;
Etc...
End
Else if iCount = 2
Then
Begin
PnlTest2 := TPanel.Create(Self);
PnlTest2.Parent := Self;
PnlTest2.Top := 0;
Etc...
End;
The problem is i can only create as many components as I have created varaibles and I need the user to craete basically an infinite amount of new components. I tried other methods as well but the same problem occurs, I also tried creating dynamic variables with pointers and so on but the data types of those varaibles worked only (in my efforts) with the basic data types (Integer, String etc) and so i could not create a variable in run time to create a component (in the example pnlTest : TPanel was declared, this did not work with the dynamic variables)
Please advise on how i can create a new component everytime the button is clicked without bieng limited by the preexisting variables or please advise on how i can have a ''ínfinite'' amount of variables to use for creating a new component each time.
You only need to declare 1 pointer variable to receive the new component instance, eg:
private
iCount: Integer;
...
procedure TForm1.btnClick (Sender: TObject);
Var
pnlTest : TPanel;
Begin
pnlTest := TPanel.Create(Self);
pnlTest.Parent := Self;
pnlTest.Name := 'pnlTest' + IntToStr(iCount);
pnlTest.Top := 0;
//...
Inc(iCount);
End;
The component instance is stored in the Form's Components and Controls properties, since you are assigning the Form as the component's Owner and Parent, respectively.

Clear a field when another field data doesn't exist

I have a Field named "refFile" which may or may not have a Path description in it.
I need to go through the entire database and check whether Paths that are defined in "refFile" still actually exist.
Using Delphi (Pascal) this takes many, many minutes
bigDB.First;
while not(bigDB.EOF) do
begin
if Trim(bigDB.FieldByName('refFile').AsString) > '' then
begin
if not(FileExists(bigDB.FieldByName('refFile').AsString)) then
begin
bigDB.Edit;
bigDB.FieldByName('refFile').AsString:='';
bigDB.Post;
end;
end;
bigDB.Next;
end;
How do I do that in SQL?
Thank you.
You cannot check the validity of a path in SQLLite but you can filter records with something in the path and reduce the list of lines to check.
You can order the records on this field (if you have an index on it) and check only the paths you didn't checked before.
You also can use threads to do this long operation in background. Simply use TThread.Createanonymousthread(procedure begin end).Start;
You can't check the existence of a file in a plain SQLLite query. You could do that by using an UDF (User defined function) but it would be a little more complex and would requires some skills in other programming languages (Note that in that case your files should be accessible from the server, otherwise it wouldn't work).
If you are looking for a simpler solution, I think you can speed up your program by reducing the number of records resulted by the query and by improving your Delphi code in order to make it a little more efficient.
Select SQL:
Use length and trim functions due to reduce the number of records to be verified by your Delphi code.
select refFile
from myTable
where (refFile is not null) and (length(trim(refFile)) > 0)
Delphi:
Call TDataSet.FieldByName only once.
Try using TDataSet.DisableControls and TDataSet.EnableControls (In this way, some dataset's components are faster, even if the dataset component is not linked to any control).
var
Fld : TField;
begin
BigDB.DisableControls();
try
Fld := BigDB.FieldByName('refFile');
BigDB.First;
while not(BigDB.Eof) do
begin
if not(FileExists(Fld.AsString)) then
begin
BigDB.Edit;
Fld.AsString := '';
BigDB.Post;
end;
BigDB.Next;
end;
finally
BigDB.EnableControls();
end;
Furthermore, you could consider these other optimizations:
If the refFile field contains the same value multiple times, you could sort the query by the refFile field and change the Delphi code in order to verify each filename only once. (You can do that by storing the last value and the result of the FileExists function).
You can run your code asyncronusly by using the TThread class. In this way your application won't freeze and it could be faster.
For example with FireDAC it's extremely easy to create user defined functions. If you're using it, try something like this. It could save some time because the engine doesn't need to fetch the resultset to the client application:
uses
FireDAC.Phys.SQLiteWrapper;
type
TForm1 = class(TForm)
Button1: TButton;
FDQuery1: TFDQuery;
FDConnection1: TFDConnection;
FDGUIxWaitCursor1: TFDGUIxWaitCursor;
procedure FormCreate(Sender: TObject);
procedure Button1Click(Sender: TObject);
private
FValidator: TSQLiteFunction;
procedure ValidateFile(AFunc: TSQLiteFunctionData; AInputs: TSQLiteInputs;
AOutput: TSQLiteOutput; var AUserData: TObject);
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
FDConnection1.Open;
FValidator := TSQLiteFunction.Create((TObject(FDConnection1.CliObj) as TSQLiteDatabase).Lib);
FValidator.Args := 1;
FValidator.Name := 'FileExists';
FValidator.OnCalculate := ValidateFile;
FValidator.InstallAll;
end;
procedure TForm1.Button1Click(Sender: TObject);
begin
FDQuery1.SQL.Text :=
'UPDATE MyTable SET FileName = NULL WHERE ' +
'FileName IS NOT NULL AND NOT FileExists(FileName)';
FDQuery1.ExecSQL;
end;
procedure TForm1.ValidateFile(AFunc: TSQLiteFunctionData; AInputs: TSQLiteInputs;
AOutput: TSQLiteOutput; var AUserData: TObject);
begin
AOutput.AsBoolean := FileExists(AInputs[0].AsString);
end;
Or simply drop the TFDSQLiteFunction component, fill out the FunctionName property with name of the function, write OnCalculate event handler similar to the above and enable the component by setting the Active property.

How do I refer to components created at runtime rather than in the form designer?

i have a little problem. I'm trying to create in Delphi7 a list of components at run-time and to resize them with form's .OnResize event but no use... i can't figure out how to do it.
Here's my code:
procedure TForm1.Button1Click(Sender: TObject);
var
//ExtCtrls
panel: TPanel;
memo: TMemo;
splitter: TSplitter;
list: TListBox;
begin
panel := TPanel.Create(Self);
list := TListBox.Create(Self);
splitter := TSplitter.Create(Self);
memo := TMemo.Create(Self);
with panel do
begin
Parent := Form1;
BevelOuter := bvNone;
Top := 12;
Left := 12;
Height := Form1.Clientheight - 24;
Width := Form1.Clientwidth - 24;
end;
with list do
begin
Parent := panel;
Align := alClient;
Top := 0;
Height := panel.Height;
end;
with splitter do
begin
Parent := panel;
Top := 0;
Width := 12;
Align := alLeft;
end;
with memo do
begin
Parent := panel;
Top := 0;
Left := 0;
Width := round(panel.Width / 4) * 3;
Height := panel.Height;
Align := alLeft;
end;
end;
Do i have to somehow register their names in order to use them in form's event? Or maybe, to create a class and include them?
Any kind of help is really appreciated! Thank you in advance.
Your variables are local to the procedure where they are created so you can't refer to them using those variables when outside that procedure. The solution is to make them fields of the form class.
type
TForm1 = class(TForm)
procedure FormResize(Sender: TObject);
procedure Button1Click(Sender: TObject);
private
FPanel: TPanel;
FMemo: TMemo;
FSplitter: TSplitter;
FList: TListBox;
end;
Then your FormResize event handler can refer to them.
procedure TForm1.FormResize(Sender: TObject);
begin
if Assigned(FPanel) then
begin
...
end;
end;
Don't forget to remove the local variables from Button1Click and use the fields instead.
procedure TForm1.Button1Click(Sender: TObject);
begin
FPanel := TPanel.Create(Self);
...
end;
Although David's answer is also very correct, I thought I would take a moment and go into some more detail. By the looks of it, you seem to be very new with Delphi. There is a very common issue with beginners, which David doesn't address in his answer, pertaining to creating and freeing these objects. Any and every time you ever call 'Create' on a class, at some point, when you're done with it, you have to also 'Free' that class. Failure to free anything will result in a memory leak, and no one wants that. Freeing is just as simple as creating - until you get into the subject of keeping a list of objects (which you don't need right now).
Let's say you wanted to create a text box (TEdit) control and place it in the center of your form. Now first of all, the Delphi IDE allows you to simply drop these controls in your form, just making sure you know. You don't necessarily need to create/free them yourself, unless there's some special scenario. But doing this is dangerous. For the sake of this example, we're assuming that this TEdit control will be there for the entire duration of your application.
First, you need to declare a variable somewhere for this control. The most reasonable place for this is inside the class where it will be used (in this case, your form which we'll call Form1). When working with variables (aka Fields) in your form, make sure you do not put anything above the private section. Everything above private is intended for auto-generated code by Delphi for anything which has been dropped (and is visual) in your form. Otherwise, any manually created things must go under either private or under public. The public area would be a good place for your control...
type
TForm1 = class(TForm)
private
public
MyEdit: TEdit;
end;
Now that it's declared, we have to create (and free) it. It's a good practice that any and every time you ever create something, that you immediately put the code to also free it before you continue working. Make an event handler for your form's OnCreate and OnDestroy events...
procedure TForm1.FormCreate(Sender: TObject);
begin
MyEdit:= TMyEdit.Create(nil);
MyEdit.Parent:= Self;
MyEdit.Left:= (ClientWidth div 2) - (Width div 2);
MyEdit.Top:= (ClientHeight div 2) - (Height div 2);
end;
procedure TForm1.FormDestroy(Sender: TObject);
begin
if assigned(MyEdit) then MyEdit.Free;
end;
If this object is not created (before creation or after destruction), then you will get an "Access Violation" when trying to use it. This is because your application tries to access an area of the computer's memory which is not allocated or not matching with the type you meant to get.
Well, that's the basics to fix your scenario. However one more thing to show you. Suppose you need to just create an object for a short time, for the duration of a procedure. There's a different approach for this. In your code above, you declared your variable directly within the procedure. This example will show you when it is necessary to do this...
procedure TForm1.Button1Click(Sender: TObject);
var
MyObject: TMyObject;
begin
MyObject:= TMyObject.Create;
try
MyObject.DoSomething;
Caption:= MyObject.GetSomething;
finally
MyObject.Free;
end;
end;
You see, as long as MyObject will only be used in this one call to this procedure, then you can declare it here. But if the object is expected to stay in memory after this procedure is over and done with, then things get more complicated. Again, in your case, stick with putting this in the form's class until you're more familiar with dynamically creating objects.
A final note, as mentioned above, you do have the ability to place the TEdit control directly on your form in design-time without writing your own code. If you do this, you need to remember NOT to try to create or free these ones. This is also the case when Delphi will automatically put the code above the private section - is when there's something which you're not supposed to play with.
I don't think I am eligible to "comment", so I'm phrasing this as an "answer". If you want to resize your runtime components when their parent changes size, take a good look at the Anchors property. It can save you a lot of work.

Delphi & shared datasources

In my app I have different forms that use the same datasource (so the queries are the same too), defined in a common datamodule. Question is, is there a way to know how many times did I open a specific query? By being able to do this, I could avoid close that query without closing it "every where else".
Edit: It's important to mention that I'm using Delphi3 and it is not a single query but several.
The idea is to use the DataLinks property of the TDataSource.
But, as it is protected, you have to gain access to it. One common trick is to create a fake descendant just for the purpose of casting:
type
TDataSourceHack = class(TDataSource);
Then you use it like:
IsUsed := TDataSourceHack(DataSource1).DataLinks.Count > 0;
You can get creative using a addref/release like approach. Just create a few functions and an integer variable in your shared datamodule to do the magic, and be sure to call them..partial code follows:
TDMShared = class(tDataModule)
private
fQueryCount : integer; // set to 0 in constructor
public
function GetQuery : tDataset;
procedure CloseQuery;
end;
function TDMShared.GetQuery : tDataset;
begin
inc(fQueryCount);
if fQueryCount = 1 then
SharedDatsetQry.open;
Result := shareddatasetqry; // your shared dataset here
end;
procedure TDMShared.CloseQuery;
begin
dec(fQueryCount);
if fQueryCount <= 0 then
shareddatasetqry.close; // close only when no refs left.
end;
EDIT: To do this with multiple queries, you need a container to hold the query references, and a way to manipulate them. a tList works well for this. You will need to make appropriate changes for your TDataset descendant, as well as create a FreeAndNil function if you are using an older version of Delphi. The concept I used for this was to maintain a list of all queries you request and manipulate them by the handle which is in effect the index of the query in the list. The method FreeUnusedQueries is there to free any objects which no longer have a reference...this can also be done as part of the close query method, but I separated it to handle the cases where a specific query would need to be reopened by another module.
Procedure TDMShared.DataModuleCreate(Sender:tObject);
begin
dsList := tList.create;
end;
Function TDMShared.CreateQuery(aSql:String):integer;
var
ds : tAdoDataset;
begin
// create your dataset here, for this example using TADODataset
ds := tAdoDataset.create(nil); // self managed
ds.connection := database;
ds.commandtext := aSql;
ds.tag := 0;
Result := dsList.add(ds);
end;
function TDMShared.GetQuery( handle : integer ) : tDataset;
begin
result := nil;
if handle > dsList.count-1 then exit;
if dsList.Items[ handle ] = nil then exit; // handle already closed
result := tAdoDataset( dsList.items[ handle ]);
Inc(Result.tag);
if Result.Tag = 1 then
Result.Open;
end;
procedure TDMShared.CloseQuery( handle : integer );
var
ds : tAdoDataset;
begin
if handle > dsLIst.count-1 then exit;
ds := tAdoDataset( dsList.items[ handle ]);
dec(ds.Tag);
if ds.Tag <= 0 then
ds.close;
end;
procedure TDMShared.FreeUnusedQueries;
var
ds : tAdoDataset;
ix : integer;
begin
for ix := 0 to dsList.Count - 1 do
begin
ds := tAdoDataset(dsLIst.Items[ ix ]);
if ds.tag <= 0 then
FreeAndNil(dsList.Items[ix]);
end;
end;
procedure TDMShared.DataModuleDestroy(Sender: TObject);
var
ix : integer;
begin
for ix := 0 to dsList.count-1 do
begin
if dsLIst.Items[ix] <> nil then
FreeAndNil(dsLIst.Items[ix]);
end;
dsList.free;
end;
Ok, a completely different solution...one that should work for Delphi 3.
Create a new "Descendant Object" from your existing dataset into a new unit, and add some behavior in the new object. Unfortunately I do not have Delphi 3 available for testing, but it should work if you can find the proper access points. For example:
TMySharedDataset = class(tOriginalDataset)
private
fOpenCount : integer;
protected
procedure Internal_Open; override;
procedure Internal_Close; override;
end;
TMySharedDataset.Internal_Open;
begin
inherited Internal_Open;
inc(fOpenCount);
end;
TMySharedDataset.Internal_Close;
begin
dec(fOpenCount);
if fOpenCount <= 0 then
Inherited Internal_Close;
end;
Then just include the unit in your data module, and change the reference to your shared dataset (you will also have to register this one and add it to the palette if your using components). Once this is done, you won't have to make changes to the other units as the dataset is still a descendant of your original one. What makes this all work is the creation of YOUR overridden object.
You could have a generic TDataSet on the shared datamodule and set it on the OnDataChange, using the DataSet property of the Field parameter
dstDataSet := Field.DataSet;
This way, when you want to close the dataset, close the dataset on the datamodule, which is a pointer to the correct DataSet on some form you don't even have to know

Resources