Workaround for anchors being broken when recreating a window? - delphi

This happens in all Delphi up to XE3:
Create a form and put a panel on it. Anchor the panel to [akLeft, akTop, akRight, akBottom], but leave space between it and the borders.
Add a button which calls RecreateWnd()
Run the app. Resize the form so that the panel is hidden because it's less than 0 pixels in size due to anchoring. Press the RecreateWnd button.
Resize the form back and note that the panel's anchoring is broken.
As long as I can remember myself using Delphi, anchors were always impossible to use because of this. Resize the form, then dock it: the window is recreated, your layout is broken.
I wonder if there's some sort of workaround?
Update
Two workarounds are available in the comments, one proven and stable but with form blinking, another one experimental but potentially more thorough and clean.
I'm not going to vote for either one for a while, as one of those is mine and I'm not even sure it is stable. Instead, I'll wait for some public input.

The two options I have used of which neither is really ideal for problems with the bottom and right anchors are:
Make the window big again before calling or causing to be called RecreateWnd();, then make it small again. Has to be visible before you make it small again however.
Set the Form's constraints so it can't be re-sized so small that stuff ends up hidden.
An example that flashes the larger form, use height and width large values enough so the panel is not hidden:
procedure TForm1.Button1Click(Sender: TObject);
Var
OldWidth, OldHeight : integer;
begin
OldWidth := Form1.Width;
OldHeight := Form1.Height;
Form1.Visible := false;
Form1.Width := 1000;
Form1.Height := 800;
RecreateWnd();
Form1.Visible := true;
Form1.Width := OldWidth;
Form1.Height := OldHeight;
end;

