Delphi. How to Disable/Enable controls without triggering controls events - delphi

I have a DataSet (TZQuery), which has several boolean fields, that have TDBCheckBoxes assigned to them.
These CheckBoxes have "OnClick" events assigned to them and they are triggered whenever I change field values (which are assigned to checkboxes).
The problem is that I do not need these events triggerred, during many operations i do with the dataset.
I've tried calling DataSet.DisableControls, but then events are called right after i call DataSet.EnableControls.
So my question is - is there a way to disable triggering Data-aware controls events.
Edit (bigger picture):
If an exception happens while let's say saving data, i have to load the default values (or the values i've had before saving it). Now while loading that data, all these events (TDBCheckBoxes and other data-aware controls) are triggered, which do all sorts of operations which create lag and sometimes even unwanted changes of data, i'm looking for an universal solution of disabling them all for a short period of time.

Building on Guillem's post:
Turn off everything:
Traverse each component on the form with the for-loop, shown below, changing the properties to the desired value.
If you want to later revert back to the original property values, then you must save the original value (as OldEvent is used below.)
Edit: The code below shows the key concept being discussed. If components are being added or deleted at run-time, or if you'd like to use the absolutely least amount of memory, then use a dynamic array, and as Pieter suggests, store pointers to the components rather than indexing to them.
const
MAX_COMPONENTS_ON_PAGE = 100; // arbitrarily larger than what you'd expect. (Use a dynamic array if this worries you.
var
OldEvent: Array[0.. MAX_COMPONENTS_ON_PAGE - 1] of TNotifyEvent; // save original values here
i: Integer;
begin
for i := 0 to ComponentCount - 1 do
begin
if (Components[i] is TCheckBox) then
begin
OldEvent[i] := TCheckBox(Components[i]).OnClick; // remember old state
TCheckBox(Components[i]).OnClick := nil;
end
else if (Components[i] is TEdit) then
begin
OldEvent[i] := TEdit(Components[i]).OnClick; // remember old state
TEdit(Components[i]).OnClick := nil;
end;
end;
Revert to former values
for i := 0 to ComponentCount - 1 do
begin
if (Components[i] is TCheckBox) then
TCheckBox(Components[i]).OnClick := OldEvent[i]
else if (Components[i] is TEdit) then
TEdit(Components[i]).OnClick := OldEvent[i];
end;
There may be a way to fold all of the if-statements into one generic test that answers "Does this component have an OnClickEvent" -- but I don't know what it is.
Hopefully someone will constructively criticize my answer (rather than just down voting it.) But, hopefully what I've shown above will be workable.

One way to do this is following:
var
Event : TNotifyEvent;
begin
Event := myCheckbox.OnClick;
try
myCheckbox.OnClick := nil;
//your code here
finally
myCheckbox.OnClick := Event;
end;
end;
HTH

The internal design of the TCustomCheckBox is that it triggers the Click method every time the Checked property if changed. Be it by actually clicking it or setting it in code. And this is happening here when you call EnableControls because the control gets updated to display the value of the linked field in your dataset.
TButtonControl (which is what TCustomCheckBox inherits from) has the property ClicksDisabled. Use this instead of (or in addition to) the DisableControls/EnableControls call. Unfortunately it is protected and not made public by TCustomCheckBox but you can use a small hack to access it:
type
TButtonControlAccess = class(TButtonControl)
public
property ClicksDisabled;
end;
...
TButtonControlAccess(MyCheckBox1).ClicksDisabled := True;
// do some dataset stuff
TButtonControlAccess(MyCheckBox1).ClicksDisabled := False;
Of course you can put this into a method that checks all components and sets this property if the control inherits from TCustomCheckBox or some other criteria.

Related

Shortcut triggers TAction on first created form instead of form with focus

