BDE to FireDAC: pack table, regenerate index - delphi

An old program of ours uses dBase tables and an .MDX index - other systems use these tables too, so we're stuck with them. We wish to replace BDE with FireDAC in our software. It seems that BDE methods DbiRegenIndex and DbiPackTable (regenerate index and pack table, respectively) are not provided by FireDAC - is there a way to perform these functions using FireDAC?

The code below shows how to index a dBase table using the MS dBase driver. I've
used the Ado components, rather than FireDAC because it is easier to set up all
their properties in code, so you can see what I'm doing. Note that as well as CREATE INDEX
the driver also supports DROP INDEX. See e.g. https://learn.microsoft.com/en-us/sql/odbc/microsoft/create-index-for-paradox
(which is for Paradox, but works for dBase as well)
To set yourself up for this project, you need to set up an ODBC system DSN called DBFTest using
the MS dBase driver.
It should be straightforward to translate this Ado example into FireDAC.
type
TForm1 = class(TForm)
ADOConnection1: TADOConnection;
btnCreateTable: TButton;
ADOQuery1: TADOQuery;
btnOpenTable: TButton;
DataSource1: TDataSource;
DBGrid1: TDBGrid;
DBNavigator1: TDBNavigator;
btnDropTable: TButton;
btnAddIndex: TButton;
procedure FormCreate(Sender: TObject);
procedure btnAddIndexClick(Sender: TObject);
procedure btnCreateTableClick(Sender: TObject);
procedure btnDropTableClick(Sender: TObject);
procedure btnOpenTableClick(Sender: TObject);
public
procedure CreatedBaseTable;
end;
[...]
procedure TForm1.FormCreate(Sender: TObject);
begin
AdoConnection1.ConnectionString := 'Provider=MSDASQL.1;Persist Security Info=False;Data Source=DBFTest';
end;
procedure TForm1.btnAddIndexClick(Sender: TObject);
var
Sql : String;
begin
if AdoQuery1.Active then
AdoQuery1.Close;
Sql := 'create index byID on dBaseTest (ID)';
AdoConnection1.Execute(Sql);
AdoQuery1.Open;
end;
procedure TForm1.btnCreateTableClick(Sender: TObject);
begin
CreatedBaseTable;
end;
procedure TForm1.btnDropTableClick(Sender: TObject);
var
Sql : String;
begin
Sql := 'drop table dBaseTest';
AdoConnection1.Execute(Sql);
end;
procedure TForm1.btnOpenTableClick(Sender: TObject);
begin
AdoQuery1.SQL.Text := 'select * from dBaseTest';
AdoQuery1.Open;
end;
procedure TForm1.CreatedBaseTable;
var
Sql : String;
i : Integer;
begin
Screen.Cursor := crSqlWait;
Update;
try
Sql := 'create table dBaseTest(ID int, AName char(20))';
AdoConnection1.Execute(Sql);
for i := 1 to 100 do begin
Sql := Format('insert into dBaseTest(ID, AName) values(%d, ''%s'')', [i, 'Name' + IntToStr(i)]);
AdoConnection1.Execute(Sql);
end;
finally
Screen.Cursor := crDefault
end;
end;
Obviously, to "regenerate" the indexes this way, you would just drop them if they exist, handling any exceptions if they don't, and then create them again.
I don't know whether the dBase driver supports a "pack table" command, but you could probably do this yourself using an INSERT INTO ... SELECT * FROM ..." to copy the active rows into temporary table, then delete all rows from your working table, then copy them back from the temporary one.

Related

How to code a result to a selection from a combobox? [closed]

