dynamically create popup menu tree from sql server table in Delphi - sql

I have a table like this:
id parent_id name
1 1 Root
2 1 Car
3 1 Plane
4 2 BMW
5 4 CLK
How can I dynamically create popup menu with all subitems in Delphi?
This is how it should look like:

Assuming root element has NULL as Parent_ID you can issue the request
Select ID, Parent_ID, Name from all_my_menus
order by Parent_ID nulls first, ID
where Menu_ID = :MenuIDParameter
1 <NULL> Root
8 <NULL> another root
2 1 Car
4 1 Plane
3 2 BMW
5 4 CLK
You would also cache in-memory created menu items: var MI_by_id: TDictionary<integer, TMenuItem>;
The traversing through the results would look like
var MI: TMenuItem;
MI_by_id: TDictionary<integer, TMenuItem>;
begin
MI_by_id := TDictionary<integer, TMenuItem>.Create;
try
While not Query.EOF do begin
MI := TMenuItem.Create(Self);
MI.Caption := Query.Fields[2].AsString;
MI.Tag := Query.Fields[0].AsInteger; // ID, would be helpful for OnClick event
MI.OnClick := ...some click handler
if Query.Fields[1].IsNull {no parent}
then MainMenu.Items.Add(MI)
else MI_by_id.Items[Query.Fields[1].AsInteger].Add(MI);
MI_by_id.Add(MI.Tag, MI); //save shortcut to potential parent for future searching
Query.Next;
end;
finally
MI_by_id.Free;
end;
end;
Actually, since we made sort upon Parent_ID on the query, all the children for given parent make single continuous list, so could be better to remove populated parents from the dictionary after we populated last child (i.e. after parent_ID got new value) and caching previously found parent otherwise in another local variable (instead of making yet another search through the dictionary).
However reasonable size for human-targeted menu should be much less to worth this. But you have to understand this approach most probably scales as O(n*n) thus would start loose speed very fast as the table grows.
http://docwiki.embarcadero.com/Libraries/XE3/en/Vcl.Menus.TMenuItem.Add
http://docwiki.embarcadero.com/CodeExamples/XE2/en/Generics.Collections.TDictionary_(Delphi)
Note: this also requires that for every non-root element ID > ParentID (put CHECK CONSTRAINT on the table)
1 <NULL> Root
8 <NULL> another root
7 1 Plane
3 4 BMW
4 7 CLK
5 8 Car
This would lead to BMW tied to create before its parent CLK created.
Violation for that conditions can be overcome by few means:
recursive load: select <items> where Parent_id is null, then for each of the added menu items do select <items> where Parent_id = :current_memuitem_id and so on that. This is like VirtualTreeView would work
ask SQL server to sort and flatten the tree - this is usually called self-recursive SQL selection and is server-dependant.
introduce one more collection variable - menu items w/o parent. After each new item added to the menu this collection should be searched if there are pending children to extract from it and move into the newly created parent.

