Delphi - modify variable from DLL - delphi

I want to make simple program that sets Edit1.Text to "6" (for example, but with usage of DLL - thats important). Here's the code:
Unit:
unit Unit1;
interface
uses
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls;
type
TForm1 = class(TForm)
Edit1: TEdit;
Button1: TButton;
procedure Button1Click(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;
var
Form1: TForm1;
a:integer;
implementation
procedure test; external 'lib.dll' name 'test';
{$R *.dfm}
procedure TForm1.Button1Click(Sender: TObject);
begin
test;
Edit1.Text:=Inttostr(a);
end;
end.
And the DLL file:
library lib;
uses
Winapi.Windows, System.SysUtils;
var
a:integer;
procedure test;
begin
a:=6;
end;
exports
test;
{$R *.res}
begin
end.
The problem is, that Edit1.Text is still 0. Can you help me, please?

You've got two different variables, one in the DLL and one in the executable. That they are both named a is incidental. Setting one has no impact on the other.
Make the DLL export a function that returns the value:
function GetValue: Integer; stdcall;
begin
Result := 6;
end;
Import it like this:
function GetValue: Integer; stdcall; external dllname;
And call it like this:
Edit1.Text := IntToStr(GetValue);
No doubt the real code will do more than return the value 6 but that's no problem. You can return anything you like. They key point is that you pass the value from the DLL to the host using a function return value.

it works good:
in MainUnit
implementation
procedure Test(Edit1: TEdit); stdcall; external 'dll_proj.dll';
in DLL
exports Test;
Procedure Test(Object1: TEdit); stdcall;
var i:integer;
begin
for i:= 0 to 100 do
begin
Object1.Text:= IntToStr(i);
Application.Processmessages();
Sleep(100);
end;
end;

Building on your original code and adding a few more buttons, here's a demonstration of how you can use procedures (or functions if you prefer) and have them play nicely with a DLL.
Note that the name option is not required unless you wish to change the function's name or use overloading - so I've commented it out.
implementation
procedure test(var a : integer); external 'lib.dll' {name 'test'};
procedure test2(ptr_a : pinteger); external 'lib.dll';
procedure test3(ptr_a : pinteger); external 'lib.dll';
procedure test4(ptr_a : pinteger = nil); external 'lib.dll';
{$R *.dfm}
procedure TForm14.Button1Click(Sender: TObject);
begin
test(a);
Edit1.Text:=Inttostr(a);
end;
procedure TForm14.Button2Click(Sender: TObject);
begin
test2(#a);
Edit1.Text:=Inttostr(a);
end;
procedure TForm14.Button3Click(Sender: TObject);
begin
test3(#a);
Edit1.Text:=Inttostr(a);
end;
procedure TForm14.Button4Click(Sender: TObject);
begin
test4(#a);
test4;
Edit1.Text:=Inttostr(a);
end;
end.
... and the library body ...
var
local_a:integer;
local_Ptr_a:pinteger;
procedure test(var a : integer);
begin
a:=6;
end;
procedure test2(ptr_a : pinteger);
begin
inc(ptr_a^);
end;
procedure test3(ptr_a : pinteger);
begin
inc(local_a);
ptr_a^:=local_a;
end;
procedure test4(ptr_a : pinteger = nil);
begin
if ptr_a = nil then
inc(local_ptr_a^)
else
local_ptr_a := ptr_a;
end;
exports test;
exports test2;
exports test3;
exports test4;
{$R *.res}
begin
local_a := 4;
end.
So - to a little explanation.
First test : using a var-parameter to return a value from a procedure. No problem there.
Second test : pass the address of the receiving variable as a pointer. I've added a little curl here - incrementing the value for entertainment er,...value.
Third test : This is showing how local values owned by the DLL can be used. The local value is initialised by the assignment of 4 at the end. The procedure itself uses the same mechanism as the second test to return the value from the DLL's local variables to the main routine's variables.
Note that test3 assigns to the program's variable (1 + the value stored in the DLL's memory, ) hence pressing button3 will actually changes a; so pressing buttons 1-2-2-3-2 will use the value from 3 for the last change, not the prior-value-from-2.
Final test: this where we can get a little more clever. It uses the optional-parameters mechanism to vary the detailed operation.
First you eecute the procedure with a parameter, being the address of (or pointer to) a variable of the appropriate type. The procedure stores that address in the DLL's memory area.
Next you can execute the procedure with no parameters and it will increment the integer to which the stored pointer is pointing. Purely for convenience, I've established the variable's address on each button-push, but once the address has been stored, it doesn't matter whether that address was set a few microseconds ago or a week, test4; will increment the integer value at that address - whatever it is. Set the address using test4(#b); then test4; will increment b - whichever integer was last pointed to when the procedure was executed with a parameter.
executing test4; without having at sometime prior executed a test4(#something) or where something is now out-of-scope (like perhaps a local variable in a procedure) is very likely to cause an access violation.

Related

Why the shortcut doesn't work in my Delphi program?

I've written a program in Delphi 10.4. The main part of the UI is just a TMemo. When the user types something in it, the app will automatically copy the text in the TMemo to clipboard. It looks like this:
This auto copy part works well. However, I also want to let the user change dark theme or light theme by a shortcut. I enabled a dark theme and a light theme.
The code looks like this:
unit Unit1;
interface
uses
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants,
System.Classes, Vcl.Graphics,
Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls, Clipbrd, System.Actions,
Vcl.ActnList, Vcl.Themes;
type
TForm1 = class(TForm)
txt: TMemo;
ActionList1: TActionList;
act_change_theme: TAction;
procedure txtChange(Sender: TObject);
procedure act_change_themeExecute(Sender: TObject);
procedure FormCreate(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;
var
Form1: TForm1;
var
is_dark: Boolean;
implementation
{$R *.dfm}
function ShortCut(Key: Word; Shift: TShiftState): TShortCut;
begin
Result := 0;
if HiByte(Key) <> 0 then
Exit; // if Key is national character then it can't be used as shortcut
Result := Key;
if ssShift in Shift then
Inc(Result, scShift); // this is identical to "+" scShift
if ssCtrl in Shift then
Inc(Result, scCtrl);
if ssAlt in Shift then
Inc(Result, scAlt);
end;
procedure TForm1.act_change_themeExecute(Sender: TObject);
begin
if is_dark then
begin
TStyleManager.TrySetStyle('Windows', false);
is_dark := false;
end
else
begin
TStyleManager.TrySetStyle('Carbon', false);
is_dark := true;
end;
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
is_dark := false;
act_change_theme.ShortCut := ShortCut(Word('d'), [ssCtrl]);
end;
procedure TForm1.txtChange(Sender: TObject);
begin
try
Clipboard.AsText := txt.Lines.GetText;
except
on E: Exception do
end;
end;
end.
However, when I press ctrl+d, nothing happened. I tried to debug it and I found that ctrl+d never triggers the action's shortcut. Why this happened? How to fix it? I've used the shortcut function in the past and it worked.
Try Word('D'), or the constant vkD, instead of Word('d'). Shortcuts use virtual key codes, and letters are represented as virtual keys using their capital values. Typing an Uppercase or Lowercase letter into an edit control uses the same virtual key, it is the current shift state that determines the case of the letter when the key is translated into a text character.
Also note that the VCL has its own ShortCut() function (and also TextToShortCut()) in the Vcl.Menus unit for creating TShortCut values, so you don't need to write your own function.
See Representing Keys and Shortcuts, especially Representing Shortcuts as Instances of TShortCut.
Also, your TAction is clearly placed on the Form at design-time, so you should simply assign its ShortCut using the Object Inspector, rather than in code. Then these details would be handled for you automatically by the framework.

Reference counted object within a record not destroyed when record goes out of scope

I have a record that contains what I believe is a pointer to a reference counted object. I would expect that if I create the reference counted object within the record that when the record goes out of scope the reference count of the object would fall to zero, and the object would be destroyed. But this does not seem to be that case. Here is sample minimum code. My form happens to have some panels and a memo, but only the TButton (and specifically Button1Click) is important.
uses
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls, Vcl.ExtCtrls;
type
TUserData = class( TInterfacedObject )
public
AData : integer;
constructor Create;
destructor Destroy; override;
end;
TTestRec = Record
AField : integer;
UserData : TUserData;
End;
TForm4 = class(TForm)
Panel1: TPanel;
Panel2: TPanel;
Panel3: TPanel;
Memo1: TMemo;
Button1: TButton;
procedure FormShow(Sender: TObject);
procedure Button1Click(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;
var
Form4: TForm4;
implementation
{$R *.dfm}
procedure TForm4.Button1Click(Sender: TObject);
var
iRec : TTestRec;
begin
iRec.UserData := TUserData.Create;
// stop too much optimisation
Button1.Caption := IntToStr( iRec.UserData.AData );
end; // I would expect TTestRec and hence TTestRec.UserData to go out of scope here
procedure TForm4.FormShow(Sender: TObject);
begin
// show leaks on exit
ReportMemoryLeaksOnShutdown := TRUE;
end;
{ TUserData }
constructor TUserData.Create;
begin
inherited Create;
AData := 4;
end;
destructor TUserData.Destroy;
begin
inherited;
end;
end.
I confess I don't really understand how reference counting works in detail, although I do understand the principle. What am I missing? Am I expecting too much and if so, is there any way to avoid memory leaks, not in this specific case (where obviously I could destroy UserData on exit) but in general, since records do not support destructors.
Automatic reference counting is performed through interface variables. You don't have any. Instead of a variable of type TUserData you need a variable that is an interface.
You could use IInterface here but that would be a little useless. So you should define an interface that exposes the public functionality you need the object to support and then have your class implement that interface.
This program demonstrates what I mean:
type
IUserData = interface
['{BA2B50F5-9151-4F84-94C8-6043464EC059}']
function GetData: Integer;
procedure SetData(Value: Integer);
property Data: Integer read GetData write SetData;
end;
TUserData = class(TInterfacedObject, IUserData)
private
FData: Integer;
function GetData: Integer;
procedure SetData(Value: Integer);
end;
function TUserData.GetData: Integer;
begin
Result := FData;
end;
procedure TUserData.SetData(Value: Integer);
begin
FData := Value;
end;
type
TTestRec = record
UserData: IUserData;
end;
procedure Main;
var
iRec: TTestRec;
begin
iRec.UserData := TUserData.Create;
end;
begin
Main;
ReportMemoryLeaksOnShutdown := True;
end.
This program does not leak. Change the variable declaration in the record type to UserData: TUserData and the leak returns.

Calling a procedure with objects as arguments

Trying to save code. I want to display text etc on an image on the form at OnActivate then print the same text on clicking button (Real program is more complicated). To save writing code twice I tried the enclosed code but it won't compile at the "Obj.Canvas" line. If I comment out this line and the enclosed line the program runs but the Obj value is ().
I've tried several other approaches but none work. Can anyone tell me where I'm going wrong.
Badger
unit Unit7;
interface
uses
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.ExtCtrls, Vcl.StdCtrls, printers;
type
TForm7 = class(TForm)
Print: TButton;
Image1: TImage;
PrintDialog1: TPrintDialog;
procedure FormActivate(Sender: TObject);
procedure PrintClick(Sender: TObject);
private
{ Private declarations }
public
DH,DW:Extended;
Procedure DoLayout(Obj:TObject);
{ Public declarations }
end;
var
Form7: TForm7;
implementation
{$R *.dfm}
procedure TForm7.FormActivate(Sender: TObject);
begin
DoLayout(Image1);
end;
procedure TForm7.PrintClick(Sender: TObject);
begin
if PrintDialog1.Execute then
begin
printer.BeginDoc;
DoLayout(Printer);
Printer.EndDoc;
end;
end;
procedure TForm7.DoLayout(Obj:TObject);
begin
if Obj =Printer then //when you run the program Obj is ()
begin
DW:=Printer.PageWidth/Image1.Width;
DH:=Printer.PageHeight/Image1.Height;
end
else
begin
DH:=1;
DW:=1;
end;
With Obj.canvas do //Error here when compiled - tried commenting it out
begin
TextOut(Int(DH*50),Int(DW*30),'This is the text'); //commented this out too
end;
end;
end.
The TPrinter class and the TImage class don't share a common ancestor class except for TObject, as a result that's what you're passing in.
A suggested refactoring is to change the DoLayout code to accept the canvas that you want to use, as well as an parameter to determine if it's a printer or an image that you're passing in e.g.
procedure TForm7.DoLayout(aCanvas : TCanvas; bPrinter : boolean);
begin
if bPrinter then //when you run the program Obj is ()
begin
DW:=Printer.PageWidth/Image1.Width;
DH:=Printer.PageHeight/Image1.Height;
end
else
begin
DH:=1;
DW:=1;
end;
With aCanvas do
begin
TextOut(Int(DH*50),Int(DW*30),'This is the text');
end;
end;
then when you call it, use the printer canvas explicitly, or the image canvas:
DoLayout(Printer.canvas, true);
or
DoLayout(Image1.canvas, false);
this is just a rough estimation based on your code; I don't have a delphi compiler to hand to verify it.

How to attach an object of any type with a TControl?

I want to add a TList with a TTreeViewItem and a custom class (TRoom)'s object with another. In delphi 2007 there was a field 'Data' of Pointer type which has been replaced with a TValue here which I don't know as to how to use. I have searched the internet with some stating that it can't handle custom types for the time being.
Can somebody devise a way to achieve this, except for making a hack class?
For example, the following form code should run properly:-
unit Unit1;
interface
uses
System.SysUtils, System.Types, System.UITypes, System.Rtti, System.Classes,
System.Variants, FMX.Types, FMX.Controls, FMX.Forms, FMX.Dialogs,
FMX.TreeView, FMX.Layouts, FMX.Edit;
type
TRoom = class
ID : WORD;
Name : String;
end;
TForm1 = class(TForm)
TreeView1: TTreeView;
TreeViewItem1: TTreeViewItem;
Button1: TButton;
Edit1: TEdit;
procedure FormCreate(Sender: TObject);
procedure Button1Click(Sender: TObject);
end;
var
Form1: TForm1;
implementation
{$R *.fmx}
procedure TForm1.Button1Click(Sender: TObject);
var
List : TList;
begin
// Get The List From TreeViewItem1
// pani's Solution - List := TList ( TreeViewItem1.TagObject );
Edit1.Text := TRoom ( List.First ).Name;
end;
procedure TForm1.FormCreate(Sender: TObject);
var
Room : TRoom;
List : TList;
begin
List := TList.Create;
Room := TRoom.Create;
Room.ID := 5;
Room.Name := IntToStr ( 5 );
List.Add ( Room );
// Add The List To TreeViewItem1
// pani's Solution - TreeViewItem1.TagObject := List;
end;
end.
If you want to "attach" an object to TControl, TControl's parent class TFmxObject introduces the .TagObject property that stores any object value.
Besides using this property you can also use the .Tag property with typecasting into NativeInt and your wanted class type, for example: TreeViewItem1.Tag := NativeInt(List); and List := TList(TreeViewItem1.Tag);
In in a 'small "g"' generic fashion, the Data property of a FMX control should get or set the control's core value. In a TImage case this will be the bitmap displayed, for a TEdit the text and so on. As such, its purpose is completely different to the Data property of a VCL tree view item, which is to hang an arbitrary piece of data off of the object.
As pani rightly answers, if you want to hang an arbitrary object reference to a FMX tree view item, then you can use TagObject. That said, and notwithstanding irritations concerning FMX's bodging of proper OOP behaviour (see here), if you are creating tree view items dynamically then a better way might be to derive a custom TTreeViewItem descendant:
uses System.Generics.Collections;
type
TRoomTreeViewItem = class(TTreeViewItem)
RoomList: TList<TRoom>; //better use a generic than non-generic list as mentioned above
end;
Or, if the lifetime of a room list is the same as the lifetime of the tree view item it is associated with, you could actually encapsulate the list in the item:
type
TRoomTreeViewItem = class(TTreeViewItem)
strict private
FRoomList: TObjectList<TRoom>;
function GetRoom(Index: Integer): TRoom;
public
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
function GetEnumerator: TEnumerator<TRoom>;
function AddRoom: TRoom;
property Rooms[Index: Integer] read GetRoom;
end;
constructor TRoomTreeViewItem.Create(AOwner: TComponent);
begin
inherited;
FRoomList := TObjectList<TRoom>.Create;
end;
destructor TRoomTreeViewItem.Destroy;
begin
FRoomList.Free;
inherited;
end;
function TRoomTreeViewItem.GetEnumerator: TEnumerator<TRoom>;
begin
Result := FRoomList.GetEnumerator;
end;
function TRoomTreeViewItem.GetRoom(Index: Integer): TRoom;
begin
Result := FRoomList[Index];
end;
function TRoomTreeViewItem.AddRoom: TRoom;
begin
Result := TRoom.Create;
FRoomList.Add(Result);
end;
Some people may consider the second variant a terrible conflation of non-UI with UI code however - personally I don't oppose it (indeed, that's why I've suggested it), though YMMV.

How I can parse a tnsnames.ora file from delphi?

How can I get the list of Oracle data source names and add them to a combobox so that I can choose whcich datasource to connect to? I need the program to read the contents of the TNS_NAMES.ora file and get the data source names. I can do a FileSearch but want the program to find the TNS_NAMES file itself like TOAD,PL/SQL developer and other Oracle managers do, as the program will be run on different computers and Oracle client might be installed into different folders.
To get the datasource or any other information contained inside of the TNS_NAMES.ora file you must parse this file. So first read the Syntax Rules for this file from here and here, and then you can use the most common approach to parse these files, which is use regular expressions. Unfortunally the Delphi 2010 RTL doesn't include support for regular expressions. But you can use the PCRE library). from here you can use as guide these articles to write your own delphi implementation.
TNSNames Reader (C#)
Parsing tnsnames.ora using regex (C#)
You can use this code for Oracle 10. Drop combobox and button on your form and link FormCreate and button1click events.
unit Unit1;
interface
uses
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Registry, Vcl.StdCtrls;
type
TForm1 = class(TForm)
ComboBox1: TComboBox;
Button1: TButton;
procedure Button1Click(Sender: TObject);
procedure FormCreate(Sender: TObject);
private
{ Private declarations }
procedure ParseTNS;
public
{ Public declarations }
end;
var
Form1: TForm1;
slTNSConfig : TStringList;
implementation
{$R *.dfm}
function GetTNSNamesPath : string;
var
Reg: TRegistry;
SubKeyNames: TStringList;
Name: string;
begin
Reg := TRegistry.Create;
Try
Reg.RootKey := HKEY_LOCAL_MACHINE;
Reg.OpenKeyReadOnly('SOFTWARE\ORACLE');
SubKeyNames := TStringList.Create;
Try
Reg.GetKeyNames(SubKeyNames);
for Name in SubKeyNames do
// oracle 10 save path to ORACLE_HOME in registry key like this
// HKEY_LOCAL_MACHINE\SOFTWARE\ORACLE\KEY_OraClient10g_home1\ORACLE_HOME
// for oracle 8 and 9 another key
if pos('KEY_',Name)=1 then
begin
Reg.OpenKeyReadOnly(Name);
// for oracle 10 path to tnsnames.ora like this
// %ORACLE_HOME%\NETWORK\ADMIN\tnsnames.ora
// for oracle 8 and 9 another path
Result :=Reg.ReadString('ORACLE_HOME')+'\NETWORK\ADMIN\tnsnames.ora';
end;
Finally
SubKeyNames.Free;
End;
Finally
Reg.Free;
End;
end;
procedure TForm1.ParseTNS;
var
slTemp : TStringList;
sPath, sTemp : string;
i : integer;
begin
slTemp:= TStringList.Create;
slTNSConfig:= TStringList.Create;
try
sPath:=GetTNSNamesPath;
if (length(sPath)<33) or (not FileExists(sPath)) then
messageDlg('tnsnames.ora not found.', mtError, [mbOk],0)
else
begin
slTemp.LoadFromFile(sPath); // Load tnsnames.ora
sTemp := StringReplace(StringReplace(UpperCase(slTemp.Text),' ','',[rfReplaceAll]),')','',[rfReplaceAll]); // delete ')' and spaces
slTemp.Clear;
slTemp.Delimiter:='(';
slTemp.DelimitedText:=sTemp; // parse like Name=Value
sTemp:='';
for i := 0 to slTemp.Count-1 do
begin
if pos('DESCRIPTION',slTemp[i])=1 then // Get Name before description
begin
sTemp:=StringReplace(slTemp[i-1],'=','',[rfReplaceAll]);
ComboBox1.Items.Add(sTemp); // Fill combobox
end;
if length(slTemp.ValueFromIndex[i])>0 then //Get filled Name=Value
slTNSConfig.Add(sTemp+'_'+slTemp[i]); // Fill TNS config like TNS_HOST=Value
end;
ComboBox1.Sorted:=true;
end;
finally
slTemp.Free;
end;
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
ComboBox1.Text:='';
ParseTNS;
end;
procedure TForm1.Button1Click(Sender: TObject);
var
sHost, sPort, sSID, sServiceName : string;
begin
sHost:=slTNSConfig.Values[ComboBox1.Text+'_HOST'];
sPort:=slTNSConfig.Values[ComboBox1.Text+'_PORT'];
sSID:=slTNSConfig.Values[ComboBox1.Text+'_SID'];
sServiceName:=slTNSConfig.Values[ComboBox1.Text+'_SERVICE_NAME'];
messageDLG('sHost:'+sHost+' sPort:'+sPort+' sSID:'+sSID+' sServiceName:'+sServiceName,mtInformation,[mbOk],0);
end;
end.

Resources