I have read this article how to add a button to another application. When the Button is added to the parent application, everything seems OK, but when this Button is added to another app called Labform (TLabForm), the code after click is not executed. I created also a descendant to implement simple behavior after click, but no success:
TButton2 = class (TButton)
public
procedure Click; override;
end;
procedure TButton2.Click;
begin
inherited;
MessageBox(ParentWindow, 'Hello', 'Window', MB_OK);
end;
procedure TForm1.btn1Click(Sender: TObject);
var
Button2 : TButton2 ;
Hand: THandle;
begin
// Hand:= FindWindow('TLabForm', 'Labform'); // button added, but SHOWS NO message after click
Hand:= FindWindow('TForm1', 'Form1'); // button added, and SHOWS message after click
if Hand <> 0 then
begin
Button2 := TButton2.Create(self);
Button2.ParentWindow := hand;
Button2.BringToFront;
end
else
ShowMessage('handle not found');
end;
How to solve it?
thanx
Whilst it is technically possible to do what you want, it is excruciatingly difficult. Raymond Chen wrote about this at some length. The executive summary:
Is it technically legal to have a parent/child or owner/owned relationship between windows from different processes? Yes, it is technically legal. It is also technically legal to juggle chainsaws.
So, you are attempting something with difficulty akin to juggling chainsaws. Unless you have a deep understanding of Win32 you've got no chance of succeeding.
So, if you want to modify the GUI of an existing process, and it's not tractable to do so with code in a different process, what can you do? Well, it follows that you need to execute code inside the target process.
That's easy enough to do with DLL injection. Inject a DLL into the process and modify it's UI from that DLL. Still not trivial. You'll have the best chance of success if you subclass a window by replacing the existing window procedure with one of your own. That will allow you to run your UI modification code in the UI thread.
Related
I need the user to be able to right click the button and it deletes itself but the following code isn't working
procedure TForm1.Button1Click(Sender: TObject); ////////Creates a new object
var
ExampleButton : TButton;
Begin
ExampleButton := TButton.Create(self); //Creates an object the same as its self
ExampleButton.Parent := self;
//Button properties go here
//Procedures called here
ExampleButton.OnMouseDown := DragOrDelete;
end;
Above creates the button, below I try to delete it
procedure TForm1.DragOrDelete(Sender: TObject; Button: TMouseButton;
Shift: TShiftState; X, Y: Integer);
begin
CursorPosition := Mouse.CursorPos; ////Location of mouse
ExampleButtonStartingLocation := TPoint.Create((Sender as Tbutton).Left, (Sender as Tbutton).Top);
if Button = mbRight then
FreeAndNil(TButton);
end;
The error I get is constant object cannot be passed as a var parameter.
Is it because I create numerous TButtons but the program doesn't know which one to refer one.
Well,
FreeAndNil(TButton);
should be
(Sender as TButton).Free; // thanks to DH
But this is not good. The RTL routines that call the event handler will still have a reference to the button, and need to continue accessing it after the event handler exits, so freeing it may cause further problems (also, Sender is not a var parameter, so setting it to nil will have no effect in the caller).
A better option might be to do something like creating a custom message with Sender as the wParam and posting it to the main form.
Edit
To do this you would create a user message, e.g.
const
WM_DELETE_CONTROL = WM_USER +1;
and replace the offending line with
PostMessage( FormMain.WindowHandle, WM_DELETE_CONTROL, WPARAM( Sender ), 0 );
Then create a procedure in your main form to handle the message, e.g.
procedure DestroyButton( var Msg : TMessage); message WM_DELETE_CONTROL;
with a definition like
procedure TForm1.DestroyButton( var Msg : TMessage);
begin
// RemoveControl( TButton( Msg.LParam ));
// correction - thanks to Remy Lebeau
TButton( Msg.WParam ).Free;
end;
You should make two changes there.
1) you should remember which object you created into a variable, living long enough that both procedures can access it.
2) you should destroy that object by the variable, mentioned above
Right now you are trying to destroy just some any random button. But that is hardly what you need! You probably want to destroy exactly the button you was creating, not some another one.
So:
1) FTableButton should be moved out of procedure TForm1.Button1Click and promoted into a variable of TForm1 class.
2) procedure TForm1.Button1Click should check if the button was already created and not create the second, third, forth... buttons.
procedure TForm1.Button1Click(Sender: TObject);
var
TableString : String;
Begin
if nil <> Self.FTableButton then
raise Exception.Create('Dynamic button already exists!!!');
TableString := IntToStr(TableNumber);
Self.FTableButton := TButton.Create(self);
....
Alternatively, you might choose to delete already existing button (if any) before creating a new one. Usually that is not a good idea, but in some specific scenarios it might make sense (when you need to "reset" the button object, to drop old customized object and create a new one with non-customized default properties).
procedure TForm1.Button1Click(Sender: TObject);
var
TableString : String;
Begin
Self.FTableButton.Free; // if there already was a dynamic button - destroy it
TableString := IntToStr(TableNumber);
Self.FTableButton := TButton.Create(self);
....
3) Now that you have that specific button remembered inside form's FTableButton variable you can use it to delete that very specific object.
procedure TForm1.ButtonMouseDown(Sender: TObject; Button: TMouseButton;
Shift: TShiftState; X, Y: Integer);
begin
...
if Button = mbRight then
FreeAndNil( Self.FTableButton );
end;
Now there is another potential issue here - the "cleanup after suicide" issue.
David Heffernan in comments states that you can not delete the TButton from its own event handler. David states that after you exit the TForm1.ButtonMouseDown procedure the just deleted button would (or at least potentially may) do some more after-event actions with itself without recognizing it was already deleted, leading to any potential error, most probably Access Violation of nil dereference kind.
I can agree with half of that claim.
1) using TForm and TButton from VCL library for Windows is safe, because VCL (at least in Delphi XE2 version) was carefully designed to avoid this trap. The button's internal event calling sequence is designed to never directly address itself after the TForm1.ButtonMouseDown exited. So, at least in that narrow case David is not correct and that is safe thing to do.
2) That is called "relying over implementation detail" however. It is bad style because it is fragile. It might work in some specific case but suddenly break from many other reasons.
As soon as you would switch from the standard VCL TButton to some other fancy buttons from any other library ( Dev Express, LMD, TMS, anything ) that code my suddenly broke. Or you might switch form VCL to FMX. Or switch from FMX/Win32 to FMX/Android.
Or maybe just upgrading Delphi to some newer version would have VCL broken in that regard (that is highly unlikely but possible nonetheless).
So, to play safe you have to decouple those two actions: the button's event handling and the process of its deletion. There are many possible ways to do it, but they all demand more or less extra work and some understanding more issues. Those ways I see split into two avenues.
1) Timing would not change, killing the button would be immediate. But it should not be button deleting itself, it should be some other component.
That is the approach I like the most. I think deleting button on right-click is not the good idea. User might click it randomly, by mistake, by sudden twitch of the hand. Doing something as extreme as suddenly deleting button would be way too harsh change for a random erratic action.
I think you should go traditional way here. Right click should open the context menu, and in the menu there should be the command to delete the button. That way, the menu would be deleting the button, not the button itself.
You would need to add the TPopupMenu onto the form having one single element - deleting the button.
object mnu1: TPopupMenu
object mniFreeBM: TMenuItem
Caption = 'Free the button'
OnClick = mniFreeBMClick
end
end
procedure TForm18.mniFreeBMClick(Sender: TObject);
begin
FreeAndNil( Self.btnMenu );
end;
Then you would have to connect that menu to the button.
.....
Self.FTableButton := TButton.Create(self);
// FTableButton.OnMouseUp := ButtonMouseUp; -- no more doing it! bad style!
Self.PopupMenu := mnu1;
mnu1.AutoPopup := True;
.....
Here we go. When user would R-click the button - there would come the menu, asking him if he wants to delete that button. If he does - then menu, not the button itself, would be freeing it. If users cancels the menu - then it was his random action and he want the button to be kept alive.
That is the better approach as I see.
2) other approaches revolve around idea of time delay, they assure that button only asks Delphi to be deleted someday later, but there would be no immediate kill.
Delphi when realizing it was asked to do it, would then delete the button some later time, after OnMouseUp procedure (and probably few other event-handling procedures too) are long executed and exited.
If the application is not heavy loaded with looong heaaavy computations, then that "later" is very very short actually. Most probably the user would never be able to see the difference. For playing safe, the button might also make itself invisible until someone would delete it.
There can be many approaches to do it, but I would list two.
2.1) DSM in his answer outlines Post_Message-centered approach. That is a good solid "old school" code. It is fast. It is well-understood. But it also has some limitations.
a) it only works on Windows. Forget Android, iOS and others. Well, if you only intend to work on Windows then you can just use standard VCL buttons which can suicide safely, at least in Delphi XE2.
b) it needs you to make a lot of boiler plate, like declaring extra procedures and constants.
c) it is unsafe - you have to make hard unchecked typecasts between integers and button pointers. Easy to make mistake typing/refactoring.
d) you need to understand Windows implementation: message loop, VCL place inside that message loop, difference between PostMessage, SendMessage and Perform, etc.
That is not a rocket science. For any "old school" desktop programmer it is easy and well known. Well, if you were one you would not ask such a question.
Another approach would use multi-threading. From performance perfectionism point of view that is abomination. Creating a new thread (quite the expensive operation!) just to call back and ask the button to be deleted - is very inefficient. But - that way you have much less code to write. You can use standard Delphi features that would most probably work with every operating system and every forms/buttons library, in current and future Delphi versions.
The code is like that.
procedure TForm18.btnThreadMouseUp(Sender: TObject; Button: TMouseButton;
Shift: TShiftState; X, Y: Integer);
begin
......
if mbRight = Button then
TThread.CreateAnonymousThread( procedure
begin
TThread.CurrentThread.Synchronize( nil, procedure
begin
FreeAndNil( btnThread );
end
);
end
).Start;
end;
This way when you exit the OnMouseButtonUp the button is not being deleted, there only is incoming request from the temporary thread to delete it. When the form would work that request out may differ, but anyway it would be another event that happens after you safely exited the button's event handler. Unless you used another abomination ProcessMessages but you did not and hopefully you never ever would.
Recently I was asked to automate a little sub-routine that presented a series of data records and any two of four potential buttons for the user to select after seeing an analysis of the record. The boss said having the users see the analysis was wasting time since the users invariably selected the number one choice in the button list and he was prepared to live with my guesses for all but the best of his users. So, he wanted a NEW series of buttons added to offer Handle Automatically, Handle Manually and Handle Case by Case. The last button would just run the already existing code. The second button would essentially do nothing and just exit. The first button? Well, that was the rub.
What I decided to do was to use a couple of flags and then have the automatic path just simulate the click of whatever sub-button was best, based on the analysis. The issue was that calling Button1Click(Sender) wasn't possible because the procedure running the Analysis was called RunAnalysis and wasn't attached to a specific object to pass the TObject through. I eventually refactored the guts of the Button1Click method into Button1Pressed and then called THAT from Button1Click. Thus I was able to call Button1Pressed from within RunAnalysis.
The avoided path would have been to call Button1Click(Nil). I didn't try it since I had an easy solution (Thanks Modelmaker, by the way). But my question is, would the nil reference have worked or would it have caused a disaster. Could I have called a higher function (randomly?) that did have a sender, JUST to have a sender object in the procedure call? Just how important IS the Sender object, if I don't use anything that actually REFERENCES the Sender?
System details: Delphi 7 in Win 7 programming environment for use in Windows XP.
Thanks in advance for any wisdom, GM
I tend to put the event handler's code into another method when possible:
procedure TForm1.DoSomething(const Test: Boolean);
begin
// Do Something based on Test
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
DoSomething(False); // init here
end;
procedure TForm1.CheckBox1Click(Sender: TObject);
begin
DoSomething(TCheckBox(Sender).Checked);
end;
So when I have a need to call CheckBox1Click(nil) it's a good sign for me to pull off the code from the event handler into a separate method.
A callback or "event" is no different than any other function. You can pass a NIL reference anywhere you want, as long as you either (a) wrote that code and know it's nil safe, or (b) you have read that code and everything it calls without checking for nil, and know it's nil safe.
Some programmers use NIL freely and some consider using NIL parameter values to be bad style. My style tends to be "don't assume Sender is assigned and don't assume that it is of a particular type", which leads to lots of check-code in my event handlers, but other code than mine, it varies widely and so the First Rule of coding comes in; "Don't make assumptions. Read the code.".
You can use nil as a parameter to a TNotify event (or any other expecting a Sender), as long as the code doesn't reference Sender:
procedure TForm1.Button1Click(Sender: TObject);
begin
DoSomeStuff(nil);
end;
procedure TForm1.DoSomeStuff(Sender: TObject);
begin
// Safe
DoSomeOtherStuff;
// Safe
// Do stuff with Sender
if Sender is TButton then
TButton(Sender).Caption := 'In DoSomeStuff'
// NOT safe!
with TButton(Sender) do
begin
Caption := 'In DoSomeStuff';
end;
end;
Short Answer - yes.
I use it to distinguish whether (say) a menu Event was clicked by a user or called directly by code.
I'm was trying to write a dll library in Delphi wih a function that creates an instance of a TFrame descendant and returns it. But when I imported this function in an application, every time I called it I would get an exception like "the 'xxx' control has no parent window". I'm not 100% sure, but the exception appeared in the constructor of that class when any of GUI controls was accessed.
Could you please tell me what the reason of that behaviour is? Should I just use TForm descendants instead or is there a better solution?
Thank you!
About the error
That error message is raised from the Controls.pas unit, from the TWinControl.CreateWnd method. Essentially that code is used to create the Window handle for your TWinControl descendant (TFrame, TButton, TEdit... if it can have keyboard focus it's an TWinControl descendant), and it's actually an very sensible error message: You can't have a Window without an WindowParent, and since we're talking about the VCL here, it makes a lot of sense to try and get the parent window handle from TWinControl.Parent; And that's not assigned.
That's not WHY the error message is popping up. You get to see that error message because some of the code you're using to set up the frame requires an Window handle for some operation. It could be anything, like setting the Caption of some component (that internally requires an window handle do to some calculation). I personally really hate it when that happens. When I create GUI's from code I try to delay the assignment of Parent as much as possible, in an attempt to delay the creation of the window, so I got bitten by this many times.
Specific to your DLL usage, possible fix
I'm going to put my psycho mind reader hat on. Since you need to return a FRAME from your DLL, and you can't return the actual Frame because that's an Delphi-specific object and you're not allowed to return Delphi-specific objects over DLL boundaries, my guess is you're returning an Window Handle, as all the nice API's do, using a function definition like this:
function GiveMeTheNiceFrame:HWND;
The trouble is, that routine requires the creation of the actual Window Handle, by a call to TWinControl.CreateWnd, and in turn that call requires an parent window handle to set up the call to Windows.CreateWindowEx, and the routine can't get an parent window handle, so it errors out.
Try replacing your function with something allong the lines of:
function GiveMeTheNiceFrame(OwnerWindow:HWND):HWND;
begin
Result := TMyNiceFrame.CreateParanted(OwnerWindow).Handle;
end;
... ie: use the CreateParented(AParentWindow:HWND) constructor, not the usual Create(AOwner:TComponent) and pass an owner HWND to your DLL.
There are a few important things to remember:
When using DLLs, both your DLL and your EXE each have an Application instance that are struggling for control. The Controls in your DLL will see the Application instance that belongs to the DLL; the Controls in your EXE will see the Application instance that belongs to the EXE. That struggle is not there when using packages, as then there will only be one Application instance.
Frames are Controls, but they are not Forms.
When using Controls in an application, they cannot visually exist without a parent Control (usually a Form or a container that has a parent hierarchy towards a Form).
Some Controls cannot expose their full functionality unless they exist visually and have a valid parent.
Try to reproduce your problem inside the EXE; if you cannot reproduce, it is probably the first thing in the above list.
--jeroen
Sounds like you simply need to assign the component (a form or part of a form, like a panel) that holds the frame to theframe.parent.
You cannot do GUI work before it is assigned. Frames are parts of forms for reuse, and normally need to assign some parent to them.
Move the GUI code to onshow or a procedure you call explicitely, so that the calling code can assign parent.
Or make the parent a parameter in the function.
I found this (CreateParams is called as part of CreateWnd):
procedure TCustomFrame.CreateParams(var Params: TCreateParams);
begin
inherited;
if Parent = nil then
Params.WndParent := Application.Handle;
end;
And Application.Handle = 0 so it always throws the error later in CreateWnd.
After reading this
Delphi: How to call inherited inherited ancestor on a virtual method?
I have solved it by overriding CreateParams in my frame to miss out the tCustomFrame version:
type
tCreateParamsMethod = procedure(var Params: TCreateParams) of object;
type
tMyScrollingWinControl = class(TScrollingWinControl);
procedure TDelphiFrame.CreateParams(var Params: TCreateParams);
var
Proc: tCreateParamsMethod;
begin
TMethod(Proc).Code := #TMyScrollingWinControl.CreateParams;
TMethod(Proc).Data := Self;
Proc(Params);
end;
Now it's just throwing errors when trying to set the focus on subcontrols, which I think I will fix by intercepting WM_FOCUS but we'll how it goes from here.
function CreateFrame(hwndParent: HWnd): HWnd; stdcall;
var
frame: tFrame;
begin
Result := 0;
try
frame := TDelphiFrame.CreateParented(hwndParent);
Result := frame.Handle;
except on e: Exception do
ShowMessage(e.Message);
end;
end;
You can avoid this message by assigning nil to the parent OnClose event, sometimes it works:
SomeControl.Parent := nil;//Before free your TControl
SomeControl.Free;
I think this is very cool solution. I think it is not tried before :)
I'm using a Dummy Parent (which is a Form).
function MyFrame_Create(hApplication, hwndParent:THandle; X, Y, W, H:Integer):Pointer; stdcall;
var Fr: TMyFrame;
F: TForm;
CurAppHandle: THandle;
begin
CurAppHandle:=Application.Handle;
Application.Handle:=hApplication;
//---
F:=TForm. Create(Application);//Create a dummy form
F.Position:=poDesigned;
F.Width:=0; F.Top:=0; F.Left:=-400; F.Top:=-400;//Hide Form
F.Visible:=True;
//---
Fr:=TMyFrame.Create(Application);
Fr.Parent:=F;//Set Frame's parent
//Fr.ParentWindow:=hwndParent;
Windows.SetParent(Fr.Handle, hwndParent);//Set Frame's parent window
if CurAppHandle>0 then Application.Handle:=CurAppHandle;
//---
Fr.Left:=X;
Fr.Top:=Y;
Fr.Width:=W;
Fr.Height:=H;
Result:=Fr;
end;//MyFrame_Create
procedure MyFrame_Destroy(_Fr:Pointer); stdcall;
var Fr: TMyFrame;
F: TObject;
begin
Fr:=_Fr;
F:=Fr.Parent;
Fr.Parent:=Nil;
if (F is TForm) then F.Free;
//SetParent(Fr.Handle, 0);
//Fr.ParentWindow:=0;
Fr.Free;
end;//MyFrame_Destroy
I use the standard Cut, Copy, Paste actions on my Main Menu. They have the shortcuts Ctrl-X, Ctrl-C and Ctrl-V.
When I open a modal form, e.g. FindFilesForm.ShowModal, then all the shortcuts work from the form.
But when I open a non-modal form, e.g. FindFilesForm.Show, then the shortcuts do not work.
I would think that those actions should work if the FindFilesForm is the active form. It's modality should have nothing to do with it, or am I wrong in my thinking?
Never-the-less, how can I get the shortcuts to work on a non-modal form?
After Cary's response, I researched it further. It is not a problem with certain controls, e.g. TMemo or TEdit.
But it is for some others. Specifically, the ones where it happens include:
the text in a TComboBox
the text in a TFindDialog
a TElTreeInplaceEdit control, part of LMD's ElPack
I'll see if there are others and add them to the list.
These are all on important Non-Modal forms in my program.
So I still need a solution.
Okay. I really need help with this. So this becomes the first question I am putting a bounty on.
My discussion with Cary that takes place through his answer and the comments there describe my problem in more detail.
And as I mentioned in one of those comments, a related problem seems to be discussed here.
What I need is a solution or a workaround, that will allow the Ctrl-X, Ctrl-C and Ctrl-V to always work in a TComboBox and TFindDialog in a Non-Modal window. If those two get solved, I'm sure my TElTreeInplaceEdit will work as well.
It takes only a couple of minutes to set up an simple test program as Cary describes. Hopefully someone will be able to solve this.
Just be wary that there seems to be something that allows it to work sometimes but not work other times. If I can isolate that in more detail, I'll report it here.
Thanks for any help you can offer me.
Mghie worked very hard to find a solution, and his OnExecute handler combined with his ActionListUpdate handler do the trick. So for his effort, I'm giving him the accepted solution and the bounty points.
But his actionlist update handler is not simple and you need to specify in it all the cases you want to handle. Let's say there's also Ctrl+A for select all or Ctrl-Y for undo you might want. A general procedure would be better.
So if you do come across this question in your search for the answer, try first the answer I supplied that adds an IsShortcut handler. It worked for me and should handle every case and does not need the OnExecute handlers, so is much simpler. Peter Below wrote that code and Uwe Molzhan gets finders fee.
Thanks Cary, mghie, Uwe and Peter for helping me solve this. Couldn't have done it without you. (Maybe I could have, but it might have taken me 6 months.)
OK, first thing first: This has nothing to do with modal or non-modal forms, it is a limitation of the way the Delphi action components work (if you want to call it that).
Let me prove this by a simple example: Create a new application with a new form, drop a TMemo and a TComboBox onto it, and run the application. Both controls will have the system-provided context menu with the edit commands, and will correctly react on them. They will do the same for the menu shortcuts, with the exception of Ctrl + A which isn't supported for the combo box.
Now add a TActionList component with the three standard actions for Cut, Copy and Paste. Things will still work, no changes in behaviour.
Now add a main menu, and add the Edit Menu from the template. Delete all commands but those for Cut, Copy and Paste. Set the corresponding action components for the menu items, and run the application. Observe how the combo box still has the context menu and the commands there still work, but that the shortcuts do no longer work.
The problem is that the standard edit actions have been designed to work with TCustomEdit controls only. Have a look at the TEditAction.HandlesTarget() method in StdActns.pas. Since edit controls in combo boxes, inplace editors in tree controls or edit controls in native dialogs are not caught by this they will not be handled. The menu commands will always be disabled when one of those controls has the focus. As for the shortcuts working only some of the time - this depends on whether the VCL does at some point map the shortcuts to action commands or not. If it doesn't, then they will finally reach the native window procedure and initiate the edit command. In this case the shortcuts will still work. I assume that for modal dialogs the action handling is suspended, so the behaviour is different between modal and non-modal dialogs.
To work around this you can provide handlers for OnExecute of these standard actions. For example for the Paste command:
procedure TMainForm.EditPaste1Execute(Sender: TObject);
var
FocusWnd: HWND;
begin
FocusWnd := GetFocus;
if IsWindow(FocusWnd) then
SendMessage(FocusWnd, WM_PASTE, 0, 0);
end;
and similar handlers for the Cut command (WM_CUT) and the Copy command (WM_COPY). Doing this in the little demo app makes things work again for the combo box. You should try in your application, but I assume this will help. It's a harder task to correctly enable and disable the main menu commands for all native edit controls. Maybe you could send the EM_GETSEL message to check whether the focused edit control has a selection.
Edit:
More info why the behaviour is different between combo boxes on modal vs. non-modal dialogs (analysis done on Delphi 2009): The interesting code is in TWinControl.IsMenuKey() - it tries to find an action component in one of the action lists of the parent form of the focused control which handles the shortcut. If that fails it sends a CM_APPKEYDOWN message, which ultimately leads to the same check being performed with the action lists of the application's main form. But here's the thing: This will be done only if the window handle of the application's main form is enabled (see TApplication.IsShortCut() code). Now calling ShowModal() on a form will disable all other forms, so unless the modal dialog contains itself an action with the same shortcut the native shortcut handling will work.
Edit:
I could reproduce the problem - the key is to somehow get the edit actions become disabled. In retrospect this is obvious, the Enabled property of the actions needs of course to be updated too.
Please try with this additional event handler:
procedure TForm1.ActionList1Update(Action: TBasicAction; var Handled: Boolean);
var
IsEditCtrl, HasSelection, IsReadOnly: boolean;
FocusCtrl: TWinControl;
FocusWnd: HWND;
WndClassName: string;
SelStart, SelEnd: integer;
MsgRes: LRESULT;
begin
if (Action = EditCut1) or (Action = EditCopy1) or (Action = EditPaste1) then
begin
IsEditCtrl := False;
HasSelection := False;
IsReadOnly := False;
FocusCtrl := Screen.ActiveControl;
if (FocusCtrl <> nil) and (FocusCtrl is TCustomEdit) then begin
IsEditCtrl := True;
HasSelection := TCustomEdit(FocusCtrl).SelLength > 0;
IsReadOnly := TCustomEdit(FocusCtrl).ReadOnly;
end else begin
FocusWnd := GetFocus;
if IsWindow(FocusWnd) then begin
SetLength(WndClassName, 64);
GetClassName(FocusWnd, PChar(WndClassName), 64);
WndClassName := PChar(WndClassName);
if AnsiCompareText(WndClassName, 'EDIT') = 0 then begin
IsEditCtrl := True;
SelStart := 0;
SelEnd := 0;
MsgRes := SendMessage(FocusWnd, EM_GETSEL, WPARAM(#SelStart),
LPARAM(#SelEnd));
HasSelection := (MsgRes <> 0) and (SelEnd > SelStart);
end;
end;
end;
EditCut1.Enabled := IsEditCtrl and HasSelection and not IsReadOnly;
EditCopy1.Enabled := IsEditCtrl and HasSelection;
// don't hit the clipboard three times
if Action = EditPaste1 then begin
EditPaste1.Enabled := IsEditCtrl and not IsReadOnly
and Clipboard.HasFormat(CF_TEXT);
end;
Handled := TRUE;
end;
end;
I didn't check for the native edit control being read-only, this could probably be done by adding this:
IsReadOnly := GetWindowLong(FocusWnd, GWL_STYLE) and ES_READONLY <> 0;
Note: I've given mghie the answer as he did a lot of work and his answer is correct, but I have implemented a simpler solution that I added as an answer myself
I posted a link to this question on my blog, and got a suggestion from Uwe Molzhan who is not on StackOverflow. Uwe used to run DelphiPool. He pointed me to this thread at borland.public.delphi.objectpascal:
Action List (mis)behavior.
Tom Alexander who asked the original question in this thread even said:
This behavior occurs usually, but not
all the time. Sometimes after a series
of the above errors, the behavior
starts acting as I would expect.
which is exactly the strange behaviour I've been having that has made this problem near to impossible to track down.
Peter Below responded in that thread that if there are colliding shortcuts, you have to take steps to make sure the active control gets first crack at the shortcut.
Taking his code (which was written for a frames problem) and I just had to modify “ctrl is TCustomFrame” to “ctrl is TControl” and it works perfect. So here is what was needed:
public
Function IsShortcut( var Message: TWMKey): Boolean; override;
Function TMyform.IsShortcut( var Message: TWMKey): Boolean;
Var
ctrl: TWinControl;
comp: TComponent;
i: Integer;
Begin
ctrl := ActiveControl;
If ctrl <> Nil Then Begin
Repeat
ctrl := ctrl.Parent
Until (ctrl = nil) or (ctrl Is TControl);
If ctrl <> nil Then Begin
For i:= 0 To ctrl.componentcount-1 Do Begin
comp:= ctrl.Components[i];
If comp Is TCustomActionList Then Begin
result := TCustomActionList(comp).IsShortcut( message );
If result Then
Exit;
End;
End;
End;
End;
// inherited; { Originally I had this, but it caused multiple executions }
End;
So far this seems to work in all cases for me.
The ironic thing is that it didn't work for Tom Alexander, the original question asker. What he did instead was add a procedure to the FrameEnter event that set the focus to the appropriate grid for the frame. That might imply yet another alternative solution to my question, but I have no need to explore that since Peter's solution works for me.
Also note that Peter includes in his answer an excellent summary of the complex steps of key handling that is worth knowing.
But I do want to now check mghie's edit on his answer and see if that is also a solution.
I created a very simple example with two forms in Delphi 2009 (Update 3 and Update 4 installed) running on Vista 64-bit. The second form, Form2 is displayed non-modally (Form2.Show;). I have a TMemo on Form2. Ctrl-X, Ctrl-V, and Ctrl-C work just fine.
This was before I placed a TMainMenu on Form2.
So, I placed a TMainMenu on the form, and added a TActionList. I create an Edit menu items, and added Copy, Cut, Paste submenu items. I hooked these up to the standard actions EditCopy, EditCut, and EditPaste. Still, everything works fine as before. I can either use the menu items, or the Ctrl-C, Ctrl-X, and Ctrl-V key combinations.
There must be something else going on here.
I have an TUpDown control whose Associate is set to an instance of a TEdit subclass. The edit class calls RecreateWnd in its overriden DoEnter method. Unfortunately this kills the buddy connection at the API level which leads to strange behavior e.g. when clicking on the updown arrows.
My problem is that the edit instance doesn't know that it is the buddy of some updown to which it should reconnect and the updown isn't notified of the loss of its buddy. Any ideas how I could reconnect the two?
I noticed how TCustomUpDown.SetAssociate checks that updown and buddy have the same parent and uses this to avoid duplicate associations. So I tried calling my own RecreateWnd method:
procedure TAlignedEdit.RecreateWnd;
var
i: Integer;
c: TControl;
ud: TCustomUpDown;
begin
ud := nil;
for i := 0 to Pred(Parent.ControlCount) do
begin
c := Parent.Controls[i];
if c is TCustomUpDown then
if THACK_CustomUpDown(c).Associate = Self then
begin
ud := TCustomUpDown(c);
Break;
end;
end;
inherited RecreateWnd;
if Assigned(ud) then
begin
THACK_CustomUpDown(ud).Associate := nil;
THACK_CustomUpDown(ud).Associate := Self;
end;
end;
et voila - it works!
You've discovered something rather unfortunate. You set up an association between two controls at the application level, so you should be able to continue to manage that association in application-level code, but the VCL doesn't provide the framework necessary for maintaining that. Ideally, there would be a generic association framework, so associated controls could notify each other that they should update themselves.
The VCL has the beginnings of that, with the Notification method, but that only notifies of components being destroyed.
I think your proposed solution is a little too specific to the task. An edit control shouldn't necessarily know that it's attached to an up-down control, and even if it does, they shouldn't be required to share a parent. On the other hand, writing an entire generic observer framework for this problem would be overkill. I propose a compromise.
Start with a new event property on the edit control:
property OnRecreateWnd: TNotifyEvent read FOnRecreateWnd write FOnRecreateWnd;
Then override RecreateWnd as you did above, but instead of all the up-down-control-specific code, simply trigger the event:
procedure TAlignedEdit.RecreateWnd;
begin
inherited;
if Assigned(OnRecreateWnd) then
OnRecreateWnd(Self);
end;
Now, handle that event in your application code, where you know exactly which controls are associated with each other, so you don't have to search for anything, and you don't need to require any parent-child relationships:
procedure TUlrichForm.AlignedEdit1RecreateWnd(Sender: TObject);
begin
Assert(Sender = AlignedEdit1);
UpDown1.Associate := nil;
UpDown1.Associate := AlignedEdit1;
end;
Try storing the value of the Associate property in a local variable before you call RecreateWnd, then setting it back afterwards.