Too many solutions for such a simple problem. Too bad you got ordered ID's because without ordered ID's things would have been more fun. Here's my own solution. On an empty form drop a button, a TClientDataSet and a TPopupMenu. Make the form's PopupMenu = PopupMenu1 so you can see the result. Add this to Button1.OnClick:
Note: I'm intentionally using TClientDataSet and not a real Query. This question is not about the query and this solution works with whatever TDataSet descendant you throw at it. Just make sure the result set is ordered on id, or else you could see the child nodes before the parents. Also note, half the code is used to fill up the ClientDataSet with the sample data in the question!
procedure TForm16.Button1Click(Sender: TObject);
var Prev: TDictionary<Integer, TMenuItem>; // We will use this to keep track of previously generated nodes so we do not need to search for them
CurrentItem, ParentItem: TMenuItem;
begin
if not ClientDataSet1.Active then
begin
// Prepare the ClientDataSet1 structure
ClientDataSet1.FieldDefs.Add('id', ftInteger);
ClientDataSet1.FieldDefs.Add('parent_id', ftInteger);
ClientDataSet1.FieldDefs.Add('name', ftString, 100);
ClientDataSet1.CreateDataSet;
// Fill the dataset
ClientDataSet1.AppendRecord([1, 1, 'Root']);
ClientDataSet1.AppendRecord([2, 1, 'Car']);
ClientDataSet1.AppendRecord([3, 1, 'Plane']);
ClientDataSet1.AppendRecord([4, 2, 'BMW']);
ClientDataSet1.AppendRecord([5, 4, 'CLK']);
end;
// Clear the existing menu
PopupMenu1.Items.Clear;
// Prepare the loop
Prev := TDictionary<Integer, TMenuItem>.Create;
try
ClientDataSet1.First; // Not required for a true SQL Query, only required here for re-entry
while not ClientDataSet1.Eof do
begin
CurrentItem := TMenuItem.Create(Self);
CurrentItem.Caption := ClientDataSet1['name'];
if (not ClientDataSet1.FieldByName('parent_id').IsNull) and Prev.TryGetValue(ClientDataSet1['parent_id'], ParentItem) then
ParentItem.Add(CurrentItem)
else
PopupMenu1.Items.Add(CurrentItem);
// Put the current Item in the dictionary for future reference
Prev.Add(ClientDataSet1['id'], CurrentItem);
ClientDataSet1.Next;
end;
finally Prev.Free;
end;
end;

Try this
procedure TForm1.MyPopup(Sender: TObject);
begin
with Sender as TMenuItem do ShowMessage(Caption);
end;
procedure TForm1.Button1Click(Sender: TObject);
var
MyItem,MySubItem1: TMenuItem;
begin
Inc(Num);
MyItem:=TMenuItem.Create(Self);
MySubItem1:=TMenuItem.Create(Self);
MyItem.Caption:='Hello'+IntToStr(Num);
MySubItem1.Caption:='Good Bye'+IntToStr(Num);
MainMenu1.Items.Add(MyItem);
MainMenu1.Items[0].Insert(num-1,MySubItem1);
MyItem.OnClick:=MyPopUp;
MySubItem1.OnClick:=MyPopUp;
end;
Taken from http://www.greatis.com/delphicb/tips/lib/components-addmenuitem.html

This solution requires parent_id of root to be 0, tested with
Select 1 as ID, 0 as Parent_ID, 'Root' as Name
union
Select 2, 1, ' Car'
union
Select 3 , 1, 'Plane'
union
Select 4, 2, 'BMW'
union
Select 5, 4, 'CLK'
should by optimized, have just a lack of time ...
Function GetMenu(pop:TPopupmenu;ID:Integer):TMenuItem;
var
i:Integer;
Function CheckItem(mi:TMenuItem):TMenuItem;
var
i:Integer;
begin
Result := nil;
if mi.Name = 'DYN_' + INtToStr(ID) then Result := mi
else for i := 0 to mi.Count-1 do
if not Assigned(Result) then Result := CheckItem(mi[i]);
end;
begin
Result := nil;
for i := 0 to pop.Items.Count-1 do
begin
if not Assigned(Result) then Result := CheckItem(pop.Items[i]);
if Assigned(Result) then Break;
end;
end;
Function InsertMenuItem(pop:TPopupMenu;mi:TMenuItem;ID:Integer;Const caption:String):TMenuItem;
begin
Result := TMenuItem.Create(pop);
Result.Caption := caption;
Result.Name := 'DYN_' + INtToStr(ID) ;
if not Assigned(mi) then pop.Items.Add(Result) else mi.Add(Result);
end;
Function AddMenuItem(pop:TPopupmenu;ID:Integer;Ads:TDataset):TMenuItem;
begin
Ads.Locate('ID',ID,[]);
Result := GetMenu(pop,id);
if (not Assigned(Result)) then
begin
if (Ads.FieldByName('parent_ID').AsInteger<>0) then
begin
result := AddMenuItem(pop,Ads.FieldByName('parent_ID').AsInteger,Ads);
Ads.Locate('ID',ID,[]);
end;
Result := InsertMenuItem(pop,Result,ID,Ads.FieldByName('Name').AsString);
end;
Ads.Locate('ID',ID,[]);
end;
procedure TForm1.Button1Click(Sender: TObject);
begin
while not ADS.Eof do
begin
AddMenuItem(Popupmenu1,ads.FieldByName('ID').AsInteger,Ads);
Ads.Next
end;
end;