I found (in Delphi 2010) that shortcuts always end up on first form (as owned by main form) that has that action, but not the currently focused form. My TMainFrm owns several TViewFrm. Each has a TActionManager with the same TActons.
I see some ways out, but wonder whats the best fix.. (and not a bad hack)
The forms are navigated using a tabset which calls their Hide() and Show(). I'd did not expect hidden forms to receive keypresses. Am i doing something wrong?
It seems that action shortcuts are always start at the main form, and using TCustomForm.IsShortCut() get distributed to owned forms. I see no logic there to respect hidden windows, should i override it and have it trigger the focused form first?
Disabling all TActions in TViewFrm.Hide() .. ?
Moving the TActionToolBar to TMainFrm but that is a pit of snakes and last resort.
I have found a workaround thats good enough for me; my main form now overrides TCustomForm.IsShortcut() and first checks visible windows from my list of editor tabs.
A list which i conveniently already have, so this might not work for everyone.
// Override TCustomForm and make it check the currently focused tab/window first.
function TFormMain.IsShortCut(var Message: TWMKey): Boolean;
function DispatchShortCut(const Owner: TComponent) : Boolean; // copied function unchanged
var
I: Integer;
Component: TComponent;
begin
Result := False;
{ Dispatch to all children }
for I := 0 to Owner.ComponentCount - 1 do
begin
Component := Owner.Components[I];
if Component is TCustomActionList then
begin
if TCustomActionList(Component).IsShortCut(Message) then
begin
Result := True;
Exit;
end
end
else
begin
Result := DispatchShortCut(Component);
if Result then
Break;
end
end;
end;
var
form : TForm;
begin
Result := False;
// Check my menu
Result := Result or (Menu <> nil) and (Menu.WindowHandle <> 0) and
Menu.IsShortCut(Message);
// Check currently focused form <------------------- (the fix)
for form in FEditorTabs do
if form.Visible then
begin
Result := DispatchShortCut(form);
if Result then Break;
end;
// ^ wont work using GetActiveWindow() because it always returns Self.
// Check all owned components/forms (the normal behaviour)
if not Result then
Result := inherited IsShortCut(Message);
end;
Another solution would be to change DispatchShortCut() to check for components being visible and/or enabled, but that might impact more than i'd like. I wonder whether the original code architects had a reason not to -- by design. Best would be have it called twice: first to give priority to visible+enabled components, and second call as fallback to normal behavior.

Delphi: TreeView elements vs. Form position changing