Closed. This question needs details or clarity. It is not currently accepting answers.
Want to improve this question? Add details and clarify the problem by editing this post.
Closed 2 years ago.
Improve this question
I have a TComboBox in Delphi 10.3. I have a Combobox with over 30 items. I need to code a different action for each item of the Combobox. At the moment I'm using if-else statements. As there are 30 different items the if statements are going to be way too long. Is there a quicker way to do this?
This entirely depends on your situation. It is almost impossible to answer your Q without knowing your precise scenario.
Nevertheless, here are a few ideas. Maybe they are relevant to your situation, maybe they are not.
Trivial parameterisation by index
In the best case scenario, your 30 actions can be parameterised. For instance, suppose the items of the combo box are
Show 1
Show 10
Show 100
Show 1000
...
which will display a message box with the given number. In this scenario, you don't need 30 different procedures (here each represented by a simple call to ShowMessage):
procedure TForm1.btnNextClick(Sender: TObject);
begin
case ComboBox1.ItemIndex of
0:
ShowMessage('1');
1:
ShowMessage('10');
2:
ShowMessage('100');
3:
ShowMessage('1000');
// ...
end;
end;
Instead, you should use only one procedure, but with a parameter:
procedure TForm1.btnNextClick(Sender: TObject);
begin
if ComboBox1.ItemIndex <> -1 then
ShowMessage(IntPower(10, ComboBox1.ItemIndex).ToString)
end;
Parameterisation by the associated object
If the action cannot be described by the item's index alone, you can use the object pointer associated with each item. Maybe it is enough to use it to store an integer:
procedure TForm1.FormCreate(Sender: TObject);
begin
ComboBox1.Items.BeginUpdate;
try
ComboBox1.Items.Clear;
ComboBox1.Items.AddObject('Show 51', TObject(51));
ComboBox1.Items.AddObject('Show 111', TObject(111));
ComboBox1.Items.AddObject('Show 856', TObject(856));
ComboBox1.Items.AddObject('Show 1000', TObject(1000));
finally
ComboBox1.Items.EndUpdate;
end;
end;
procedure TForm1.btnNextClick(Sender: TObject);
begin
if ComboBox1.ItemIndex <> -1 then
ShowMessage(Integer(ComboBox1.Items.Objects[ComboBox1.ItemIndex]).ToString);
end;
Otherwise, you can let it be a true pointer to some object with any amount of data (integers, strings, ...).
Unrelated procedures
The examples above all require that the procedures can be parameterised, i.e. replaced by a single procedure with a parameter. If this is not the case, if the procedures are completely unrelated, you need a different approach. But again, which approach is most suitable depends on your precise situation.
Here are a few examples.
Simple case statement
At design time, set the items to Play sound, Run Notepad, and Show Start Menu.
procedure PlaySound;
begin
MessageBeep(MB_ICONINFORMATION);
end;
procedure RunNotepad;
begin
ShellExecute(Form1.Handle, nil, 'notepad', nil, nil, SW_SHOWNORMAL)
end;
procedure ShowStartMenu;
begin
Form1.Perform(WM_SYSCOMMAND, SC_TASKLIST, 0)
end;
procedure TForm1.btnNextClick(Sender: TObject);
begin
case ComboBox1.ItemIndex of
0:
PlaySound;
1:
RunNotepad;
2:
ShowStartMenu;
end;
end;
Storing procedural pointers with the items
procedure PlaySound;
begin
MessageBeep(MB_ICONINFORMATION);
end;
procedure RunNotepad;
begin
ShellExecute(Form1.Handle, nil, 'notepad', nil, nil, SW_SHOWNORMAL)
end;
procedure ShowStartMenu;
begin
Form1.Perform(WM_SYSCOMMAND, SC_TASKLIST, 0)
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
ComboBox1.Items.BeginUpdate;
try
ComboBox1.Items.Clear;
ComboBox1.Items.AddObject('Play sound', TObject(#PlaySound));
ComboBox1.Items.AddObject('Run notepad', TObject(#RunNotepad));
ComboBox1.Items.AddObject('ShowStartMenu', TObject(#ShowStartMenu));
finally
ComboBox1.Items.EndUpdate;
end;
end;
procedure TForm1.btnNextClick(Sender: TObject);
begin
if ComboBox1.ItemIndex <> -1 then
TProcedure(ComboBox1.Items.Objects[ComboBox1.ItemIndex])();
end;
Benefit: no risk of confusing the indices; the actions are "attached" to the items.
Using a dictionary of commands
Maybe your application has a global set of commands, denoted by English words. Then you might want to use a dictionary to get the procedure associated with a word. This can be used for the combo box as well. At design time, let there be three items: beep, write, and start:
type
TForm1 = class(TForm)
ComboBox1: TComboBox;
btnNext: TButton;
procedure btnNextClick(Sender: TObject);
procedure FormCreate(Sender: TObject);
procedure FormDestroy(Sender: TObject);
private
FCommands: TDictionary<string, TProcedure>;
public
end;
procedure PlaySound;
begin
MessageBeep(MB_ICONINFORMATION);
end;
procedure RunNotepad;
begin
ShellExecute(Form1.Handle, nil, 'notepad', nil, nil, SW_SHOWNORMAL)
end;
procedure ShowStartMenu;
begin
Form1.Perform(WM_SYSCOMMAND, SC_TASKLIST, 0)
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
FCommands := TDictionary<string, TProcedure>.Create;
FCommands.Add('beep', PlaySound);
FCommands.Add('write', RunNotepad);
FCommands.Add('start', ShowStartMenu);
end;
procedure TForm1.FormDestroy(Sender: TObject);
begin
FCommands.Free;
end;
procedure TForm1.btnNextClick(Sender: TObject);
var
Cmd: TProcedure;
begin
if
(ComboBox1.ItemIndex <> -1)
and
FCommands.TryGetValue(ComboBox1.Items[ComboBox1.ItemIndex], Cmd)
then
Cmd();
end;

Change column name of a dbf file

I have this items in my dbase file (.dbf)
INDICE NOME COR ESTILO ESCALA
100 SAOJOAO 18 0,00
I need to change column name of INDICE to ID, so I use this code:
while not ADOQuery1.Eof do
begin
Adoquery1.Edit;
ADOQuery1.FieldByName('NOME').TEXT:= 'ID';
Adoquery1.Post;
ADOQuery1.Next;
end;
When I run the above I get these results:
INDICE NOME COR ESTILO ESCALA
ID SAOJOAO 18 0,00
Connection string used:
Driver={Microsoft dBASE Driver (*.dbf)};DriverID=277;Dbq=C:_workspace\projects\DBFEditor\te‌​mp
I have a system that need import dbf file and only recognize a file which have id column name.
The demo project below shows a way to do what you seem to want. I don't claim that
it's the most efficient way or the best way, but it's probably as simple as you're likely to get.
If you were wanting just to change the displayed name of a field in a Delphi application, for example in the column header of a DBGrid, you could do that by changing the DisplayLabel property of the field in question (AdoQuery1.FieldByName('INDICE').DisplayLabel := 'ID'), as I said in a comment earlier. But in your latest edit, it seems that what you actually want to do is to change the name of the INDICE column as it seen by a program reading the datafile to ID. To do that, you have to make an alteration to the on-disk structure of your .DBF file. This is what my code below does.
It uses a User DSN set up for the MS ODBC driver for dBase files as the target of the AdoConnection's connection string.
Ideally, I would have liked the find a flavour of the ALTER TABLE Sql statement
which would simply rename the INDICE column, but the MS dBase driver doesn't seem
to support that, because it generated an exception when I tried. So instead, my code works by making a copy of the table and its contents, with the INDICE column renamed to ID.
In short, the program
Creates a table MATest with a first column named INDICE and a couple of other columns and inserts a single row into it. This is just to set up a table to work from.
Creates a second table MATest2 with the same structure as the MATest one, except that the first
column is named ID rather than INDICE.
Populates the MATest2 table by copying all the rows from MATest, using an INSERT INTO Sql statement.
The important steps for what you want to do are carried out in the btnCreateTableCopyClick
procedure. Note that you will have to comment out the first two lines, which drop the
table MATest2 the first time you run the app, otherwise it will complain, cryptically,
that MATest2 can't be dropped because it doesn't exist.
I leave it to you to adapt the code as necessary to your data.
Code:
type
TForm1 = class(TForm)
ADOConnection1: TADOConnection;
btnCreateSrcTable: TButton;
ADOQuery1: TADOQuery;
btnOpenSrcTable: TButton;
DataSource1: TDataSource;
DBGrid1: TDBGrid;
DBNavigator1: TDBNavigator;
btnDropTable: TButton;
btnCreateTableCopy: TButton;
procedure btnCreateSrcTableClick(Sender: TObject);
procedure btnDropTableClick(Sender: TObject);
procedure btnOpenSrcTableClick(Sender: TObject);
procedure btnCreateTableCopyClick(Sender: TObject);
private
protected
public
procedure CreateSourceTable;
end;
[...]
procedure TForm1.btnCreateTableCopyClick(Sender: TObject);
var
Sql : String;
begin
Sql := 'drop table MATest2';
AdoConnection1.Execute(Sql);
Sql := 'create table MATest2(ID int, AName char(20), AValue char(20))';
AdoConnection1.Execute(Sql);
Sql := 'insert into MATest2 select INDICE, AName, AValue from MATest';
AdoConnection1.Execute(Sql);
end;
procedure TForm1.btnCreateSrcTableClick(Sender: TObject);
begin
CreateSourceTable;
end;
procedure TForm1.btnDropTableClick(Sender: TObject);
var
Sql : String;
begin
// Sql := 'drop table MATest';
// AdoConnection1.Execute(Sql);
end;
procedure TForm1.btnOpenSrcTableClick(Sender: TObject);
begin
AdoQuery1.Open;
end;
procedure TForm1.btnCreateTableCopyClick(Sender: TObject);
var
Sql : String;
begin
Sql := 'drop table MATest2';
AdoConnection1.Execute(Sql);
Sql := 'create table MATest2(ID int, AName char(20), AValue char(20))';
AdoConnection1.Execute(Sql);
Sql := 'insert into MATest2 select INDICE, AName, AValue from MATest';
AdoConnection1.Execute(Sql);
end;
procedure TForm1.CreateSourceTable;
var
Sql : String;
begin
Sql := 'create table MATest(INDICE int, AName char(20), AValue char(20))';
AdoConnection1.Execute(Sql);
Sql := 'insert into MATest(INDICE, AName, AValue) values(1, ''aaa'', ''vvv'')';
AdoConnection1.Execute(Sql);
end;
Obviously it would be better to generate your data with the ID fieldname in the first place and avoid all this, but presumably there is a good reason why you can't.

FireDac freezes GUI

I am working with FireDac under Delphi 10.1 Berlin.
For displaying data to the user i use data aware controls like TDBEdit.
I use TFDQuery and TDataSource to link them with the controls.
This works but long sql queries that take some time to exectute will freeze the GUI.
I am wondering how to stop the gui from freezing while performing those long running queries.
I was thinking about background threads.
On the wiki i read that FireDac can work with multithreads:
http://docwiki.embarcadero.com/RADStudio/XE6/en/Multithreading_(FireDAC)
However in embarcadero community forums thread Jeff Overcash writes:
One thing I didn't see asked or Dmitry mention is you can not have
TDataSource or LiveBindings against your background threaded queries.
If you are background threading a query that displays the results you
should disconnect the LB or DataSource, open and fetch all the data
then re establish the connection.
Those two will be trying to move the cursor on you or querying the
buffer for display while the buffer is very volatile being moved
around in a different thread.
I am wondering if someone that also uses FireDac and displays the values on a form can help me out here.
The code sample below shows one way to retrive records from an MSSql Server
in a background thread using FireDAC. This omits a few details. For example, in practice, rather than the TQueryThreads Execute opening the query only once and then terminating, you would probably want the thread's Execute to contain a while loop in which it waits on a semaphore after the call to Synchronize and then close/re-open the query to update the main thread as often as you want.
type
TForm1 = class;
TQueryThread = class(TThread)
private
FConnection: TFDConnection;
FQuery: TFDQuery;
FForm: TForm1;
published
constructor Create(AForm : TForm1);
destructor Destroy; override;
procedure Execute; override;
procedure TransferData;
property Query : TFDQuery read FQuery;
property Connection : TFDConnection read FConnection;
property Form : TForm1 read FForm;
end;
TForm1 = class(TForm)
FDConnection1: TFDConnection;
FDQuery1: TFDQuery;
DataSource1: TDataSource;
DBGrid1: TDBGrid;
DBNavigator1: TDBNavigator;
Button1: TButton;
procedure Button1Click(Sender: TObject);
procedure FormCreate(Sender: TObject);
public
QueryThread : TQueryThread;
end;
[...]
constructor TQueryThread.Create(AForm : TForm1);
begin
inherited Create(True);
FreeOnTerminate := True;
FForm := AForm;
FConnection := TFDConnection.Create(Nil);
FConnection.Params.Assign(Form.FDConnection1.Params);
FConnection.LoginPrompt := False;
FQuery := TFDQuery.Create(Nil);
FQuery.Connection := Connection;
FQuery.SQL.Text := Form.FDQuery1.SQL.Text;
end;
destructor TQueryThread.Destroy;
begin
FQuery.Free;
FConnection.Free;
inherited;
end;
procedure TQueryThread.Execute;
begin
Query.Open;
Synchronize(TransferData);
end;
procedure TQueryThread.TransferData;
begin
Form.FDQuery1.DisableControls;
Form.FDQuery1.Data := Query.Data;
Form.FDQuery1.EnableControls;
end;
procedure TForm1.Button1Click(Sender: TObject);
begin
QueryThread.Resume;
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
QueryThread := TQueryThread.Create(Self);
end;
MJN's comment about bookmarks tells you how to preserve the current data row position in the gui.
Btw, although I've often done this with TClientDataSets, putting this answer together was the first time I'd tried it with FireDAC. In terms of configuring the components, all I did was to drag the components off the Palette, "wire them together" as you'd expect and then set the FDConnection's Params and the FDQuery's Sql.

How to pass a method's nested procedure as a parameter?

Given a TForm with a TListBox on it, the following works:
procedure TForm1.FormCreate(Sender: TObject);
procedure _WorkOnListBox;
begin
ListBox.Items.Append('Test');
end;
begin
_WorkOnListBox;
end;
As does the following:
procedure TForm1.DoWithoutListBoxEvents(AProc: TProc);
begin
ListBox.Items.BeginUpdate;
try
AProc;
finally
ListBox.Items.EndUpdate;
end;
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
DoWithoutListBoxEvents(procedure
begin
LayersListBox.Items.Append('Test');
end);
end;
But the following does not:
procedure TForm1.FormCreate(Sender: TObject);
procedure _WorkOnListBox;
begin
ListBox.Items.Append('Test');
end;
begin
DoWithoutListBoxEvents(_WorkOnListBox);
end;
I get an E2555 Cannot capture symbol '_WorkOnListBox'. Why? Is there any way to get the DoWithoutListBoxEvents to work without using an anonymous procedure? Although I think it looks elegant with it, I'm trying to stay FPC compatible.
DoWithoutEvents() takes a TProc as input:
type
TProc = procedure;
Only a standalone non-class procedure and an anonymous procedure can be assigned to a TProc. _WorkOnForm is neither of those, it is a local procedure instead. A local procedure has special compiler handling that ties it to its parent's stack frame. Thus, _WorkOnForm is not compatible with TProc.

How to set different cookies for different instances of Chromium embedded browser

I'm working on an application that requires multiple embedded instances and each of this instance logins to the same external site with different authentication details.
I'm using CEF (Chromium embedded framework ) in Delphi , I have a folder that i stored the cookies on it for different instance of my browsers , So i have this code in the first unit
procedure TForm2.Button1Click(Sender: TObject);
begin
form33 := Tform3.Create(nil);
form33.Show;
end;
when the form33 created
procedure TForm3.FormCreate(Sender: TObject);
var
CookieManager: ICefCookieManager;
folder: string;
begin
Randomize;
Chromium1.SetBrowserID(Random(1244));
folder := Randomtext(5);
if DirectoryExists(folder) = False then
MkDir(folder);
CookieManager := TCefCookieManagerRef.Global;
path := ExtractFilePath(Application.ExeName) + folder;
CookieManager.SetStoragePath(path, true);
end;
The problem is that when i open two or more of instances of the form33 , i can't have for each browser in each form33 a specific cookie ...
You should create a new manager for each instance by TCefCookieManagerRef.New(Path) and store the returned reference into some private field of your form. Then you need to return the stored reference to the Result parameter of the OnGetCookieManager event method:
type
TForm1 = class(TForm)
Chromium1: TChromium;
procedure FormCreate(Sender: TObject);
procedure Chromium1GetCookieManager(Sender: TObject;
out Result: ICefCookieManager);
private
FCookieManager: ICefCookieManager2;
end;
implementation
procedure TForm1.FormCreate(Sender: TObject);
begin
FCookieManager := TCefCookieManagerRef.New('C:\UniquePathToTheCookieStorage');
end;
procedure TForm1.Chromium1GetCookieManager(Sender: TObject;
out Result: ICefCookieManager);
begin
Result := FCookieManager;
end;

Resources