Interesting conundrum ...another late night thought, a practical answer for re-use :)
Make a derived component:
type
TCascadeMenuItem = class(TMenuItem)
private
Id: Integer;
public
function AddItem(const ToId, WithId: Integer; AName: string): Boolean;
end;
with code
function TCascadeMenuItem.AddItem(const ToId, WithId: Integer; AName: string): Boolean;
var
i: Integer;
cmi: TCascadeMenuItem;
begin
if ToId = Id then
begin
cmi := TCascadeMenuItem.Create(Owner);
cmi.Caption := AName;
cmi.Id := WithId;
Add(cmi);
Result := True;
end
else begin
i := 0;
Result := False;
while (i < Count) and (not Result) do
begin
Result := TCascadeMenuItem(Items[i]).AddItem(ToId,WithId, ANAme);
inc(i);
end;
end;
end;
Main form, Assumes your data:
procedure TForm4.Button2Click(Sender: TObject);
var
mi: TCascadeMenuItem;
i: Integer;
Added: Boolean;
begin
cds1.First;
while not cds1.Eof do
begin
i := 0;
Added := False;
while (i < pup.Items.Count) and (not Added) do
begin
Added := TCascadeMenuItem(pup.Items[i]).AddItem(cds1Parent_Id.AsInteger, cds1id.AsInteger, cds1name.AsString);
inc(i);
end;
if not Added then
begin // new root
mi := TCasCadeMenuItem.Create(Self);
mi.Caption := cds1name.AsString;
mi.id := cds1Parent_Id.AsInteger;
pup.Items.Add(mi);
end;
cds1.Next;
end;
end;
You could derive a TCascasePopupMenu and put it on the palette :)

Related

Is possible to set filter option in SMDBGrid to CaseInsensitive?

I have SMDBGrid component with show filter bar option set to true, but filter just working in case-sensitive mode
1.Try with lower case
2.Try with upper case
I have tried to insert the code in SMDBgrid.pas like this
procedure TSMDBGrid.ApplyFilter;
var
TempCol: Integer;
begin
if (eoFilterAutoApply in ExOptions) then
begin
TempCol := LeftCol;
BeginUpdate;
try
if DataLink.Active then
begin
DataLink.DataSet.CheckBrowseMode;
DataLink.DataSet.Filtered := False;
DataLink.DataSet.OnFilterRecord := nil;
DataLink.DataSet.OnFilterRecord := OnFilterRecord;
DataLink.DataSet.FilterOptions := [foCaseInsensitive]; <-- this the inserted code
DataLink.DataSet.Filtered := not FilterIsEmpty();//True;
end;
finally
LeftCol := TempCol;
EndUpdate;
end;
end;
if Assigned(OnFilterChanged) then
OnFilterChanged(Self);
end;
But no luck, Is posible filter the record ignoring the case?
PS:
I use Delphi 2009
You may use the OnAccentStringConvert event to transform the value for filter in column before compare:
begin
Result := UpperCase(S)
end;
Looks like I cope with this problem too. Trying to find any solution for Delphi XE 10.3 community edition and wrote to author of SMDBGrid and he found workaround.
Please use SQL ADOQuery as follows.
SELECT UPPER(field) FROM your_table
then use event OnAccentStringConvert and uppercase S String as follows:
function TYourFormName.DBGrridNameAccentStringConvert(Sender: TObject; const S: string): string;
begin
Result := UpperCase(S)
end;
This works very ugly, but at least works. Or you may just create filter by yourself for every table.

How to bulk copy the values from a TSQLQuery record to a TdxMemData record?