We have found something is seems to be a bug(?), and cause bug in our code.
Delphi XE3, Win32. Two forms, the main have button:
procedure TForm4.Button1Click(Sender: TObject);
begin
with TForm1.Create(Application) do
begin
ShowModal;
Release;
end;
end;
The Form1 is doing this:
procedure TForm1.FormCreate(Sender: TObject);
var
i, j: integer;
mn: TTreeNode;
begin
for i := 1 to 10 do
begin
mn := TreeView1.Items.Add(nil, 'M' + IntToStr(i));
for j := 1 to 10 do
begin
TreeView1.Items.AddChild(mn, 'C' + IntToStr(j));
end;
end;
Position := poDesigned;
beep;
Caption := IntToStr(TreeView1.Items.Count);
end;
After this I get 0 elements in caption.
But when I have a button in that form with this code...
procedure TForm1.Button1Click(Sender: TObject);
begin
Caption := IntToStr(TreeView1.Items.Count);
end;
...
Then I can see the good number (110 elements).
If I write TreeView1.Handleneeded after Position changing, the count is also good.
The problem is based on RecreateWnd, which calls DestroyHandles.
But they will be repaired only in Show (in the Activate event I can see good result too).
TreeView is a special control, because the tree elements are children, and the count is calculated by on them, no matter it have real sub-object list.
The main problem that ReCreateWnd called often by another methods to, so it can cause problems in another sections too, and I cannot put HandleNeeded before all .Count calculation.
(We have special base form that correct the Position to poDesigned if it was poScreenCenter to it can be positionable later. This happens after FormCreate call, in an inner method. We found this problem only with these kind of forms, but later we could reproduce it in a simple code too)
So the question is - what is the global solution to this problem?
(Did you experience this in XE5 too?)
Thank you for all help, info, doc.
The Form's HWND is getting destroyed when you set the Position. That also destroys all child HWNDs. The TreeView's HWND has not been re-created yet when you read its Count, which is why it reports 0. Calling TreeView.HandleNeeded after setting the Position forces the TreeView to re-create its HWND immediately, which will re-load any TreeNodes that had beencached internally when the TreeView's HWND was destroyed (but only if the TreeView.CreateWndRestores property is True, which it is by default).
A TreeView stores its child nodes inside of its HWND. Reading the Items.Count merely asks the HWND how many nodes it has, so if there is no HWND then the Count will be 0. TTreeView does not keep its own list of TTreeNode objects, it merely assigns them as user-defined data in the physical nodes themselves. When nodes are removed from the tree, TTreeView frees the associated TTreeNode objects. In the case of HWND recreation, TTreeView caches the TTreeNode data and then re-assigns it back to new nodes when the HWND is re-created. But again, it does not keep track of the TTreeNode objects.
What TTreeView could have done is store the current number of nodes during HWND destruction, and then have Items.Count return that value if the HWND has not been re-created yet. But alas, TTreeView does not do that. But you could implement that manually by subclassing TTreeView to intercept the CreateWnd() and DestroyWnd() methods, and then write your own function that returns the actual Items.Count when HandleAllocated is true and returns your cached value if it is false.
If the Form is visible to the user then its HWND (and the HWND of its children) will be available, since a control is not visible without an HWND, so Items.Count will be available if the TTreeView is visible to the user. If its HWND is ever destroyed while visible, the VCL will re-create the HWND immediately so the user does not see a missing control. However, if the Form (or TreeView) is not visible to the user when the HWND is destroyed, the HWND will not be re-created until it is actually needed when the Form/TreeView is made visible again.
Since you are forcing the Position to always be poDesigned at the time of Form creation, why not just set the Position to poDesigned at design-time? Otherwise, you can simply set the Position before populating the TreeView instead of afterwards, at least:
procedure TForm1.FormCreate(Sender: TObject);
var
i, j: integer;
mn: TTreeNode;
begin
Position := poDesigned; // <-- here
for i := 1 to 10 do
begin
mn := TreeView1.Items.Add(nil, 'M' + IntToStr(i)); // <-- first call to Add() forces HWND recreation if needed
for j := 1 to 10 do
begin
TreeView1.Items.AddChild(mn, 'C' + IntToStr(j));
end;
end;
Beep;
Caption := IntToStr(TreeView1.Items.Count); // <-- correct value reported
end;
This happens in all versions of Delphi. This is simply how things work in the VCL.
On a side note, you should use Free() instead of Release(). Release() is meant to be used only when a Form needs to Free() itself, such as in an event handler, by delaying the Free() until the Form becomes idle. Since the form is already closed and idle by the time ShowModal() exits, it is safe to Free() the Form immediately.
I think you are over-reacting. You state:
The main problem that ReCreateWnd called often by another methods to, so it can cause problems in another sections too, and I cannot put HandleNeeded before all .Count calculation.
But then in comments you say that these other scenarios are:
CMCtlD3Changed CMSysColorChange BORDERSTYLE SetAxBorderStyle SetBorderIcons Dock SetPosition SetPopupMode set_PopupParent RecreateAsPopup ShowModal SetMainFormOnTaskBar
Indeed these methods will cause window recreation. But why does that matter for you? Do you routinely assign to MainFormOnTaskBar and then immediately request the count of items in your tree view? Do you change Ctl3D regularly? Are you changing BorderIcons dynamically? I very much doubt it.
So I think you need to tackle the immediate problem which relates to the timing of the setting of Position. I would deal with that by making sure that Position is set before you populate the tree view. Do that by either setting Position at design time, or simply before you populate the tree view.
Of course, there may well be other problem related to window creation. You'll need to treat them on a case by case basis. I suspect that you are hoping for some sort of magic switch that just makes this issue disappear. I don't believe that there is one. If you try to read the item count before the handle is created, then you will suffer from this problem. The solution is not to read the item count before the handle is created.

accessing components of frame1 with frame2, with frame2 being on frame1, in delphi, frames are created dynamically

