I have a custom component of type TSpeedButton that has two extra properties defined:
CommentHeading: string;
CommentText: string;
I set CommentHeading at design time.
When the speed button is pressed a memo is shown with a button beneath it for saving its contents. The procedure that handles this:
procedure CustomSpeedButton1Click(Sender: TObject);
begin
Receiver := CustomSpeedButton1.Name; // possibly used to save the memo text back to this speedbuttons property after comments are submitted
ViewComments(CustomSpeedButton1.CommentTitle,CustomSpeedButton1.CommentText);
end;
And the ViewComments procedure itself:
procedure ViewComments(comment_caption:string; comment_text:string);
begin
label15.Hide; // label showing editing in progress, hidden until user begins typing
Button1.Enabled := false; // the button for saving the memo text, hidden until user begins typing
CommentsBox.Visible := true; // pop up the comment box at the bottom of the form
CommentsBox.Caption := 'Comments: ' + comment_caption;
CommentsMemo.Text := comment_text; // if there are existing comments assign them to memo
end;
The contents of the memo need to be assigned to the CommentText property of the custom SpeedButton.
What I was initially thinking was that I could pass the component name to a variable when the custom SpeedButton gets pressed and then retrieve that name when the save button on the memo is pressed and use it to assign the memo text to the speedbuttons CommentText property. But then I realized that to do this I'd have to use some kind of case..of statement that checked for each possible speedbutton name and then assign the memo value to its properties and this just seems ridiculously tedious.
Is there an easier way to assign the memo text to the speedbutton that opened the memo to begin with?
Ultimately, you're asking how to tell the ViewComments function which button's properties it's working with.
Do you understand what the Sender parameter is doing in the OnClick event? It's telling the event handler which object's event is being handled. It's serving precisely the role that you're looking to bring to the ViewComments function.
That's what Mason was getting at in his answer. Rather than pass all the property values, pass the object itself:
procedure ViewComments(CommentButton: TCustomSpeedButton);
Then call it from all your buttons' event handlers:
procedure TForm1.CustomSpeedButton1Click(Sender: TObject);
begin
ViewComments(CustomSpeedButton1);
end;
procedure TForm1.CustomSpeedButton2Click(Sender: TObject);
begin
ViewComments(CustomSpeedButton2);
end;
No strings, no case statements, no lookups.
That should answer your question, but you can do it even better. Remember what I said before about the Sender parameter? When someone clicks the first button, the Sender parameter of that OnClick handler will be the button, so we can rewrite the first event handler like this:
procedure TForm1.CustomSpeedButton1Click(Sender: TObject);
begin
ViewComments(Sender as TCustomSpeedButton);
end;
And you can rewrite the second event handler like this:
procedure TForm1.CustomSpeedButton2Click(Sender: TObject);
begin
ViewComments(Sender as TCustomSpeedButton);
end;
Hmm. They're the same. Having two identical functions is wasteful, so get rid of one and rename the other so it doesn't sound button-specific:
procedure TForm1.CommentButtonClick(Sender: TObject);
begin
ViewComments(Sender as TCustomSpeedButton);
end;
Then set the OnClick properties of both buttons to refer to that one event handler. You can't do that just by double-clicking the property in the Object Inspector. You'll need to either type the name yourself, choose it from the drop-down list, or assign the event property at run time:
CustomSpeedButton1.OnClick := CommentButtonClick;
CustomSpeedButton2.OnClick := CommentButtonClick;
I'd also like to encourage you to use more meaningful names for your controls. That Label15 is particularly egregious. How can you remember that the fifteenth label is the one that indicates that editing is in progress? Call it EditInProgressLabel, for instance.
Since you're already passing extra variables around, why not just pass the SpeedButton itself? Then you won't need to look up the reference.
A small change to your code should do the trick:
procedure TForm1.CustomSpeedButton1Click(Sender: TObject);
var
btn: TCustomSpeedButton;
begin
btn := Sender as TCustomSpeedButton;
Receiver := btn.Name;
ViewComments(btn.CommentTitle, btn.CommentText);
end;
and after editing the comment:
procedure TForm1.StoreComments(comment: string);
var
btn: TCustomSpeedButton;
begin
btn := FindComponent(Receiver) as TCustomSpeedButton;
btn.CommentText := comment;
end;
You can also memorize the button itself instead of just it's name.
Related
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;
This is in Delphi Pascal
I have dynamical created a 2d array of buttons. I want it such that when you click these buttons, the button pressed will dissapear. (This is to make a minesweeper style game).
Currently i have this array of these buttons, and am trying to set the onclick event to a procedure and passing in the x and y indexes of this button to the procedure such that it can hide the correct button.
Here is what i am using to link to the procedure:
buttons[x,y].OnClick := buttonClicked(x,y);
The x and y are made from a for loop, in order that i can have a 2d array.
An this is the error I am getting.
E2010 Incompatible types: 'TNotifyEvent' and 'procedure, untyped pointer or untyped parameter'
Here is the Procedure:
procedure TMainPage.buttonClicked(buttonx: Integer; buttony: Integer);
begin
buttons[buttonx,buttony].Visible:=False;
end;
Im assuming that the problem is that you cannot send variables through on the OnClick event.
I am unsure of a way to do what i want, so guidance would be appreciated.
Currently i have this array of these buttons, and am trying to set the onclick event to a procedure and passing in the x and y indexes of this button to the procedure such that it can hide the correct button.
You will not be able to pass that information directly to the OnClick event handler using input parameters. The event is declared as a TNotifyEvent, so the only input parameter it can accept is a TObject for the object that triggers the event.
However, that object is the clicked TButton itself, so you can simply use it as-is:
procedure TMainPage.buttonClicked(Sender: TObject);
begin
TButton(Sender).Visible := False;
end;
If, for some reason, you actually needed the XY indexes within the array, you would have to pass those values to the event handler indirectly, such as with the button's Tag property:
buttons[x,y].Tag := Integer(SmallPoint(x, y));
...
procedure TMainPage.buttonClicked(Sender: TObject);
var
pt: TSmallPoint;
begin
pt := TSmallPoint(TButton(Sender).Tag);
// use pt.x and pt.y as needed...
end;
Though, a safer option would be to just derive a new class from TButton instead, and then you can add whatever you want to it:
type
TMyButton = class(TButton)
public
ArrayX, ArrayY: Integer;
end;
...
buttons[x,y] := TMyButton.Create(...);
buttons[x,y].ArrayX := x;
buttons[x,y].ArrayY := y;
...
procedure TMainPage.buttonClicked(Sender: TObject);
var
Btn: TMyButton;
begin
Btn := TMyButton(Sender);
// use Btn.ArrayX and Btn.ArrayY as needed...
end;
As Remy pointed out, there's a simpler way to have the button be made not visible.
To set the OnClick event when creating each new button, use the following:
buttons[x,y].OnClick := #buttonClicked;
I have multiple TTabSheet on TPageControl and on certain action, user click on a button, I would like run OnResize event on a selected TTabSheet. The problem is that not all TTabSheet controls have OnResize event created.
I have this code on button:
procedure TForm1.Button1Click(Sender: TObject);
begin
TTabSheet(PageControl1.ActivePage).OnResize(PageControl1.ActivePage);
end;
procedure TForm1.TabSheet1Resize(Sender: TObject);
begin
// actions on Resize
end;
It works when TabSheet1 is active. But when TabSheet2 is active and it doesn't have OnResize event I get error:
Project Project1.exe raised exception class $C0000005 with message
'access vialotion at 0x00000000: read of address 0x00000000'.
I tried to check for nil, like this:
If TTabSheet(PageControl1.ActivePage).OnResize(PageControl1.ActivePage) <> nil then...
But it doesn't compile:
E2008 Incompatible type.
The workaround I discovered is for each TTabSheet control to have empty OnResize event, with just a comment, no code.
Is there any better check than <> nil, that doesn't work, if TTabSheet has OnResize event?
Thank you
You are probably going about this the wrong way. You should not invoke event handlers in your code. Leave that to the framework. Instead write it like this:
procedure TForm1.DoTabsheet1Resize;
begin
// actions on Resize
end;
procedure TForm1.Button1Click(Sender: TObject);
begin
DoTabsheet1Resize;
end;
procedure TForm1.TabSheet1Resize(Sender: TObject);
begin
DoTabsheet1Resize;
end;
Here DoTabsheet1Resize is a private method that you define.
procedure TForm1.Button1Click(Sender: TObject);
begin
If Assigned(PageControl1.ActivePage.OnResize)
then PageControl1.ActivePage.OnResize(Sender);
end;
Judging by this comment of yours
I set OnResize events, but nothing for TabSheet2. So TabSheet2 has OnResize in Object Inspector but not in code in unit1.
I think you may be confused about exactly what goes on the Form Designer, it terms of defining event-handling code.
Try this:
On a newly-created blank form, drop a TPageControl, PageControl1, right-click it and create two TabSheets in it, TabSheet1 and TabSheet2.
In the OI, click TabSheet1, click its Events tab in the OI and click the drop-down button in the OnResize event property. You will find the drop down list empty.
Now double-click in Tabsheet1's OnResize event. Doing this causes the OI to create an empty event handling procedure TabSheet1Resize in the form's code. It also assigns the OnResize property of TabSheet1 to this procedure, TabSheet1Resize. Don't save the form at this point, or the IDE will detect that this procedure contains no code (or comments) and will delete it.
In TabSheet1Resize add this code
Caption := 'TabSheet1.Resize';
and save the form's unit.
Now select TabSheet2's event tab in the OI and single-click in its OnResize event. At this point TabSheet2's OnResize property is not assigned to any procedure so attempting to call TabSheet2.OnResize() will result in an AV, because the value of its OnResize property is Nil.
Now, In TabSheet2's OnResize, click the drop-down button and you will see
TabSheet1Resize as the single entry in the list. Select it from the list and now the value of TabSheet2's OnResize property is the same as TabSheet1'1, i.e. the procedure TabSheet1Resize.
Steps 3 and 6 are the ones which are vital for TabSheet1 and TabSheet2 to have the same OnResize event-handler code.
In other words, your comment seems to overlook the difference between the event-handler property (e.g. OnResize) which the object, in this case a TTabSheet always has (which is why the OI shows you the event-handler property) and the code in the event-handler procedure, if any, to which the event-handler property is assigned.
Sorry if all the above is painfully obvious, if it is then I've misunderstood several of your comments.
If you want all the other tabsheets to have the same OnResize handler as TabSheet1, you can set this up in, e.g., your FormCreate event handler, like so:
procedure TForm1.FormCreate(Sender: TObject);
var
i : integer;
begin
for i := 1 to PageControl1.PageCount - 1 do
PageControl1.Pages[i].OnResize := PageControl1.Pages[0].OnResize;
// PageControl1.Pages[0] will be equal to TabSheet1, if that's the first Tabsheet
// you created
end;
I need assistance for trying to get the name of a dynamically created button using and OnClick event in Delphi.
I am then want to use the name of that button and store it in a global variable.
This is where I am currently:
procedure TMap.FormShow(Sender: TObject);
var
btnCache : TButton;
begin
btnCache := TButton.Create(imgAerial);
with btnCache do
begin
onclick := ClickButton;
end;
procedure TMap.ClickButton(Sender: TObject);
begin
//Code for getting the name of the button
end;
The button's name can be retrieved by casting Sender to the type that introduces Name. That is TComponent.
(Sender as TComponent).Name
Don't expect this name to be very informative since your code does not assign a name to the button. As the code is written in the question, the dynamically created button has no name.
Please assist me: How to assign an up arrow keyboard shortcut to action or menu item, and keep it actual for navigating the list control (e.g. ListBox/Virtual Treeview/other) at the same time?
Thanks!
You comment:
And how about the Winamp player? It has Volume Up/Volume Down features assigned to the up arrow key and down arrow key correspondingly.. Okay, if that impossible in Delphi, then ...
but it certainly is possible, it just isn't a good idea to do it, and against the Windows User Experience Interaction Guidelines.
But if you're set on implementing it, here's how. Override the following method in your form class that contains the action components:
function IsShortCut(var Message: TWMKey): Boolean; override;
and in it you can prevent the Up and Down key from triggering the actions they are shortcuts for:
function TWeirdForm.IsShortCut(var Message: TWMKey): Boolean;
begin
if (Message.CharCode in [VK_UP, VK_DOWN])
// insert test whether message needs to go to the focused control instead
and (...)
then begin
// insert calls to code that should be executed instead
Result := False;
exit;
end;
inherited;
end;
Note that you should test for the correct shift state too, and check that your code doesn't break any other window behaviour users expect, like moving of the window with the arrow keys.
On the form properties set KeyPreview := true
then on KeyUp event of the form write event to check if you Up key is pressed and make it call the menu item (on this case menu item called Action1):
procedure TForm1.FormKeyUp(Sender: TObject; var Key: Word; Shift: TShiftState);
begin
if (Key = VK_UP) and (ActiveControl = ListBox1)then
Action11.Click;
end;
procedure TForm1.Action11Click(Sender: TObject);
begin
if ListBox1.ItemIndex >=0 then
ShowMessage(ListBox1.Items[ListBox1.ItemIndex]);
end;
If you need the Action1 to be executed even if they Current Control isn't the listbox, remove the and part of the IF statement