Just to make it clear, I do not want to copy the entire TSQLQuery into the TdxMemData, as I would use memds.CopyFromDataSet(qry) for that.
I am interating through each record from the TSQLQuery, and I may or may not be adding a record(s) to the TdxMemData. Generally the record in memds matches that in qry, but sometimes the values are altered and sometimes additional records are added to memds. My example did not make this clear since all it seemed to do was copy over each record.
So given an active record in the TSQLQuery, I want to copy over the values into an active editable record in the TdxMemData.
The following code works in so far as it creates a copy of the record:
qry := TSQLQuery.Create(nil);
memds := TdxMemData.Create(nil);
try
qry.SQLConnection := cn;
qry.Text := 'SELECT Field1, Field2, Field3 FROM Table1';
qry.Open
memds.CreateFieldsFromDataSet(qry);
memds.Open;
while not qry.Eof do
begin
if {some condition} then
begin
memds.Append;
for i := 0 to qry.FieldCount-1 do
memds.Fields[i+1].Value := qry.Fields[i].Value; //First field is RecID
//Do something with the current memds record
end
else if {some other condition} then
begin
memds.Append;
//change values
memds.Append;
//change values
memds.Append;
//change values
end
else if {a third condition} then
; //Skip any work on memds
qry.next;
end;
qry.Close;
//Do something with memds
memds.Close;
finally
memds.Free;
qry.Free;
end;
Is there a better way? I had looked at AppendRecord but creating the array of TVarRec doesn't seem to be straightforward.
EDIT:
Let's use these examples with very simplified criteria. Note that the actual conditions that determine how many records to append and the changes to the field values in the destination are complex and not in any database.
Method 1:
While not tblSource.Eof do
Begin
If (iCondition = 1) Then
Begin
// Add one record
tblDestination.Append;
tblDestination.FieldByName('Field1').Value := tblSource.FieldByName('Field1').Value;
tblDestination.FieldByName('Field2').Value := tblSource.FieldByName('Field2').Value;
tblDestination.FieldByName('Field3').Value := tblSource.FieldByName('Field3').Value;
tblDestination.FieldByName('Field4').Value := tblSource.FieldByName('Field4').Value;
tblDestination.FieldByName('Field5').Value := tblSource.FieldByName('Field5').Value;
if bSomethingCondition then
tblDestination.FieldByName('Field4').Value := 'Something';
End
Else If (iCondition = 2) Then
Begin
// Add two records
tblDestination.Append;
tblDestination.FieldByName('Field1').Value := tblSource.FieldByName('Field1').Value;
tblDestination.FieldByName('Field2').Value := tblSource.FieldByName('Field2').Value;
tblDestination.FieldByName('Field3').Value := tblSource.FieldByName('Field3').Value;
tblDestination.FieldByName('Field4').Value := tblSource.FieldByName('Field4').Value;
tblDestination.FieldByName('Field5').Value := tblSource.FieldByName('Field5').Value;
if bAnotherThingCondition then
tblDestination.FieldByName('Field4').Value := 'Another thing';
tblDestination.Append;
tblDestination.FieldByName('Field1').Value := tblSource.FieldByName('Field1').Value;
tblDestination.FieldByName('Field2').Value := tblSource.FieldByName('Field2').Value;
tblDestination.FieldByName('Field3').Value := tblSource.FieldByName('Field3').Value;
tblDestination.FieldByName('Field4').Value := tblSource.FieldByName('Field4').Value;
tblDestination.FieldByName('Field5').Value := tblSource.FieldByName('Field5').Value;
if bSomethingElseCondition then
tblDestination.FieldByName('Field4').Value := 'Something else';
End
Else If (iCondition = 0) Then
Begin
// Add no records
End;
tblSource.Next;
End;
Since the number of fields in the source and destination tables can vary, hard-coding field names as in Method 1 is not suitable.
Method 2:
While not tblSource.Eof do
Begin
If (iCondition = 1) Then
Begin
// Add one record
tblDestination.Append;
for i := 0 to tblSource.FieldCount-1 do
tblDestination.Fields[i+1].Value := tblSource.Fields[i].Value;
if bSomethingCondition then
tblDestination.Fields(iSomethingConditionFieldIndex).Value := 'Something';
End
Else If (iCondition = 2) Then
Begin
// Add two records
tblDestination.Append;
for i := 0 to tblSource.FieldCount-1 do
tblDestination.Fields[i+1].Value := tblSource.Fields[i].Value;
if bAnotherThingCondition then
tblDestination.Fields(iAnotherThingConditionFieldINdex).Value := 'Another thing';
tblDestination.Append;
for i := 0 to tblSource.FieldCount-1 do
tblDestination.Fields[i+1].Value := tblSource.Fields[i].Value;
if bSomethingElseCondition then
tblDestination.Fields(iSomethingElseConditionFieldIndex).Value := 'Something else';
End
Else If (iCondition = 0) Then
Begin
// Add no records
End;
tblSource.Next;
End;
While method 2 above does work, and is the way it is currently done, this question is whether there is a way to pass the variant array of field values from tblSource to tblDestination using AppendRecord.
Instead of this:
// Add one record
tblDestination.Append;
for i := 0 to tblSource.FieldCount-1 do
tblDestination.Fields[i+1].Value := tblSource.Fields[i].Value;
if bSomethingCondition then
tblDestination.Fields(iSomethingConditionFieldIndex).Value := 'Something';
Do this:
tblDestination.AppendRecord({tblSource fields var array);
if bSomethingCondition then
tblDestination.Fields(iSomethingConditionFieldIndex).Value := 'Something';
Of course, it might be that there is no answer, and that the method I currently employ is the best solution.
Try the following
Query1.Open();
dxMemData1.AddFieldsFromDataSet(Query1);
dxMemData1.Open;
dxMemData1.LoadFromDataSet(Query1);
Creating an array of TVarRec isn't difficult, and AppendRecord may indeed help. The following code adds a record to a TClientDataSet (named CDS for brevity) that has 4 fields of type string, float, boolean, and string in that order:
CDS.AppendRecord(['Smith', 123.45, False, 'Test text']);
Note that you have to create a value for every single field (column) in the dataset, in the order that they exist in the FieldDefs collection, or you'll get an exception.
(Of course, the real question is why you're returning extra rows and iterating through them, instead of testing the conditions in your SQL statement WHERE clause and only returning the rows you actually need. This can almost always be done using parameters.)
If you don't want to use AddFieldsFromDataSet -> LoadFromDataSet , and want to load fields and records manually, you also can do it. You can create fields programmatically and append records by iterations. Look example:
MD.Fields.Clear;
MD.FieldDefs.Clear;
MD.Close;
with MD.FieldDefs.AddFieldDef do
begin
Name := 'UserID';
DataType := TFieldType.ftInteger;
CreateField(MD);
Name := 'GridName';
DataType := TFieldType.ftString;
Size := 255;
CreateField(MD);
Name := 'TemplateGrid';
DataType := TFieldType.ftBlob;
CreateField(MD);
end;
MD.Close;
MD.Open;
MD.Append;
MD.FieldByName('UserID').AsInteger := 1;
MD.FieldByName('GridName').AsString := Self.Name
TBlobField(MD.FieldByName('TemplateGrid')).LoadFromStream(LStream);
MD.Post;
You can easily modify it to load fields with names and types from your dataset as they are.

delphi - Save multiple records at once

Dont know how to formulate this exactly so bear with me please... I am saving text from a memo to a database with date selected in the PlannerCalendar1. Since I can select multiple dates in the PlannerCalendar1, how can I post the value of the memo to all dates selected in the PlannerCalendar1?So when I click 'save' the contents of the memo gets saved to all selected dates.Database is SQLite. The table also has an ID field which is autoinc (primary).PlannerCalendar is from the set of TMS components.
procedure TForm1.cxButton1Click(Sender: TObject);
var i:integer;
begin
with UniQuery1 do
begin
UniQuery1.SQL.Text:='INSERT INTO LOG (DATE,PERSON,DONE,TIME) VALUES (:a1,:a2,:a3,:a4)';
UniQuery1.PARAMS.ParamByName('A1').VALUE := PlannerCalendar1.Date;
UniQuery1.PARAMS.ParamByName('A2').VALUE := cxmemo1.Lines.text ;
UniQuery1.PARAMS.ParamByName('A3').VALUE := (0);
UniQuery1.PARAMS.ParamByName('A4').Value := AdvOfficeStatusBar1.Panels[0].Text;
UniQuery1.ExecSQL;
cxmemo1.Clear;
UniTable1.Refresh;
Tried this at the end but it wont work :
with plannercalendar1.Dates do
begin
for i := 0 to -1 do
begin
UniQuery1.PARAMS.ParamByName('A1').VALUE :=plannercalendar1.dates.Add + i ;
UniQuery1.ExecSQL;
end;
I have no idea what a PlannerCalendar is, but presumably there's some way to get at the list of dates that are selected. You want to do something like this:
UniQuery1.SQL.Text:='INSERT INTO LOG (DATE,PERSON,DONE,TIME) VALUES (:a1,:a2,:a3,:a4)';
UniQuery1.PARAMS.ParamByName('A2').VALUE := cxmemo1.Lines.text ;
UniQuery1.PARAMS.ParamByName('A3').VALUE := (0);
UniQuery1.PARAMS.ParamByName('A4').Value := AdvOfficeStatusBar1.Panels[0].Text;
for i := 0 to PlannerCalendar1.NumberOfDatesSelected-1 do begin
UniQuery1.PARAMS.ParamByName('A1').VALUE := PlannerCalendar1.SelectedDate[i];
UniQuery1.ExecSQL;
end;
Of course, NumberOfDatesSelected and SelectedDate are wild guesses. You'll need to find out what they're really called.
You need to use the Planner's SelectionToAbsTime method :-
Var
lStart, lEnd : TDateTime;
Begin
Planner1.SelectionToAbsTime(lStart, lEnd);
For I := Trunc(lStart) To Trunc(lEnd) Do
SaveMemosForDate(I);
End;

DelphiXE2 : How to Display Data From Database to NextGrid

I've searched around, ask my local teacher at my school. But there is no way i could find how to display data from database to NextGrid. I used SELECT * but it doesnt shown on the next grid.
ZQuery1.Close;
ZQuery1.SQL.Clear;
ZQuery1.SQL.Text := 'SELECT * FROM tb_siswa';
ZQuery1.Open;
NextGrid1.ClearRows;
x:= 0;
while not ZQuery1.Eof do
begin
NextGrid1.AddRow();
NextGrid1.Cell[0,x].AsString := ZQuery1.Fields[0].AsString;
NextGrid1.Cell[1,x].AsString := ZQuery1.Fields[1].AsString;
NextGrid1.Cell[2,x].AsString := ZQuery1.Fields[2].AsString;
NextGrid1.Cell[3,x].AsString := ZQuery1.Fields[3].AsString;
NextGrid1.Cell[4,x].AsString := ZQuery1.Fields[4].AsString;
inc(x);
ZQuery1.Next;
end;
ZQuery1.Close;
Any support please?
On the nex suite component page, you have the NxDataCellSource component. Place it on the form. Place the next grid on the form. In the NxDataCellSource properties, change the datasource to the one you are using, ZQuery1. Change the Associate properties to NextGrid1 (or other name you have associated to nextgrid). Set active to true and enjoy.
i'm use NextGrid1.RowCount-1 this code,dont need manual increment value
while not ZQuery1.Eof do
begin
NextGrid1.AddRow();
NextGrid1.Cell[0,NextGrid1.RowCount-1].AsString := ZQuery1.Fields[0].AsString;
Next;
end;

Variable structure for database results

A lot of times when we query the database, we just need one column with varchar.
So I've made a nice function for querying the database and putting the results in a stringlist:
function Getdatatostringlist(sqlcomponent, sqlquery: string): TStringlist;
What I'm looking for now is basically the same function but for results with multiple columns where you don't know in advance what type the data is, be it varchar, int, datetime.
What kind of datastructure would be good to use here.
The reason I want this is that I try not to work on open datasets. I like much more to fetch all results into a temporary structure, close the dataset and work on the results.
After Kobiks reply about using in Memory datasets I came up with the following, it's fast put together to test the concept:
procedure TForm1.Button2Click(Sender: TObject);
var
MyDataSet : TAdoDataSet;
begin
MyDataSet := GetDataToDataSet('SELECT naam FROM user WHERE userid = 1', ADOConnection1);
try
Form1.Caption := MyDataSet.FieldByName('naam').AsString;
finally
MyDataSet.free;
end;
end;
function TForm1.GetDataToDataSet(sSql: string; AdoConnection: TADOConnection): TAdoDataSet;
begin
Result := TAdoDataSet.Create(nil);
Result.LockType := ltBatchOptimistic;
Result.Connection := AdoConnection;
Result.CommandText := sSql;
Result.Open;
Result.Connection := nil;
end;
I think this is something to build on.
You should use any disconnected in-memory TDataSet descendant, such as TClientDataSet.
Do not attempt to re-invent the wheel by storing a record-set in some new "Variant" structure. A TClientDataSet already contains all features you need to manipulate a "temporary" data structure.
Here is how you create a TClientDataSet structure:
cds.FieldDefs.Add('id', ftInteger);
cds.FieldDefs.Add('name', ftString, 100);
// ...
// create it
cds.CreateDataSet;
// add some data records
cds.AppendRecord([1, 'Foo']);
cds.AppendRecord([2, 'Bar']);
Many TDataSets has an ability to be used as an in-memory (client) datasets depending on the provider and LockType, for example a TADODataSet with LockType=ltBatchOptimistic could fetch results-set from the server, and then remain disconnected.
For exchanging Data with Excel this structure is usefull, might be useful for other purposes.
Function GetDatasetasDynArray(Ads: TDataset; WithHeader: Boolean = true): Variant;
// 20130118 by Thomas Wassermann
var
i, x, y: Integer;
Fields: Array of Integer;
begin
x := 0;
y := Ads.RecordCount;
if WithHeader then
inc(y);
SetLength(Fields, Ads.FieldCount);
for i := 0 to Ads.FieldCount - 1 do
if Ads.Fields[i].Visible then
begin
Fields[x] := i;
inc(x);
end;
SetLength(Fields, x);
Result := VarArrayCreate([0, y - 1 , 0, length(Fields) - 1], VarVariant);
y := 0;
if WithHeader then
begin
for i := Low(Fields) to High(Fields) do
begin
Result[y, i] := Ads.Fields[Fields[i]].DisplayLabel;
end;
inc(y);
end;
try
Ads.DisableControls;
Ads.First;
while not Ads.EOF do
begin
for i := Low(Fields) to High(Fields) do
begin
Result[y, i] := Ads.Fields[Fields[i]].Value;
end;
Ads.Next;
inc(y);
end;
finally
Ads.EnableControls;
end;
end;
procedure TForm1.Button1Click(Sender: TObject);
var
DynArray:Variant;
begin
DynArray := GetDatasetasDynArray(Adodataset1,true);
//DynArray[0,x] Header or First row
//DynArray[1,x] First row or SecondRow
Excel.Range.Value := DynArray;
end;
Why don't you like working with open datasets? They usually do not block the server. Copying the data from the dataset to whatever you want is extra overhead which is most likely not necessary.
A dataset provides exactly the functionality you want: A matrix with variable columns and rows.
EDIT: However, if you have iterate through the dataset often, you should consider creating a class holding the relevant information and then copy the data into a generic list, dictionary, tree or whatever you need as fast lookup structure.
Of course you could think of building something smart which can be as flexible as a dataset but: The more general things get, the poorer the performance (usually).