Originally the question had to be how to access the components in the first place, however I somehow managed to figure it out. I am just learning Delphi so I am prone to dumb and obvious questions. I am also at a stage when I'm not actually writing anything useful, but just messing with random stuff to see how it works and maybe learn something.
Wall of text incoming, i want to explain what i am exploring at the moment...
Basically i have a form1 with a button1, pressing it creates a frame2, that frame2 has a button2, pressing button2 creates a frame3 within frame2 (it being frame3's parent and owner). Each frame has an another freeandnil-button. Upon pressing each button1/2/3, it gets disabled to prevent creating multiple instances. My original problem was that after using freeandnil-button, I couldnt access the button on the previous frame (it worked fine for forms, form1.button1.enabled:=true works fine from within frame2) that got disabled in order to re-enable it (frame2.button1.enabled:=true from within frame3 creates an access violation, I think).
Suppose I write something in the future that requires such communication? So I added an editbox to each frame, with a button on the other to change the editbox text, this is my current working solution:
procedure TFrame2.Button3Click(Sender: TObject);
var i,z:integer;
begin
for i := 0 to ComponentCount - 1 do
if components[i] is tframe3 then
for z := 0 to (components[i] as tframe3).ComponentCount - 1 do
if (components[i] as tframe3).Components[z] is TEdit then
((components[i] as tframe3).Components[z] as TEdit).Text:='ping';
end;
and
procedure TFrame3.Button3Click(Sender: TObject);
var i:integer;
begin
for i := 0 to parent.ComponentCount-1 do
if parent.components[i] is TEdit then
(parent.Components[i] as TEdit).Text:='pong';
end;
If I have a bunch of editboxes or whatever the hell, I suppose I could use Tag property to identify them, however, this much component counting and passing something AS something doesn't really look right or efficient enough to me.
My questions at the moment are: can it be done in a better way? and can someone provide the reasoning why cant I access "parent-frame" components from a "child-frame" in a dumb way (ie: frame2.button1.enabled:=true from within frame3)?
A couple of points:
Controls/Components are usually set to be owned by the form/frame that controls their lifetime and parented to the control that shows them. So when you create a TEdit to sit on a TPanel on a TForm, you would set TForm as the owner and TPanel as the parent of the TEdit. With frames you do the same: the frame is the owner of all controls on it, controls are parented to whatever container on the frame (can be the frame itself) is holding and and is thus reponsible for showing (painting) it.
When you iterate over the children of a control, iterating over Components uses the Owner relation; iterating over Controls uses the Parent relation. So your code above would already do a lot less if it were to iterate over the Controls.
Of course you can reference other controls in a "dumb" way, but you will have to provide for the access method. If you want others (not just child frames) you will at the very least have to declare a method to retrieve that control. Many ways to do this. One is to "ask" the form when you need it (using a method on the form), or have the form set a property on the frame when it is created...
Avoid accessing form's and frame's published properties. They are mainly there for Delphi's streaming system. When you tie your application together using your approach described above it can very soon become one incredibly tangled mess...
Example coming up (sometime this evening), it will take me a bit of time to also explain and I have work to do...
The following example deals only with the ping-pong game between the frames. Freeing any form or frame from one of its own event handlers is not such a good idea. Use the Release method for that as it prevents the form/frame from processing messages after is was freed. (Plenty of questions about this on SO by the way). Also, when you do release a frame from one of its own buttons, you will need to take care that the frame that created it has a chance to nil the references it may have held to that frame otherwise you are setting yourself up for some interesting to debug mayhem. Look into "Notification" and "NotifyControls" and the automatic notification by forms and frames that is sent to their owner/parent so these can remove the control from their components/controls collection. In your example, if you were to release Frame3 from its own "FreeAndNil" button's OnClick event handler, you would have to make sure that the Frame2 responds to the deletion (I think) notification message and nil's any references it holds to Frame3 (besides the ones that will already be cleared automatically in the components/controls collections).
Now, the ping pong game. There are a couple of ways to go about this.
Method 1
The first way is what you already tried with a loop over the components of the other frame. While it certainly is a way to avoid having to "use" the other frame, it is cumbersome and not very concise. Plus when you get more controls on your forms/frames, you will have to add a check on the name to know that you have the correct TEdit. And then you might just as well use the name directly, especially as one frame already has the other frame in its uses clause because it is creating it.
// PingFrame (your Frame2)
uses
...
Pong_fr;
type
TPingFrame = class(TFrame)
...
procedure CreateChildBtnClick(Sender: TObject);
procedure PingPongBtnClick(Sender: TObject);
private
FPong: TPongFrame; // This is the "extra" reference you need to nil when
// freeing the TPongFrame from one of its own event handlers.
...
end;
procedure TPingFrame.CreateChildBtnClick(Sender: TObject);
begin
CreateChildBtn.Enabled := False;
FPong := TPongFrame.Create(Self);
FPong.Parent := ContainerPnl;
FPong.Align := alClient;
end;
procedure TPingFrame.PingPongBtnClick(Sender: TObject);
begin
if Assigned(FPong) then
FPong.Edit1.Text := 'Ping';
end;
And on the other end:
// PongFrame (your Frame3)
type
TPongFrame = class(TFrame)
...
procedure PingPongBtnClick(Sender: TObject);
end;
implementation
uses
Ping_fr;
procedure TPongFrame.PingPong1BtnClick(Sender: TObject);
begin
(Owner as TPingFrame).Edit1.Text := 'Pong called';
end;
This method seems all nice and dandy, but it has drawbacks:
You need to use the unit that holds the TPingFrame (Ping_fr in this example). Not too bad I guess, but... as Ping_fr already uses Pong_fr (the unit holding TPongFrame), you can't have Ping_fr using Pong_fr in the interface section and have Pong_fr using Ping_fr in the interface section as well. Doing so would have Delphi throwing an error because of a circular references. This can be solved by moving one of the uses to the implementation section (as done in the example for the use of Ping_fr in the Pong_fr unit.
A bigger drawback is that there is now a very tight coupling between the two frames. You cannot change the TEdit in either one to another type of control (unless that happens to have a Text property as well) without having to change the code in the other unit as well. Such tight coupling is a cause of major headaches and it is good practice to try and avoid it.
Method 2
One way to decrease the coupling between the frames and allow each frame to change it controls as it sees fit is not to use the controls of another form/frame directly. To do so you declare a method on each frame that the other can call. Each method updates its own controls. And from the OnClick event handlers you no longer access the other frame's controls directly, but you call that method
type
TPingFrame = class(TFrame)
...
public
procedure Ping;
end;
implementation
procedure TPingFrame.PingPongBtnClick(Sender: TObject);
begin
if Assigned(FPong) then
FPong.Ping;
end;
procedure TPingFrame.Ping;
begin
Edit1.Text := 'Someone pinged me';
end;
And on the other end:
type
TPongFrame = class(TFrame)
...
public
procedure Ping;
end;
implementation
procedure TPongFrame.PingPongBtnClick(Sender: TObject);
begin
(Owner as TPingFrame).Ping;
end;
procedure TPongFrame.Ping;
begin
Edit1.Text := 'Someone pinged me';
end;
This is better than Method 1 as it allows both frames to change their controls without having to worryabout "outsiders" referencing them, but still has the drawback of the circular reference that can only be "solved" by moving one "use" to the implementation section.
It is a good practice to try and avoid circular references altogether instead of patching them by moving units to the implementation section's uses clause. A rule of thumb I use is:
Any form/frame may know and use the public interface of the forms/frames it instantiates (though it should avoid the controls in the "default visibility" part of that interface), but no form/frame should not have any knowledge of the specific form/frame that created it.
Method 3
One way of achieving this (there are many) it to use events just like the TButton's OnClick event.
On the TPongFrame side of things you can remove the use of Ping_fr. You no longer need it.
Then you need to declare a property and the field it references and declare a method to fire the event.
type
TPongFrame = class(TFrame)
private
FOnPingPongClicked: TNotifyEvent;
protected
procedure DoPingPongClicked;
public
property OnPingPongClicked: TNotifyEvent
read FOnPingPongClicked write FOnPingPongClicked;
end;
The Ping method stays and its implementation is unchanged. The PingPongBtnClick event handler also stays, but its implementation now becomes:
procedure TPongFrame.PingPongBtnClick(Sender: TObject);
begin
DoPingPongClicked;
end;
procedure TPongFrame.DoPingPongClicked;
begin
if Assigned(FOnPingPongClicked) then
FOnPingPongClicked(Self);
end;
You could put the code from DoPingPongClicked straight in here, but it is a good practice to fire an event in a separate method. It avoids duplicating the exact same code if you would have an event that can be fired from multiple parts of your code. And it also allows descendants (when you get those) to override the "firing" method (you'll have to mark it virtual in the ancestor) and do something specific all the times the event is fired.
On the TPingFrame side of things you need to code a handler for the new event:
type
TPingFrame = class(TFrame)
...
protected
procedure HandleOnPingPongClicked(Sender: TObject);
Note that the signature of this handler needs to be what the TNotifyEvent specifies. And of course you need to ensure that this event handler gets called when the event fires in TPongFrame:
procedure TPingFrame.CreateChildBtnClick(Sender: TObject);
begin
CreateChildBtn.Enabled := False;
FPong := TPongFrame.Create(Self);
FPong.Parent := ContainerPnl;
FPong.Align := alClient;
// Listen to event fired by PongFrame whenever it's PingPingBtn is clicked
FPong.OnPingPongClicked := HandleOnPingPongClicked;
end;
In the handler code you can do what you need to do. It can be general:
procedure TPingFrame.HandleOnPingPongClicked(Sender: TObject);
begin
Edit1.Text := 'OnPingPongClicked event was fired';
end;
And of course you can also use the fact that the event passes a reference to the instance firing the event as well:
procedure TPingFrame.HandleOnPingPongClicked(Sender: TObject);
var
Pong: TPongFrame;
begin
// This checks that Sender actually is a TPongFrame and throws an exception if not
Pong := Sender as TPongFrame;
// Use properties from the Pong instance that was passed in
Edit1.Text := 'OnPingPongClicked event was fired by ' + Pong.Name;
end;
Enjoy!

Delphi KBMMemtable filter speed problem

Hi I am using KBmmemtable in a small project and come across a small speed issue i cannot seem to fix.
Basically I have a field in the table which has a boolean value, the table has about 100 records in it. If I itterate though the records in the table setting the value of the field to true it does it very quickly, however if I set a filter on the table and then itterate through the filtered records it takes about 10 times longer even though there could only be 10 records to iteerate through.
Anyone got any ideas
The code I am using is
DM1.DS1.Enabled := False;
with DM1.DS1.DataSet do begin
First;
while not Eof do begin
edit;
Fields[18].AsBoolean := TickState;
// FieldByName('Selected').AsBoolean := TickState;
post;
next;
end;
end;
DM1.DS1.Enabled := true;
I do have an index on the field, I have also tried it without an index
thanks
colin
There is a way to use a filter on a kbmMemTable and make it work really fast...
Set kbmmem.Filtered:=true;
and dont use the Filter property, instead use the OnFilter Event...
procedure Tform1.kbmmemFilterRecord(DataSet: TDataSet;
var Accept: Boolean);
begin
Accept:=Fields[18].AsBoolean;
// when you iter your table you would see only thouse rows having "true"
// on the field "Selected"
end;
and yes dont forget to DisableControls before the while...
with kbmMem do
try
DisableControls;
Filtered:=true;
First;
while not eof do
begin
// do your stuff here
Next;
end;
finally
EnableControls;
end;
This is a suggestion for the loop, it should not take any longer than with an unfiltered kbmMemTable:
with kbmMemTable do
begin
First;
while not EOF do
begin
//do something, but don't change the position of the record-pointer!
//if you do some writing to the record, be sure to
// enable "AutoReposition" in your kbmMemTable
Next;
end;
end;
Disabling the DataSource is not such a good option. Every Component attached to the DataSource is then "empty" and must be refreshed. You get a lot of problems if you use recursion or more than one "disabling" of a DataSource. Same, when you enable the DataSet. With DisableControls you signal all the attached components, that they must not update data. With EnableControls, this condition is ended and the controls are refreshed. Another advantage is, that there is a counter incremented with every DisableControls and decremented with every EnableControls. So you can call this multiple times (for example in an recursion) and only the last call of EnableControls finally enables the controls.

Maintain UpDown-Associate connection while recreating the associate

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.

Resources