What can I do that shortcuts for menu items don't overwrite those from local controls?
Imagine this simple app in the screenshot. It has one "undo" menu item with the shortcut CTRL+Z (Strg+Z in German) assigned to it. When I edit some text in the memo and press CTRL+Z I assume that the last input in the memo is reverted, but instead the menu item is executed.
This is especially bad in this fictional application because the undo function will now delete my last added "Item 3" which properties I was editing.
CTRL+Z is just an example. Other popular shortcuts cause similar problems (Copy&Paste: CTRL+X/C/V, Select all: CTRL+A).
Mini Demo with menu item with CTRL+Z short-cut http://img31.imageshack.us/img31/9074/ctrlzproblem.png
The VCL is designed to give menu item shortcuts precedence. You can, however, write your item click handler (or action execute handler) to do some special handling when ActiveControl is TCustomEdit (call Undo, etc.)
Edit: I understand you don't like handling all possible special cases in many places in your code (all menu item or action handlers). I'm afraid I can't give you a completely satisfactory answer but perhaps this will help you find a bit more generic solution. Try the following OnShortCut event handler on your form:
procedure TMyForm.FormShortCut(var Msg: TWMKey; var Handled: Boolean);
var
Message: TMessage absolute Msg;
Shift: TShiftState;
begin
Handled := False;
if ActiveControl is TCustomEdit then
begin
Shift := KeyDataToShiftState(Msg.KeyData);
// add more cases if needed
Handled := (Shift = [ssCtrl]) and (Msg.CharCode in [Ord('C'), Ord('X'), Ord('V'), Ord('Z')]);
if Handled then
TCustomEdit(ActiveControl).DefaultHandler(Message);
end
else if ActiveControl is ... then ... // add more cases as needed
end;
You could also override IsShortCut method in a similar way and derive your project's forms from this new TCustomForm descendant.
You probably need an alike solution as below. Yes, feels cumbersome but this is the easiest way I could think of at the time. If only Delphi allowed duck-typing!
{ you need to derive a class supporting this interface
for every distinct control type your UI contains }
IEditOperations = interface(IInterface)
['{C5342AAA-6D62-4654-BF73-B767267CB583}']
function CanCut: boolean;
function CanCopy: boolean;
function CanPaste: boolean;
function CanDelete: boolean;
function CanUndo: boolean;
function CanRedo: boolean;
function CanSelectAll: Boolean;
procedure CutToClipBoard;
procedure Paste;
procedure CopyToClipboard;
procedure Delete;
procedure Undo;
procedure Redo;
procedure SelectAll;
end;
// actions....
procedure TMainDataModule.actEditCutUpdate(Sender: TObject);
var intf: IEditOperations;
begin
if Supports(Screen.ActiveControl, IEditOperations, intf) then
(Sender as TAction).Enabled := intf.CanCut
else
(Sender as TAction).Enabled := False;
end;
procedure TMainDataModule.actEditCutExecute(Sender: TObject);
var intf: IEditOperations;
begin
if Supports(Screen.ActiveControl, IEditOperations, intf) then
intf.CutToClipBoard;
end;
....
Related
I am generally satisified with functionality of TEditCut, TEditCopy, TEditPaste and TEditSelectAll except they don't work on any controls which are not standard.
For example, they may work fine on TEdit or TMemo controls, but not on TEmbeddedWB — with it, standard actions are always disabled regardless of whether text is selected, even though TEmbeddedWB has methods like CopyToClipboard and SelectAll.
How can I make standard actions work with TEmbeddedWB? How do standard actions determine whether they should be enabled or disabled (and in what event they do it — is it in the OnUpdate event)? Can I extend standard actions to add support for unrecognized components, or do I need to write their replacement?
The default Edit actions do not work on a TEmbeddedWB control, because that component does not descend from TCustomEdit. TEditAction, which TEditSelectAll descends from, only knows how to handle TCustomEdits.
Use the OnUpdate and OnExecute events of the action to override this behaviour. Note that the default behaviour then will be ignored, so implement that manually. Here an example for a TEditSelectAll action.
procedure TForm1.EditSelectAll1Update(Sender: TObject);
begin
EditSelectAll1.Enabled := (Screen.ActiveControl is TEmbeddedWB) or
EditSelectAll1.HandlesTarget(ActiveControl)
end;
procedure TForm1.EditSelectAll1Execute(Sender: TObject);
begin
if ActiveControl is TEmbeddedWB then
TEmbeddedWB(Screen.ActiveControl).SelectAll
else
EditSelectAll1.ExecuteTarget(Screen.ActiveControl);
end;
Or use the same events of the ActionList (or OnActionUpdate and OnActionExecute of an ApplicationEvents component) to centralize this custom behaviour:
procedure TForm1.ActionList1Update(Action: TBasicAction; var Handled: Boolean);
begin
if Action is TEditAction then
begin
TCustomAction(Action).Enabled := (Screen.ActiveControl is TEmbeddedWB) or
Action.HandlesTarget(Screen.ActiveControl);
Handled := True;
end;
end;
procedure TForm1.ActionList1Execute(Action: TBasicAction; var Handled: Boolean);
begin
if (Action is TEditSelectAll) and (Screen.ActiveControl is TEmbeddedWB) then
begin
TEmbeddedWB(Screen.ActiveControl).SelectAll;
Handled := True;
end;
end;
Delphi 2010
I am using a OnFormKeyDown event on my main form, and I basically use the same event on a Dialog
//main form
procedure TfrmMain.FormKeyDown(Sender: TObject; var Key: Word;
Shift: TShiftState);
begin
case Key of
VK_DOWN: btnLast.OnClick(Self);
VK_Up: btnFirst.OnClick(Self);
VK_Left: btnPrev.OnClick(Self);
VK_Right: btnNext.OnClick(Self);
end;
end;
procedure TfrmMain.mniShowOwnedClick(Sender: TObject);
var
I: Integer;
begin
frmMain.KeyPreview:= False;
frmOwned.KeyPreview:= True;
frmOwned.Owned2.Clear;
for I := 0 to Tags.Count - 1 do
if Owned.IndexOf(Tags.Names[I]) <> -1 then
frmOwned.Owned2.Add(Tags[I]);
if frmOwned.ShowModal = mrOK then
begin
frmMain.KeyPreview:= True;
frmOwned.KeyPreview:= False;
end;
end;
//Dialog
procedure TfrmOwned.FormKeyDown(Sender: TObject; var Key: Word;
Shift: TShiftState);
begin
case Key of
VK_DOWN: btnLast.OnClick(Self);
VK_Up: btnFirst.OnClick(Self);
VK_Left: btnPrev.OnClick(Self);
VK_Right: btnNext.OnClick(Self);
end;
end;
The Form's OnKeyDown works fine, but I can't seem to get the dialogs to work
The problem is that these keys are used as dialog navigation keys. And as such, they never make their way to the OnKeyDown event.
To be honest I had a hard time understanding why they are firing for your main form's OnKeyDown event. I could not make that happen in my test environment. That's because I had added a button to the form. That's enough to mean that the arrow keys are treated as navigation keys. Try creating a an app with a single form and adding a few buttons. Then run the app and use the arrow keys to move the focus between the buttons. That's what I mean when I say that the arrow keys are treated as navigation keys.
I expect that the difference between your two forms is that the main form has nothing that can be navigated around by arrow keys, but the modal form does.
Now, you could stop the arrow keys being treated as navigation keys. Like this:
type
TMyForm = class(TForm)
....
protected
procedure CMDialogKey(var Message: TCMDialogKey); message CM_DIALOGKEY;
....
end;
....
procedure TMyForm.CMDialogKey(var Message: TCMDialogKey);
begin
case Message.CharCode of
VK_LEFT, VK_RIGHT, VK_UP, VK_DOWN:
Message.Result := 0;
else
inherited;
end;
end;
However, a better solution, in my view, is to stop trying to implement shortcuts using OnKeyDown events. That seems like the wrong solution. The right solution is to use actions. Create an action list. Add actions for first, last, previous and next actions. Give them the appropriate ShortCut properties. Assign those actions to your buttons. And the job is done.
One of the benefits of this is that you can stop trying to fake button click events. For what it is worth, calling OnClick is the wrong way to do that. Call the button's Click method if ever your really need to do that. However, use an action and it's all taken care of.
Another benefit is that you'll no longer need to pfaff around with KeyPreview. Simply put, if you want to implement short cuts, use TAction.ShortCut.
I have added hints to components on my form. When the components receive the focus, I'd like to set the caption of a label component to display the hint.
I have added a TApplicationEvents object and set the OnShowHint event to
procedure TImportFrm.ApplicationEvents1ShowHint(var HintStr: string;
var CanShow: Boolean; var HintInfo: THintInfo);
begin
HelpLbl.Caption := HintStr;
end;
However it seems that the ShowHint event only fires with mouse movements. Is there a way to fire the hint code when components receive focus, without having to implement the OnEnter event for every single component on the form?
Add a handler for TScreen.OnActiveControlChange in your main form's creation, and handle the hints in that event:
type
TForm2=class(TForm)
...
private
procedure ScreenFocusControlChange(Sender: TObject);
end;
implementation
procedure TForm2.FormCreate(Sender: TObject);
begin
Screen.OnActiveControlChange := ScreenFocusControlChange;
end;
procedure TForm2.ScreenFocusControlChange(Sender: TObject);
begin
Label1.Caption := ActiveControl.Hint;
Label1.Update;
end;
Note that Sender won't do you much good; it's always Screen. You can filter (for instance, to only change the Label.Caption for edit controls) by testing the ActiveControl:
if (ActiveControl is TEdit) then
// Update caption of label with ActiveControl.Hint
Note that if you'll need to reassign the event when you show child forms (to an event on that child form), or you'll always be updating the original form's label with the hints. The easiest way to do the reassignment is to give every form an OnActiveControlChange handler, and assign it in the form's OnActivate event and unassign it in the OnDeactivate event:
procedure TForm1.FormActivate(Sender: TObject);
begin
Screen.OnActiveControlChange := Self.ScreenActiveControlChange;
end;
procedure TForm1.FormDeactivate(Sender: TObject);
begin
Screen.OnActiveControlChange := nil;
end;
This will allow you to update controls other than Label1 on each form, and only use the hint changes on forms you want to do so.
A simple solution is to use OnIdle event:
procedure TForm1.ApplicationEvents1Idle(Sender: TObject; var Done: Boolean);
begin
if Assigned(ActiveControl) then
Label1.Caption:= ActiveControl.Hint;
end;
A more advanced solution is to override protected ActiveChanged method of TForm:
type
TForm1 = class(TForm)
...
protected
procedure ActiveChanged; override;
end;
...
procedure TForm1.ActiveChanged;
begin
inherited;
if Assigned(ActiveControl) then
Label1.Caption:= ActiveControl.Hint;
end;
Receiving focus and OnShowHint are quite different events; OnShowHint can be triggered for non-focused control as well.
Why would you need to implement the OnEnter event for every single component? You can create one generic method / event handler like:
procedure TForm1.AnyControlEnter(Sender: TObject);
begin
lbl1.Caption := TControl(Sender).Hint;
end;
and assign it to every component you placed on the form.
You said:
it seems that the ShowHint event only fires with mouse movements
This is a normal behaviour. The problem you have ( it's a guess) is that hints are not fired directly. Don't try to make a workaround, what you try to do with MouseEnter is exactly what is already happening...the only difference is that you'be forget something...
Keep the event ApplicationEvents1ShowHint() as you've initially done but add this in the form constructor event:
Application.HintPause := 1;
And then hints will be displayed (almost) without delay.
I'm in the middle of a project with a number of child forms. Many of the forms may be open at once. I'd like to know if there's already something I can use to manage and keep track of these forms, much like the windows taskbar and/or task manager. If not, then what would be the best approach? I don't want to have to reinvent the wheel if this is already done.
Description
As mentioned above, this project has many forms which may be opened at once. I will also be implementing some visual list control (much like the taskbar or task manager) for user control of these forms (or in the user's case, the forms are called windows). The most ideal way to manage these would be to first capture each of these forms as they're created and keep record of them somewhere. Some forms need this behavior, and some forms do not. For example, modal forms will never need this handling.
I will be giving the user access to show, minimize, or close these forms, as well as some other future un-thought handling, like maybe a custom popup menu associated with one of these forms (but that's another subject). The point is, I need to build something to capture these forms and keep them in order.
This will also include some other user interaction with all the forms at once, as well as simple access to each one of them, similar to how Screen.Forms already works. For example, a command to minimize all forms (FormManager.MinimizeAll), to maximize the currently active form (FormManager.ActiveForm.Maximize), or with a particular form (FormManager[3].Maximize).
Possible Options
I understand there are a few far different approaches to accomplish similar results, and haven't started coding it yet because each of those approaches has a different starting point. The options are...
Wrap Screen.Forms and other associated functionality from the Screen (which wouldn't allow too much of my desired flexibility)
Every time I create a form, register it with this form manager (which is very flexible, but I have to make sure I always register each created form)
Build a master form to register its self with the form manager and inherit everything from it (which is also very flexible, but in different ways, and much more complex)
The second option is sounding the most promising so far. But again, I don't want to start building it if there is already a solution for this. I'm pretty confident that I'm not the first person to do this. I don't know how to search for such a thing, I get nothing related to what I want on Google.
The global variable Screen (in Forms unit) does some "tracking", ie
Screen.Forms list all currently open forms;
Screen.ActiveForm form which has input focus (see also FocusedForm);
Screen.OnActiveFormChange event;
You could add each form to a TObjectList. I wrote a component called FormStack, which allows you to add forms (even forms with the same name), retrieve, remove, etc. To get a Task Manager like behavior, I think you'd just need to iterate the list to obtain form names . Hopefully you can use something here to shed some light on your idea..
Here's the code for FormStack.
unit uFormstack;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls, Contnrs;
type
TFormstack = class(TComponent)
private
{ Private declarations }
FormList: TObjectList;
protected
{ Protected declarations }
public
{ Public declarations }
Constructor Create(AOwner: TComponent); Override;
Destructor Destroy; Override;
Procedure Add(InstanceClass: TComponentClass; Var Reference);
Procedure RemoveLast;
Procedure RemoveAll;
Function FindForm(AComponentClass: TComponentClass): Boolean;
Function GetForm(AComponentClass: TComponentClass): TObject;
Function GetByIndex(AIndex: Integer): TObject;
Procedure RemoveByIndex(AIndex: Integer);
published
{ Published declarations }
end;
procedure Register;
implementation
//{$R *.res}
procedure Register;
begin
RegisterComponents('FormStack', [TFormstack]);
end;
{-----------------------------------------------------------------------------
TFormStack
-----------------------------------------------------------------------------}
Constructor TFormStack.Create(AOwner: TComponent);
Begin
Inherited Create(AOwner);
FormList := TObjectList.Create;
FormList.OwnsObjects := True;
End;
Destructor TFormStack.Destroy;
Begin
FormList.Free;
Inherited Destroy;
End;
Procedure TFormStack.Add(InstanceClass: TComponentClass; Var Reference);
Var
Instance: TComponent;
Begin
Instance := TComponent(InstanceClass.NewInstance);
TComponent(Reference) := Instance;
Instance.Create(Self); // Owner is FormList <<-- blows up if datamodule in D2010
FormList.Add(Instance);
Instance.Tag := FormList.Count-1;
End;
Procedure TFormStack.RemoveAll;
Var
I: Integer;
Begin
For I := FormList.Count -1 downto 0 do // last in first out
begin
Self.RemoveLast;
End;
End;
// This removes the last form on the stack
Procedure TFormStack.RemoveLast;
Begin
if FormList.Count > 0 then
FormList.Remove(FormList.Items[FormList.Count-1]);
End;
Function TFormStack.FindForm(AComponentClass: TComponentClass): Boolean;
Var
I: Integer;
Begin
Result := False;
For I := FormList.Count-1 downto 0 do
If Formlist.Items[I].ClassType = AComponentClass then
Result := True;
End;
Function TFormStack.GetForm(AComponentClass: TComponentClass): TObject;
Var
I: Integer;
begin
Result := Nil;
For I := FormList.Count-1 downto 0 do
If Formlist.Items[I].ClassType = AComponentClass then
Result := FormList.Items[I];
end;
Function TFormStack.GetByIndex(AIndex: Integer): TObject;
begin
Result := Nil;
If FormList.Count-1 >= AIndex then
Result := FormList.Items[AIndex];
end;
Procedure TFormStack.RemoveByIndex(AIndex: Integer);
begin
If FormList.Count-1 >= AIndex then
FormList.Remove(FormList.Items[AIndex]);
end;
end.
If I understand you correctly, you want to track this in code while the app is running?
Maybe you can do something with Screen.Forms?
I've got a pretty big setup form which I'd like to populate with data from a class. so I'm doing a lot of
Edt1.text := ASettings.FirstThing;
I'd like to avoid
Edt1.onchange := nil;
Edt1.text := ASettings.FirstThing;
Edt1.onchange := edt1Onchange;
How do I change the text in a text box and sidestep the onchange event.
I have used something like changing the OnChange handler, but more often, I use a flag.
updatingFromCode := true;
Edt1.Text := ASettings.FirstThing;
updatingFromCode := false;
then
procedure TForm1.OnChange(...);
begin
if updatingFromCode then
Exit;
...
Also, rather than hardcoding the OnChange the the actual OnChange procedure, I would store the Edit control's current value, then reset it (which will work if it is not set, or if another place has changed it, etc.)
oldOnChange := Edt1.OnChange;
Edt1.OnChange := nil;
Edt1.Text := ASettings.FirstThing;
Edt1.OnChange := oldOnChange;
You might consider using an object to manage the NIL'ing of the event and restoring the previously installed event handler. It's a little dangerous to assume that the event to be restored just happens to be the one assigned at design-time/which happens to have the "name that fits" - you should always save/restore the currently assigned handler, just to be safe.
This would provide an even more re-usable utility than the SetTextWithoutOnChange() routine:
TSuspendEvent = class
private
fObject: TObject;
fEvent: String;
fHandler: TMethod;
public
constructor Create(const aObject: TObject; aEvent: String);
destructor Destroy; override;
end;
constructor TSuspendEvent.Create(const aObject: TObject; aEvent: String);
const
NILEvent : TMethod = (Code: NIL; Data: NIL);
begin
inherited Create;
fObject := aObject;
fEvent := aEvent;
fHandler := GetMethodProp(aObject, aEvent);
SetMethodProp(aObject, aEvent, NILEvent);
end;
destructor TSuspendEvent.Destroy;
begin
SetMethodProp(fObject, fEvent, fHandler);
inherited;
end;
In usage, this would look something like:
with TSuspendEvent.Create(Edit1, 'OnChange') do
try
Edit1.Text := 'Reset!';
finally
Free;
end;
For the "Thou shalt not use 'with' crowd" - by all means declare yourself an additional local variable and use that if it will help you sleep easier at night. :)
Or, to make it even more convenient to use and eliminate "with", I would make the TSuspendEvent class an interfaced object and wrap its use in a function that yielded an interface reference to it that could be allowed to "live in scope", as exemplified by my AutoFree() implementation. In fact, you could use AutoFree() as-is to manage this already:
AutoFree(TSuspendEvent.Create(Edit1, 'OnChange'));
Edit1.Text := 'Reset!';
Dsabling events for a period that extends beyond the scope of a single procedure requires more management than any helper utilities are likely to be able to provide in a generic fashion I think, at least not without also having specific means for restoring events explicitly, rather than automatically.
If you simply wrapped TSuspendEvent inside it's own interface yielding function, following the same pattern as AutoFree() you could simplify this further to:
SuspendEvent(Edit1, 'OnChange');
Edit1.Text := 'Reset!';
As a final note, I think it should be fairly easy to see how this could be quite simply extended to support suspending multiple events on an object in a single call, if required, for example:
SuspendEvent(Edit1, ['OnChange', 'OnEnter']);
As far as I know if the OnChange of your object is designed to fire when the Text property is changed you have to stick with setting the event to nil temporarly. Myself, I do it this way (in a try finally):
Edt1.onchange := nil;
try
Edt1.text := ASettings.FirstThing;
finally
Edt1.onchange := edt1Onchange;
end;
You could also do some procedure to handle it for you:
procedure SetTextWithoutOnChange(anEdit: TEdit; str: String);
var
anEvent: TNotifyEvent;
begin
anEvent := anEdit.OnChange;
anEdit.OnChange := nil;
try
anEdit.Text := str;
finally
anEdit.OnChange := anEvent;
end;
end;
I know this is an old question but I thought I would add my solution that does not involve any of the complicated procedures outlined in the previous answers in case it comes up in another search.
The problem is the onChange event itself. I don't use it at all for text fields.
remove all OnChange and use the OnExit instead and tie it to the OnKeyUp.
All Edits have a common ancestor TCustomEdit.
I generally use one method called CustomEditKeyUp and point all the edits on a form to this single method (TEdit, TLabeledEdit etc etc.)
type THack = class(TCustomEdit);
procedure TForm1.CustomeEditKeyUP(Sender: TObject; var Key: Word;
Shift: TShiftState);
begin
if (Key=VK_RETURN) and (Sender is TCustomEdit) then
{This is just to isolate the one occasion when they press the
enter but don't exit immediately}
if Assigned(THack(Sender).OnExit) then THack(Sender).OnExit(Sender);
end;
For some reason, the OnExit is private in a TCustomEdit so the Hack is needed. If you know that the edits are from a different route where the OnExit is public, cast if differently and the Hack is not necessary.
Then For each Edit control, use a specific OnExit
procedure TForm1.MyEditExit(Sender: TObject);
begin
if MyEdit.Modified then
begin
{Do Something here}
MyEdit.Modified := false;
end;
end;
If you want to change the value programmatically without it firing 'OnExit'
....
MyEdit.Text :='I've changed it'
MyEdit.Modified := false;
....
The big advantage for me is that when I am parsing the input for say a valid number, I only have to do this once when editing is completed and not for every single backspace, delete insert etc all surrounded by try except as the various formating functions error out as they don't understand spaces etc.. For database etc, the number of calls will be greatly reduced.
That's my two penneth.
Another way is by using Class Helpers introduced in Delphi 8.
http://docwiki.embarcadero.com/RADStudio/Tokyo/en/Class_and_Record_Helpers_(Delphi)
You could write:
type
TEditHelper = class helper for TEdit
public
procedure SetTextDisableOnChange(const AText: string);
end;
{ TEditHelper }
procedure TEditHelper.SetTextDisableOnChange(const AText: string);
var
OnChangeTmp: TNotifyEvent;
begin
OnChangeTmp:=OnChange;
try
OnChange:=nil;
Text:=AText;
finally
OnChange:=OnChangeTmp;
end;
end;
and then:
EditCtrl.SetTextDisableOnChange('I do not invoke OnChange!');
EditCtrl.Text:='I invoke OnChange';