I am new to delphi and pascal and was wondering if there was a way to get/access a property of the component that the Sender is referencing within the procedure.
More specifically I would like to make a procedure that changes the caption property of a label, that label being the component that Sender is referencing.
I imagine that procedure looking something like:
procedure TForm1.LabelEdit(Sender: TObject);
begin
Sender.caption := 'Sample Text';
end;
Naturally this wouldn't work but can something like or something similar to this be done?
Although the example in your question doesn't really make sense (it incorrectly suggests that a TLabel has an OnEdit event), it is very much possible to use the Sender parameter to obtain information about the sender object.
Create a new VCL application and drop a number of TLabel controls on the form. Give them different captions (like Dog, Cat, Rabbit, Horse etc.).
Now select them all in the form designer and then use the Object Inspector to create a common OnClick handler for them. You can name it LabelClick (write LabelClick in the edit field next to OnClick and press Enter).
This will create the following empty method:
procedure TForm1.LabelClick(Sender: TObject);
begin
end;
It has a Sender parameter of type TObject. Now, depending on how this method is called, Sender can be any TObject (a button, a form, a bitmap, ...), or nil (no object at all).
But in our case, we expect this method mainly to be called in response to the labels being clicked on, and in these cases, the Sender will be the corresponding TLabel object.
Let's try to display the caption of the clicked label in a message box!
We try
procedure TForm1.LabelClick(Sender: TObject);
begin
ShowMessage(Sender.Caption); // won't compile!
end;
But this doesn't even compile! The problem is that TObject has no public Caption member. But TLabel does, so we can write
procedure TForm1.LabelClick(Sender: TObject);
begin
ShowMessage(TLabel(Sender).Caption);
end;
Here we are telling the compiler that we know Sender will always be a TLabel, and we ask it to assume that it is.
But this will crash or do other bad things if somehow this method is called with a non-TLabel Sender. So it is safer to do
procedure TForm1.LabelClick(Sender: TObject);
begin
ShowMessage((Sender as TLabel).Caption);
end;
This does the same, except that the compiler will now create code that checks at runtime that Sender really is a TLabel object. If not, the code will raise an exception. That's much better than the kind of memory corruption/AV issues you may get with the unsafe cast above.
Arguably even better is
procedure TForm1.LabelClick(Sender: TObject);
begin
if Sender is TLabel then
ShowMessage(TLabel(Sender).Caption);
end;
This will also test the type of Sender at runtime. If it is a label, we display its caption. Otherwise, we choose to do nothing. Notice that there is no point in using a safe (and slightly, slightly, slower) as cast here.
You cast Sender to the type that the event connects.
procedure TForm1.Button1Click(Sender: TObject);
begin
if Assigned(Sender) then
(Sender as TButton).Caption := 'Clicked';
end;
If you're sharing the event among different types of controls, you can test first to see what type it is:
procedure TForm1.ControlClick(Sender: TObject);
begin
if (Sender is TEdit) then
TEdit(Sender).Text := 'Clicked'
else if (Sender is TButton) then
TButton(Sender).Caption := 'Clicked';
end;
end;
Related
The good (and bad) old Delphi taught us the "classic" way of building application because of the way we write code "behind" the IDE.
Based on this paradigm, I built some time ago a library that allows me to save/load the GUI to INI file with a single line of code.
LoadForm(FormName)
BAM! That's it! No more INI files!
The library only saves "relevant" properties. For example, for a TCheckBox it saves only its Checked property not also its Color or Top/Left. But it is more than enough.
Saving the form has no issues. However, I have "problems" during app initialization. They are not quite problems, but the initialization code is not that nice/elegant.
For example when I assign Checkbox1.Checked := True it will trigger the OnClick event (supposing that the checkbox was unchecked at design time). But assigning a False value (naturally) will not trigger the OnClick event.
Therefore, I have to manually call CheckBox1Click(Sender) after SaveForm(FormName) to make sure that whatever code is in CheckBox1Click, it gets initialized. But this raises another problem. The CheckBox1Click(Sender) might be called twice during application start up (once by SaveForm and once my me, manually).
Of course, in theory the logic of the program should be put in individual objects and separated from the GUI. But even if we do this, the initialization problem remains. You load the properties of the object from disk and you assign them to the GUI. When you set whatever value you have in your object to Checkbox1, it will (or not) call CheckBox1Click(Sender) which will set the value back into the object.
On app startup:
procedure TForm.FormCreate (Sender: TObject);
begin
LogicObject.Load(File); // load app logic
Checkbox1.Checked := LogicObject.Checked; // assign object to GUI
end;
procedure TForm.CheckBox1Click(Sender: TObject);
begin
LogicObject.Checked := Checkbox1.Checked;
end;
Probably the solution involves writing stuff like this for EVERY control on the form:
OldEvent := CheckBox1.OnClick;
CheckBox1.OnClick := Nil;
CheckBox1.Checked := something;
CheckBox1.OnClick := OldEvent;
Not elegant.
Question:
How do you solve this specific problem OR what is your approach when saving/restoring your GUI to/from disk?
This is one of the things which botthered me in some components from the beginning. What I know the are 3 options, except separating GUI and the business logic as #David said, which is not always an option.
As you wrote above, always unassign the events so they don't get triggered
Use non-triggered events such as OnMouseDown or OnMouseUp
Or a similar solution that I use and I think is the most elegant
Create a global variable FormPreparing which is set during initialization and check its value at the beginning of the events like below.
procedure TForm.FormCreate (Sender: TObject);
begin
FormPreparing := True;
try
LogicObject.Load(File); // load app logic
Checkbox1.Checked := LogicObject.Checked; // assign object to GUI
finally
FormPreparing := False;
end;
end;
procedure TForm.CheckBox1Click(Sender: TObject);
begin
if FormPreparing then
Exit;
LogicObject.Checked := Checkbox1.Checked;
end;
I would like to get the path of the install directory, when the user clicks on Next, when I'm in the wpSelectDir of Inno Setup.
I need to check the path, because I need to verify the path, and if it's not correct, I won't let the user continue.
My problem is that the constant {app} is not set yet, because it will be set after the wpSelectDir and I'm still in.
Use WizardDirValue support function:
Returns the current contents of the edit control on the Select Destination Location page of the wizard.
Unlike ExpandConstant('{app}'), this function will not fail if called after the wizard is shown but prior to the user selecting a directory. Rather, it will return the default directory name.
It's more idiomatic than the WizardForm.DirEdit.Text.
Though internally it does nearly the same:
RemoveBackslashUnlessRoot(WizardForm.DirEdit.Text)
See also How do you find the user-selected install path in Inno Setup?
You can do something like this....
procedure onDirChange(Sender: TObject);
var
currentDir: String;
begin
currentDir := WizardForm.DirEdit.Text;
// your validation goes here....
end;
procedure InitializeWizard;
begin
WizardForm.DirEdit.onChange := #onDirChange;
end;
The WizardForm.DirEdit.Text returns the current value in the DirEdit Text Box. The procedure onDirChange is called everytime the text in the dirEdit text box changes. You can use this value to perform your validations.
I am working on a total rewrite of a PL/SQL program which is an EBS inbound interface to a third party system. In this process we receive input from the 3rd party system which is loaded into a table, and the program loops through the table to call EBS APIs for modifying and creating records. The business logic is very complicated and the code has spiraled out of control which is the reason for the rewrite.
Before calling some of the EBS APIs, we need to validate the incoming data against what is in the Oracle EBS system. The current code has several (20+) procedures to check various pieces of incoming data. Each of these procedures returns a status and a message. Then there is one main procedure which calls all of these validation procedures, and after each procedure call it looks at the status and message - the main procedure is basically a sea of if-else statements and it is extremely cumbersome to follow.
I'm wondering if there's a more suitable practice for performing this quantity of validations so that the code is easier to read and maintain. I have considered making all of the procedures into functions that return boolean values but then I still have the problem of tons of if-else statements in the main calling procedure. I'm just looking for other ideas at this point, the ultimate goal is to improve maintainability of this program.
Thanks in advance
You can't escape the fact that if you have 20 things you have to evaluate you need to have 20 check points in a one way or another.
Your problem statement is very broad but below is one example how I have implemented the same pattern succesfully. The main idea is that the checks (or validation) do not pollute that main business logic but are clearly separated so the main business logic is easy to follow. PL/SQL is sometimes a bit verbose language - here the verbosity goes into exception handling.
Note that in the example I have made some assumptions that might not hold in you specific case.
Validation package:
--
-- NOTE: PL/SQL look alike pseudo code - won't compile
--
-- br = business rule
create or replace package body so46_br is
-- different exceptions required only if each case needs to be identified
-- later in exception handling
err_br_1 constant pls_integer := -20001;
ex_br_1 exception;
pragma exception_init(ex_br_1, -20001);
-- ...
err_br_20 constant pls_integer := -20020;
ex_br_20 exception;
pragma exception_init(ex_br_1, -20020);
procedure assert_br_1(/* input params */) is
v_fail boolean := false;
v_msg varchar2(2000);
begin
-- validate the business rule #1
if v_fail then
v_msg := 'Construct detailed reason why the validation failed.';
raise_application_error(err_br_1, v_msg, true);
end if;
end;
-- ...
procedure assert_br_20(/* input params */) is
v_fail boolean := false;
v_msg varchar2(2000);
begin
-- validate the business rule #20
if v_fail then
v_msg := 'Construct detailed reason why the validation failed.';
raise_application_error(err_br_20, v_msg, true);
end if;
end;
end;
Business logic package:
--
-- NOTE: PL/SQL look alike pseudo code - won't compile
--
-- bl = business logic
create or replace package body so46_bl is
procedure main(/* input params */) is
begin
-- #1 validate input params/business rules
-- assumes we can quit if any validation fails
so46_br.assert_br_1(/* input params */);
-- ...
so46_br.assert_br_20(/* input params */);
-- #2 do the other business logic things
exception
-- assumes each validation failure needs to be identifiable
when so46_br.ex_br_1 then
so46_log.log_br_1(dbms_utility.format_error_stack ||
dbms_utility.format_error_backtrace);
raise;
-- ...
when so46_br.ex_br_20 then
so46_log.log_br_20(dbms_utility.format_error_stack ||
dbms_utility.format_error_backtrace);
raise;
end;
end;
So, I'm trying to call a procedure from a DLL in Delphi XE2.
but the procedure just won't assign.
I have tried several examples found on the internet.
The DLL is being loaded as expected.
The exports are correctly written.
Everything seems fine but still no success.
What is up with that?
The code I have is the following
type
TStarter = procedure; stdcall;
...
fTheHookStart: TStarter;
...
procedure TForm1.LoadHookDLL;
begin
LogLn('Keyboard Hook: Loading...');
// Load the library
DLLHandle := LoadLibrary('thehookdll.DLL');
// If succesful ...
if Handle <> 0 then
begin
LogLn('Keyboard Hook: DLL load OK!');
LogLn('Keyboard Hook: assigning procedure ...');
fTheHookStart := TStarter(GetProcAddress(DLLHandle, 'StartTheHook'));
if #fTheHookStart <> nil then
begin
LogLn('Keyboard Hook: procedure assignment OK!');
LogLn('Keyboard Hook: Starting...');
fTheHookStart;
end
else
begin
LogLn('Keyboard Hook: procedure assignment FAIL!');
FreeLibrary(DLLHandle);
if Handle <> 0 then LogLn('Keyboard Hook: DLL free OK!') else LogLn('Keyboard Hook: DLL free FAIL!');
end;
end
else
begin
LogLn('Keyboard Hook: DLL load FAIL!');
end;
end;
One error is that you assign DllHandle when you load the dll, but then you check if Handle <> nil. Handle is actually your forms handle, which ofcourse is not nil. That will not matter if the loading succeeded, but if it failed, you will get wrong logging.
Since you also have some logging functions, what does the log show?
As I understand it, the DLL loads, but GetProcAddress returns nil. There is only one such failure mode. The DLL does not export a function with that name.
Watch out for name decoration and letter case. C and C++ DLLs may export decorated names. And exported names are sensitive to letter case.
Use dumpbin or Dependency Walker to check the exported function name.
For reference, when GetProcAddress fails, as the documentation explains, a call to GetLastError will yield an error code.
And it looks like the other answer is onto something. You believe that you have loaded the DLL correctly, but your code doesn't perform that check correctly.
If you'd called GetLastError then the system could have alerted you to this. If you'd inspected the variables under the debugger, the problem would have been obvious.
I'm trying to write a plugin system for my application based on jvPlugin. I create forms in the plugin dll, then reparent them into DevExpress docking controls. At first sight, it seems to work. The problem is that none of the controls on the dll forms ever receive focus. Also, when clicking on controls like TSplitter, a "Control 'xxx' has no parent window" exception is raised.
Here's how I'm doing it (condensed version).
The plugin host implements an IPluginHost interface
IPluginHost = interface
['{C0416F76-6824-45E7-8819-414AB8F39E19}']
function AddDockingForm(AForm: TForm): TObject;
function GetParentApplicationHandle: THandle;
end;
The plugin implments an IMyPlugin interface
IMyPlugin = interface
['{E5574F27-3130-4EB8-A8F4-F709422BB549}']
procedure AddUIComponents;
end;
The following event is called when the plugin is initialised:
procedure TMyPlugin.JvPlugInInitialize(Sender: TObject; var AllowLoad: Boolean);
var
RealApplicationHandle: THandle;
begin
if Supports(HostApplication.MainForm, IPluginHost, FPluginHost) then
begin
RealApplicationHandle := Application.Handle;
Application.Handle := FPluginHost.GetParentApplicationHandle; // Returns Application.Handle from the host application
try
FMyPluginForm:= TMyPluginForm.Create(Application); // Plugin host app owns the form
finally
Application.Handle := RealApplicationHandle;
end;
end;
end;
When the plugin host has loaded I call IMyPlugin.AddUIComponents in my plugin. It's implemented like this:
procedure TMyPlugin.AddUIComponents;
begin
// Add the docking form
FPluginHost.AddDockingForm(FMyPluginForm);
end;
AddDockingForm is implemented in the host like this:
function TfrmMyPluginHost.AddDockingForm(AForm: TForm): TObject;
var
DockPanel: TdxDockPanel;
begin
// Create a new dockpanel
DockPanel := TdxDockPanel.Create(Self);
DockPanel.Name := DPName;
DockPanel.Height := AForm.Height;
DockPanel.DockTo(dxDockSite1, dtBottom, 0);
DockPanel.AutoHide := TRUE;
// Rename the dock panel and parent the plugin
DockPanel.Caption := AForm.Caption;
DockPanel.Tag := Integer(AForm);
AForm.Parent := DockPanel;
AForm.BorderStyle := bsNone;
AForm.Align := alClient;
AForm.Show;
FDockedPluginFormList.Add(AForm);
Result := DockPanel;
end;
If I run the following function on any of the controls on the plugin form I see a list going all the way back to my host's main form. Yet TSplitters tell me they have no parent window. How can this be?
function TfrmMyPlugin.GetParents(WC: TWinControl): String;
begin
if (WC <> nil) and (WC is TWinControl) then
Result := WC.Name + ' [' + WC.ClassName + '] - ' + GetParents(WC.Parent);
end;
I must be missing something somewhere. Anybody got any good ideas?
Build both the plugin and the host application with runtime packages.
Without using runtime packages, the DLL uses a distinct copy of the RTL, VCL and any used units. For example, the DLL's TForm class is not the same as the host's TForm class (is operator fails across the host/DLL boundary and therefore a DLL control will not recognize host parent form as a valid instance of TForm), global variables like Application, Mouse, Screen are separate instances, you have two copies of RTTI, etc, etc.
The VCL was simply not designed to be used like this.
With runtime packages on, all the problems are solved and the plugin (either a runtime package itself or a DLL built with runtime packages) can integrate seamlessly with the host application using runtime packages.
We made it work using "runtime packages" adding "vcl,rtl,ourownpckg"
Where ourownpckg is a our-own-package created with all the DX dependencies and some other that we use across exe-plugins, including JVCL ones.
The three packages must be shiped along the exe
Hope it helps