Introduction
When I started with my first real application in IntraWeb, I wanted to make a
consistent layout through out my application. Since with IntraWeb you can make a web
application just like a normal Delphi application: my first thought was:
(visual) form inheritance (VFI). So I started happily programming making a baseform as a
parent for all my other forms, putting on it a title bar, and started inheriting
from it. And it worked: I had the title bar on all my forms!
But after I started programming really functional forms, I soon also wanted
to change something about my baseform, make the layout a bit nicer and better
fitting with the forms I was making. And then it happened: I had added something
to my baseform, compiled the application and it just gave an exception: "Control
'IWButton1' has no parent window". I hadn't changed anything really, except
enhancing my baseform: and there wasn't even a IWButton1 on my baseform, it was
on my mainform! Well after a little debugging, I looked at the IntraWeb
newsgroups and it's website and soon found the answer: IntraWeb doesn't support visual
form inheritance. You can use inherited code, but you can't inherit the visual
layout...
Finding the problem...
Since I am a stubborn person (I wanted to use visual form inheritance!), and
in my opinion problems are there to be solved, I started digging deeper in the
problem. With the help of Delphi's VCL code (one of the things I love
most about Delphi: you can always look at the source!) I soon found out in which
part I had to search for the problem: the SetZOrderPosition method of TControl.
When you add controls to a form you have inherited from (the baseform) the
ordering of the controls on your descendant forms (in my case the Mainform)
changes. You can see this ordering when you look at the dfm file of a descendant
form: you see numbers in brackets [] after the controls. These numbers tell Delphi in which
order the controls appear. In this SetZOrderPosition method there is a call to
ValidParentForm (found in Forms.pas) that returns the parent form of the current
control if that parent forms is a descendant of TCustomForm. And that was the
problem: a TIWAppForm is not a descendant of TCustomForm!
...and trying to fix it.
Well only finding the problem doesn't solve it, I needed to find a way to fix
it. My first thought was: I wanted that Borland had made ValidParentForm a
virtual protected method of TControl, so anyone could override it, and the guys
from IntraWeb could have made a special version that worked with IntraWeb forms.
But since that wasn't the case, I had to go one level up to the
SetZOrderPosition method. And found out that that one is private to TControl, so no
chance of
overriding that. (B.T.W. If that one could have be overridden it would have also meant that
it had to be done in TIWBaseControl: and I would not be able to do that.)
So I needed to go up one level: the caller of SetZOrderPosition which was in this case the
SetChildOrder method of TWinControl, where this TWinControl was actually my Mainform. And
the nice news was that SetChildOrder of TWinControl could be overridden! However if I did
that there could be other problems because SetZOrderPosition was also called by
other methods in TControl and TWinControl. But if
that happened I would find out sooner or later.
Seeing that I needed to override SetChildOrder of my Mainform, and
since that one inherited from my baseform: I put my implementation of
SetChildOrder in my baseform. My implementation looked as follows:
procedure TIWBaseVFIForm.SetChildOrder(Child: TComponent; Order: Integer);
begin
if Child is TIWBaseControl then
TIWBaseControlHelper(Child).SetZOrderPosition(Order)
else
inherited;
end;
So if the Child is a TIWBaseControl I call a special implementation of
SetZOrderPosition else I just call the inherited version. That special
implementation of
SetZOrderPosition I put in a class called TIWBaseControlHelper:
type
TIWBaseControlHelper = class(TIWBaseControl)
public
procedure SetZOrderPosition(Position: Integer);
end;
To hack or not to hack?
Until so far it was simple. But now the real challenges appeared: I had to
copy the implementation of SetZOrderPosition, but that implementation used a
private field called FControls (see the VCL implementation of TControl.SetZOrderPosition
in the Controls.pas unit). So I looked at what does the SetZOrderPosition has to
do: it has to put itself at a certain position in it's parent list of controls.
How can it do that, well look at the code I produced:
procedure TIWBaseControlHelper.SetZOrderPosition(Position: Integer);
var
I, J, Count: Integer;
ParentForm: TIWBaseForm;
TmpParent: TWinControl;
TmpControls: TList;
begin
if Parent = nil then
Exit;
// First find our current position (in I)
Count := Parent.ControlCount;
I := 0;
while I < Count do
begin
if Parent.Controls[I] = Self then
Break;
Inc(I);
end;
if I = Count then
Exit; // strange we are not in our parent list?
if Position < 0 then Position := 0;
if Position >= Count then Position := Count - 1;
if Position <> I then
begin
// re-position, but since we can't directly manipulate Parent.Controls,
// we need to hack and make a copy in TmpControls
TmpControls := TList.Create;
try
for J := 0 to Count-1 do
TmpControls.Add(Parent.Controls[J]);
// delete ourselves from the list:
TmpControls.Delete(I);
// and insert at new position
TmpControls.Insert(Position, Self);
// save Parent (we'll be setting it to nil in the next lines)
TmpParent := Parent;
// remove all controls from the Parent's Controls list
// we do that by setting the Parent property of the controls to nil
while TmpParent.ControlCount > 0 do
TmpParent.Controls[0].Parent := nil;
// And put them back in in our TmpControls order
for J := 0 to Count-1 do
TControl(TmpControls[J]).Parent := TmpParent;
finally
TmpControls.Free;
end;
// Invalidate; Not necessary in IntraWeb.
ParentForm := ValidParentIWForm(Self);
if csPalette in ParentForm.ControlState then
THackIWBaseForm(ParentForm).PaletteChanged(True);
end;
end;
This code was of course inspired by the original TControl implementation: but
the important part I had to rewrite. What it does is it first checks if it has a
parent, then it searches for it's current position in the parent's control list
and sees if it's new position is not the same. When it finds out it has to
reposition, the hacking begins. We can't manipulate the parent's control list
directly, but we can indirectly through the parent property of all the controls
in the control list. So what I do is make a copy of the current control list and
reposition our self in there. Then I set the parent of all the controls
currently in the list to nil, so they are removed from the list and after that I
add them again (in the correct order) to the list by setting their parent
property back. So this results in a correctly ordered list.
But this is not all that has to be done in this method! If it was we wouldn't
have had any visual form inheritance problems, because this part already worked in the
TControl implementation. The problem lied in the call to ValidParentForm in that
implementation. First I thought I could just remove that call from my
implementation, but I wasn't really sure and since the solution was not so hard,
I made a call to a new function ValidParentIWForm. This function was simply
based on the equivalent functions in Forms, I copied them and replaced
references to TCustomForm to TIWBaseForm:
// next funcs base on Forms.GetParentForm and Forms.ValidParentForm
function GetParentIWForm(Control: TControl): TIWBaseForm;
begin
while Control.Parent <> nil do Control := Control.Parent;
if Control is TIWBaseForm then
Result := TIWBaseForm(Control) else
Result := nil;
end;
function ValidParentIWForm(Control: TControl): TIWBaseForm;
begin
Result := GetParentIWForm(Control);
if Result = nil then
raise EInvalidOperation.CreateFmt(SParentRequired, [Control.Name]);
end;
(I might be violating a little copyright of Borland here, but since this is
actually the most obvious implementation and you can't use it if you don't have
Delphi: I hope Borland allows me to publish it.)
Now only one thing in my SetZOrderPosition code might need some explanation.
I needed to call the PalletteChanged method of the ParentForm, but that is a
protected method of TControl, so I used a standard hack to call a protected
method: define a descendant class, and call the method by type casting to that
descendant class:
type
THackIWBaseForm = class(TIWBaseForm);
Trying it out
And now the excitement begins: does it all work? So I started running my web
application: and it gave no error! To really test it I made a lot of changes to
my baseform, I made changes to my mainform and other forms: and it kept on
working. I also changed the order of the controls on my baseform and mainform by
selecting the control and right-click, choose Control -> sent to back and
bring to front, since that actually manipulates that Z-Ordering, but it kept
working.
Conclusion
So this finishes my solution for using visual form inheritance (VFI) with
IntraWeb 5.1(.28) in Delphi 7 in application mode. We are using it for quite
some time now, and haven't run into any bugs. You can get the source of this
solution, including a small test project at codecentral.
Included in that source is a TIWBaseVFIForm (in IWBaseVFIForm.pas) that you
can use to inherited from. Of course you can also just copy the code from that
form to your own base form.
Remember one thing when using this
solution: the developer of IntraWeb doesn't support VFI, so I have no idea if it will
work for other versions of IntraWeb nor if it will work with pagemode! I also haven't tested this code with
IntraWeb in Delphi 5 or 6, but a quick scan of the relevant source code of those
implementations of TControl doesn't show any possible problems: so just try it
out.
About the Author:
Tjipke A. van der Plaats works as a software engineer for Agrovision
B.V. where he works with IntraWeb. He also has it's own company Tiriss,
for which he creates software on request or just because he needs the software himself.
Copyright ) MMIII by Tjipke A. van der Plaats