Turns out that the function which breaks everything is UpdateAnchorRules. TControl stores FOriginalParentSize and it's own original size in FAnchorRules, and uses that to auto-resize as parent resizes. UpdateAnchorRules() takes current parent size and current control Width and Height and saves those into FOriginalParentSize and FAnchorRules.
If everything worked properly that would have no effect during normal resizes, as the control and it's parent change size in accord.
But when the control Width is less than zero due to anchoring, Windows, and consequently Delphi still considers it 0. If UpdateAnchorRules is called at that point, it saves wrong, out-of-accord 0 value for original width. After this the layout is beyond repair.
(If it's not called, Width continues to be updated in proper relation to parent Width due to original sizes preserved)
Turns out anything which involves creating a window handle calls UpdateAnchorRules twice: first in WinAPI CreateWindow as it dispatches WM_SIZE before returning (and WM_SIZE handler calls UpdateAnchorRules), and second, explicitly in CreateHandle after handle creation.
It would seem that as long as we can disable UpdateAnchorRules for the duration of CreateHandle, we will succeed. But there are explicit calls to UpdateAnchorRules in CreateHandle, which means someone thought there needs to be an adjustment of Anchor rules after handle creation.
So perhaps I'm missing something, and by disabling it something will break?
Anyway, there are two ready ways to disable UpdateAnchorRules: to set FAnchorMove or to set csLoading. First one is no good as there's code which clears it midway through RecreateWnd and then calls UpdateAnchorRules again.
Second one works and here's a solution:
type
TComponentHack = class helper for TComponent
public
procedure SetCsLoading(Value: boolean);
end;
procedure TComponentHack.SetCsLoading(Value: boolean);
var i: integer;
begin
if Value then
Self.FComponentState := Self.FComponentState + [csLoading]
else
Self.FComponentState := Self.FComponentState - [csLoading];
for i := 0 to Self.ComponentCount-1 do
if Self.Components[i] is TControl then
TControl(Self.Components[i]).SetCsLoading(Value);
end;
procedure SafeRecreateWnd();
begin
MyControl.SetCsLoading(true);
try
MyControl.RecreateWnd(); //or any operation which triggers it -- such as docking or making the window visible first time after RecreateWnd()
finally
MyControl.SetCsLoading(false);
end;
end;
Disclaimer:
I have no idea what else will be broken by running TControl operations with csLoading set.
Better alternative would be to hook UpdateAnchorRules procedure and add another flag check specifically for this purpose, but that'd require either reimplementing UpdateAnchorRules completely (prone to breaking on different versions of Delphi with different original UpdateAnchorRules) or inventing some way to call original UpdateAnchorRules which is usually destroyed by rewriting it with a hook.

Related

How to work around AutoSize's being off while invisible?

Delphi controls have AutoSize property which is exposed e.g. in TPanel. It adjusts the width/height of the panel depending on the content.
Apparently it does nothing when the panel is invisible, and does no realigning later when it's set to visible. So if I put some controls into it and then make it visible, the size is not adjusted.
I can trigger adjusting size by setting height to any value in FormShow:
procedure TForm1.FormShow(Sender: TObject);
begin
Panel1.Height := Panel1.Height + 1; //triggers auto-resize
end;
But I have to do this manually for every control which has AutoSize on. I'm bound to forget something.
Are there better ways to fix this, preferably once and for all?
I don't think there's much that you can do. A better way to for the re-sizing is to add a call to the Realign method of the panel immediately after you make it visible.
You could hook into the CM_VISIBLECHANGED message and force the matter there, for auto-sized controls. For instance, using an interceptor:
type
TPanel = class(Vcl.ExtCtrls.TPanel)
protected
procedure CMVisibleChanged(var Message: TMessage); message CM_VISIBLECHANGED;
end;
procedure TPanel.CMVisibleChanged(var Message: TMessage);
begin
inherited;
if Visible and AutoSize then
Realign;
end;
Its been a while since i used delphi, but one thing i remember is that the controls did play a bit of mind games on me, most of the time it was because the rendering engine did not refresh the form and the controls.
If you have the control set to auto-resize i would suggest checking if form1.refresh or panel1.refresh since its been a few years since i played with it ( delphi 7 ) i might confuse refresh with repaint. which some controls had, which initiated the size calculation before repainting the control. since delphi controls are open source you can go in to the apropriate pas file to find the control and look at refresh/repaint to see if you can persist the auto-resizing.
hope that helped.

How do you make a TFrame's OnResize event occur when it first appears?

I have a frame that is being placed on a form. I expect to be placing a number of instances of this frame on the form.
It has a drawgrid with 2 columns and in the OnResize event I make the 2nd column extend to the end of the available space. That works when the form is manually resized with the frame Align set to alTop. But when the form first appears even though FrameResize gets called it has no effect. (Though it did have the desired effect when I put a break point on it).
So, what I am doing now is calling the FrameResize from the forms OnShow handler, but that is ugly. The frame should be able to appear correctly formed without help from the Form.
Any ideas? I did try overriding SetParent, but that didn't work. Using Xe2.
TIA
Mark
I have solved it with advice from Peter Below, Delphi Team B Delphi member.
I overrode the frame's set bounds. It was getting called even before the component variables were set, so it looks like this
procedure TfaDupDisplay.SetBounds(ALeft, ATop, AWidth, AHeight: Integer); // Had to use SetBounds because OnRezise was not working
var grid: TDrawGrid;
begin
inherited;
if pnlWebData = nil then
exit;
pnlWebData.Width := Width div 2;
for grid in TArray<TDrawGrid>.Create(grdData, grdDup) do
grid.ColWidths[1] := grid.Width - grdData.ColWidths[0];
end{ SetBounds};

The anchor property of components when the application is maximized in Delphi

I have three buttons placed at the right on my form. The buttons' anchor property parameters akTop, akRight are set to true, the other ones left to false so that the buttons always remain at the right side near the boarder when the form is resized. Then I set the form's WindowState property to wsMaximized so that it covers the whole screen when run at start-up. But when I start the application the buttons are closer to the middle rather than on the right. But when I resize the form at design-time everything seems to work just fine.
Here are some snapshots to show you exactly what I mean:
At design-time:
At run-time:
Please, explain what I'm doing wrong and how to fix this so that it works as intended.
This looks like the buttons are being created with their designed positions, the form is then set to Maximized, then the anchor properties are set or put in place.
In design time the anchors are already set and so that’s why you see them move as you want. To prove my theory on this make the form much smaller, run the application and notice that the items are in their smaller design time locations.
An easy fix to get what you want. Keep the Window state at wsNormal and on FormShow (which occurs after Create) do this:
procedure TForm1.FormShow(Sender: TObject);
begin
self.WindowState := wsMaximized;
end;
You will see the results you want.
I have seen your answer in one of those tutorials. But realy dont remember was which one. You can watch all videos, even you will learn more things. it will not time waste all the way.
Link: Learn Delphi TV
Also you can try something like below if you are lazy enough to watch videos. Put this code in form resize:
buttoncreate.left := panel.width - (buttoncreate.width + buttonedit.width + buttondelete.width);
buttonedit.left := panel.width - (buttonedit.width + buttondelete.width);
buttondelete.left := panel.width - buttondelete.width;

How to make a Drawn Line on a Form Invisible in Delphi 7

I am trying to make an analog clock, wherein I would like to make my 'seconds' Line Invisible when the Seconds Changes. I have tried to set the Pen mode to pmNotCopy but it only gives Inverse of Pen Color. What Property must be set in this Form1.Canvas.Pen.Mode:=<Blank> so that My Line Disappears?
Any other Ideas are also appreciated.
Thanks
Modern computers are very fast. If I were you, I'd definitely draw the entire clock from scratch every second. Problem solved. In addition, if you need anti-aliasing, then a simple pen mode trick will never work.
(If you are not using a remote desktop, you might want to employ double-buffering.)
I don't know anything about delphi but just some out if the box thinking:
couldn't you change the color of your line to the background color, making it 'invisible'
You were close. You need to use pmXOR.
Try this:
Create a new Delphi VCL Forms application. Drop a TButton on the bottom of the form (Button1).
Add the code below to the Button1Click event.
Run the application. Click the button, and three parallel lines will be drawn across the top. Click the button again, and the three lines will disappear.
procedure TForm1.Button1Click(Sender: TObject);
var
i: Integer;
begin
Canvas.Pen.Mode := pmXor;
Canvas.Pen.Color := clGreen;
for i := 1 to 3 do
begin
Canvas.MoveTo(50, i * 20);
Canvas.LineTo(Width - 50, i * 20);
end;
end;
All drawing should normally be done in the OnPaint event; I'm intentionally drawing in the Button1Click event for demonstration reasons only.
Just as a note: You should never use Form1 inside an event handler for that form. Referencing the Form1 variable prevents you from creating more than one instance of the form. Use Self instead, and that will automatically refer to the instance the code is running in.

How do you handle multiple TPanel's at design-time?

I have several TPanel's on my main form that I show/hide depending on options a user selects. The problem is that at design time I have to constantly move them around to edit them. Is there an easier/better way that others handle this situation?
If only one panel is visible at a time, then you might want to use a TPageControl to organize things. You can click the tabs to select which one to work on at design time, and then hide the tabs at run time, or hide the tabs all the time and select pages by setting the ActivePage property.
Use the Structure Pane to find the one you want, and then bring it to the front.
(source: embarcadero.com)
(But don't follow Embarcadero's example above; give your controls meaningful names so your controls are easier to tell apart.)
If the panels are stacked, you can reorder them by right-clicking on one and choosing Control->Bring to Front or Control->Send to Back from the context menu.
A caveat I've found when doing Panel1.Visible:= false in runtime is that it messes up with layout.
The solution I've found works is to do:
//Design time
//-----------
Panel1.BevelOuter:= bvNone; //Make the panel look flat.
//Run time when hiding the panel
//------------------------------
procedure HidePanel(APanel: TPanel);
var
H,W: integer;
begin
case APanel.Align of
alTop, alBottom: begin
APanel.Tag:= Max(APanel.Tag, APanel.Height);
APanel.Height:= 1;
end; {alTop, alBottom:}
alLeft, alRight: begin
APanel.Tag:= Max(APanel.Tag, APanel.Width);
Panel1.Width:= 1;
end; {alLeft, alRight:}
alNone: begin
H:= Max(APanel.Tag and $FFFF, APanel.Height);
W:= Max((APanel.Tag shl 16) and $FFFF0000, APanel.Width shl 16);
APanel.Tag:= (H or W);
APanel.Height:= 1;
APanel.Width:= 1;
end; {alNone}
//alClient: do nothing
end;
end;
//Run time when restoring the panel
//---------------------------------
procedure UnhidePanel(APanel: TPanel);
var
H,W: integer;
begin
case APanel.Align of
alTop, alBottom: begin
APanel.Height:= APanel.Tag;
APanel.Tag:= 0;
end; {alTop, alBottom:}
alLeft, alRight: begin
APanel.Width:= APanel.Tag;
APanel.Tag:= 0;
end; {alLeft, alRight:}
alNone: begin
H:= APanel.Tag and $FFFF;
W:= APanel.Tag shr 16;
APanel.Height:= H;
APanel.Width:= W;
APanel.Tag:= 0;
end; {alNone}
//alClient: do nothing
end; {case}
end;
Simply hiding the panels can mess up the careful alignment you've constructed in Designtime
(esp. when using splitters).
This code prevents that from happening.
It really only works visually when the panel has no Bevels set and the panelcolor equals the color of the control it's on top of.
I select the Frame or Panel using the Object inspector Combo then on the main menu click Edit--> bring to front
(which is similar to opening the structure view)
I also have used a TPageControl, and I kept the tabs visible at designtime. This gave me a designtime usability (via clicking on the tabs I want). Then at runtime, I hide the page tabs, and switch active pages on the page control using code, as a way of switching which pane is visible. However, this lead to some horrifically huge and complicated forms, which was in turn, the cause of many problems.
For your case, I would suggest that you consider refactoring each pane into its own Form or Frame. My preference would be to use Forms, not frames, and the reasons for this are well known and well documented in the Delphi world.
In my most well-structured applications, each "pane" (implementing using TForm, though, not TFrame) is separated into different units, and this solves both your design-time control problems, and also results in a more well structured overall solution.
While I think that the Structure Pane (someone else pointed out) is a great help to you, when you want to work with forms that are so complex that the regular designer visual tools start getting harder to use, it is also a good idea to consider breaking your form up, when you reach this point of "diminishing returns" of using the form Designer, on what is becoming one super-super complicated form.
It is much easier to use frames for this purpose. I usually create them at runtime and add them to the form as needed. It also tends to make the code much more readable because you can use the same component names (e.g. ed_Name or l_Welcome) on different frames without having name clashes rather than having ed_NameForPanel1, ed_NameForPanel3 etc.

Resources