views:

166

answers:

4

I've got a custom frame I need to inherit from in multiple projects. This frame includes some code and some components, and it resides somewhere on disk, in it's own project directory. I don't want to COPY it to the Object Repository folder, that doesn't seem right to me: I'd end up having two copies of the form, one in my Mercurial-backed repository, one in Delphi's Object Repository. Absolutely not a good idea.

What I want is to have my frame in a Package and have the Package do all that's required to make the frame known to the IDE and allow the IDE to create new siblings of the given frame, without actually adding the frame to every single project.

What I've done so far, problems I've encountered, solutions I tried:

  1. I added my frame to a package, registered my frame using both RegisterClass and RegisterNoIcon. Problem: When I go into some other project and try to open a derived frame for editing it says it can't find my original frame.
  2. To fix problem "1" I figured I'd have to register my frame as an Custom Module. So I called RegisterCustomModule(TMyFrameName, TCustomModule). Problem: From the 'other' project I open a derived frame, the IDE doesn't create the components on my original frame and the IDE is complaining about one of the "inherited" components missing.
  3. To fix "2" I thought I'd give the IDE a helping hand by calling InitInheritedComponent(Self, TFrame). This helped, when I tried opening the frame in the 'other' project everything got re-created and I was able to see the frame as I expected. Problem: when I save the frame it forgets all about the inherited components, treats every single component as a new component added to this particular frame. If I look into the saved DFM everything starts with "object", nothing starts with "inherited" as I'd expect.

Unfortunately I got stuck on problem "3". I tried digging into Classes.pas, ToolsAPI, DesignIntf and DesignEditors but didn't find anything helpful. Apparently the "inherited" attribute I was hoping to see in the DFM is generated by TWriter when it's "TWriter.Ancestor" property is assigned before streaming a TComponent, but there's no way for me to set it up, the IDE needs to set it up. And I can't convince the IDE to do it for me.

Here are the cumulated, relevant parts of code:

TTestFrame = class(TFrame)
public
  constructor Create(Owner:TComponent);override;
end;

constructor TTestFrame.Create(Owner: TComponent);
begin
  inherited;
  if csDesignInstance in ComponentState then InitInheritedComponent(Self, TFrame);
end;

procedure Register;
begin
  RegisterClass(TTestFrame);
  RegisterNoIcon([TTestFrame]);
  RegisterCustomModule(TTestFrame, TCustomModule);
end;

Any ideas, besideds "give up and put your stuff into Object Repository"? Thanks!


Edit

Why I need to do this and why solutions that depend on actual path names getting written into my project's files don't work: I want to support Branching: when one branches it's reasonable to expect multiple versions of the same project being "alive" in different directories on the same machine. The corollary, I can't have multiple versions of the same project in the same place at the same time.

To make sure this works I decided to make my projects not depend on where the live, and to enforce this I have everyone on our team Clone (Mercurial terminology) or Check Out (SVN terminology) our projects in different directories. A hard-coded path on my system will not be good on my colleague's system: if any of us makes the mistake of hard-coding any sort of path into the application, it will not be long before it brakes on one of us, so the error gets fixed.

This is of course a problem with forms and frames that are part of some library (so they're not in our project's directory) that we need to inherit from! In order to get IDE support when working with those files we need to temporarily add them to the project and we need not forget removing them after we're done. If we forget and push/check in changes, the changes would brake the build for our colleagues (because they have the libraries checked out at different locations).

In order to solve this I attempted adding those frames and forms to a design time package (packages are loaded using full-path into the IDE, but the path is not part of the project's files so it's OK). Unfortunately this failed, and that's wyhy I posted this question.

A: 

Uh, what's wrong with simply adding the frame you want to reuse to the project in which you want to reuse it?

e.g.

  • Project1.dpr uses Form1 and Frame1, you want to reuse Frame1.
  • Start new VCL forms project in a different folder.
  • When you click on Frames on the "Standard" page of the Tool Palette is says there are none.
  • Add Frame1 unit to this project via the project manager (so that it is added to the uses list in the dpr).
  • Now clicking on Frames on the "Standard" page of the Tool Palette shows Frame1 available for selection.
  • And right clicking on the project in the project manager, and then selecting add new | other and going to "inheritable items", also shows Frame1 as an inheritable item.

Edit

If you do not want to include the hardcoded path to the frame in the dpr, there is always the ploy of using the IDE's environment variables.

  • Go to Tools | Options | Environment variables.
  • Add a var MYLIB and point it to the folder that is applicable for the branch you are currently in.
  • Add a file containing the folder path to your sources and add it to source control. This could be an export of the registry's key now containing the value of MYLIB.
  • Add $(MYLIB) to your project's library path.
  • Add the frames to your project. They should now be included in the dpr without the path (because they can be found on the library path).
  • When integrating branches: ensure that the source file with the value for MYLIB is changed appropriately.
  • When switching branches: activate the proper value for MYLIB. If you added a .reg file to source control: simply double click it to change the MYLIB registry's key value.
Marjan Venema
Long story, but it's wrong (for me). I'm using Mercurial for version control and I want to be able to "Branch". To do that my projects need not be dependent on the actual physical path where one installs them (I obviously can't have two versions of the same project in the same place at the same time). In order to ensure my projects really don't depend on the physical path I've got everyone on our team Cloning the projects we're working on in different folders: The frame I'm talking about is not located in the same directory for everyone, so I can't add it to the projects!
Cosmin Prund
So, add it to the the dpr without a path by ensuring that the source is available from the project's library path and use an environment variable in the IDE similar to the $(BDS) symbol to point to the specific path needed for the specific branch. Put the value for that environment var in a file under source control and "activate" it when changing branches (either using a .reg file or changing the Tools | Options | Environment variables by hand. All my own folder paths in all my projects are like $(MVC)\<library>\<folder>. Changing the value of MVC changes the sources used...
Marjan Venema
Who gave this an -1? Would you mind sharing why? This is a solution that probably works, unfortunately I don't have the time to test it for the moment. If this works it gets an +1 from me and there's good chance I'll also accept the answer.
Cosmin Prund
Hmmm Nice trick, Marjan... I'll try this for some pathing problems I have !!
Fabricio Araujo
@Marjan, this is unfortunately not working for me. I've got my "MYLIB" environment variable set up, and I can see it in Tools|Options|Environment|Environment Variables. I added $(MYLIB) to both the library path and the search path, made sure the same path is not already in there using it's actual path. When I add my file to the project it gets the full path name. Next I tried manually modifying both DPR and DPROJ files to use the $(MYLIB) syntax. It works fine until Delphi decides it's time to rewrite both files: Delphi expands my $(MYLIB) sintax into the full path.
Cosmin Prund
@Marjan, next I tried manually modifying the DPR and remove $(MYLIB) from the file, living nothing but the file name. It works for Build, but when I try to actually open the file Delphi can't find the file: It tries opening from my project's folder, doesn't use the Search path or Library path. Am I doing something wrong here?
Cosmin Prund
@Cosmin: hmm, normally you wouldn't add library files to the dpr of course, unless the dpr code itself needs it. So maybe both practices bite each other somewhat. I'll take a further look and let you know.
Marjan Venema
@Marjan, those frames need to be added to the DPR so the IDE allows one to do "New | Inherited". If the files were actually part of the given project I'd have no problem adding them to the DPR. Anyway, I don't see that $(MYLIB) sticking no matter where the file is: When the IDE needs to rewrite the DPR, it simply rewrites every single path in there.
Cosmin Prund
@Cosmin: yep, that is unfortunately exactly what it does. See my second answer for a more detailed "post mortem."
Marjan Venema
A: 

You could create a package and inherit the frame from there.

You'll have to link it as a runtime package (Project/Options.../Packages, check Build with runtime packages and place your package in the list there).

I don't know if Delphi IDE works well with that, tho. Maybe it doesn't show in the list when trying to create a new inherited frame. I've never have done that directly.

But if you create a new frame and change

TFrame2 = class(TFrame)

to

TFrame2 = class(TMyBaseFrame)

You should also add the unit name where your frame is defined to the uses clause. This I've done using forms defined in packages and it should work without the need for dcu, pas or even dfm files. (dfm files are needed if the package is not linked as a runtime package)

-

God bless you!

Trinidad
+1  A: 

There are a couple of aspects to this problem:

  • Using frames from a package at design time to include them on forms.
  • Creating a new frame in a project that inherits from a frame in a package.
  • Enabling different project branches to use different versions of the package and thus the frames, without being faced with hardcoded paths in either the dpr or the dproj.

Enabling different project branches to use different versions of your own packages

  • Create an Environment variable in the Delphi IDE using Tools | Options | Environment variables and point it to the folder where the frames for your current branch reside. For this discussion we'll call this environment variable "MyLib" and point it to a "D:\Whatever\Version1" folder.
  • Go into the registry editor and export this entry to a file "MyLibEnvironmentVariable.reg". Put this file under source control and edit it to remove any other environment variables that were also present under the same registry key.
  • Export the contents of the Known Packages key of your Delphi installation.
  • Edit the exported file -- Remove all values that are not your own packages. -- Change the values of your own packages from for example D:\\Whatever\\Version1\xxx.bpl to $(MyLib)\\xxx.bpl.
  • Remove all keys pointing to your own packages from the Known Packages key and import the file you just edited. This effectively changes your package registrations to use the MyLib var and will ensure that the IDE will now also use the MyLib var to find your packages.

Using frames from a package at design time

  • Create a package "LibraryPackage", save it in the D:\Whatever\Version1 folder.
  • Add a frame "LibraryFrame", save it in the D:\Whatever\Version1 folder.
  • Put some controls on the frame or give it an ugly color so you can visually recognize it.
  • Add a Register; procedure to the LibraryFrame unit, and put RegisterComponents('MyFrames', [TLibraryFrame]); in its implementation.
  • Build the package and install it in the IDE.
  • Remove the harcoded path to this package from the Environment Option's Library path and/or change it to use the $(MyLib) environment variable.
  • Change the Registry entry for this package to use the MyLib environment variable.
  • The frame will still not be available under Tool Palette | Frames, but it will be available from its own MyFrames Tool Palette page and you can include it on any form you want. The IDE will add the frame's unit name to the forms uses clause. It shouldn't add anything to the dpr, but it may still add a fully hardcoded path to your dproj. To avoid that you will have to do the export / change the bpl's path to use $(MyLib) / double-click trick for this package as well.

Inheriting from a frame in a package

Now that all of the above is set up, the Frame from the package is still not available to be inherited from in any other project than the library in which it was "created". However, it is now fairly simple to make it available for inheritance. Simply add LibraryFrame_fr in 'LibraryFrame_fr.pas' {LibraryFrame}, to your project's dpr and the frame will now show up in the "inheritable items" list when you use "Add New | Other" to add something to your project.

Unfortunately however, when you select it, the IDE will complain: Cannot open file "D:\Whatever\SecondFormReusingFrame\LibraryFrame_fr.pas". The system cannot find the file specified.

It obviously now expects the LibraryFrame in the project's folder instead of trying to find it in the MyLib folder. The IDE also redlines the TLibraryFrame type in the form although ctrl-clicking it does bring up the correct file...

Changing LibraryFrame_fr in 'LibraryFrame_fr.pas' {LibraryFrame}, to $(MyLib)\LibraryFrame_fr in 'LibraryFrame_fr.pas' {LibraryFrame}, helps in as much as the IDE now no longer complains about not finding the LibraryFrame_fr.pas. But it does have the effect that when you save everything, the IDE "kindly" changes it to a relative path. In my case ..\FrameToBeReusedSecond\LibraryFrame_fr in 'LibraryFrame_fr.pas' {LibraryFrame}, which defeats the entire object of the exercise as it reintroduces dependency on path names albeit partly.

However, leaving LibraryFrame_fr in 'LibraryFrame_fr.pas' {LibraryFrame}, in the dpr is not really an option either. When you reload the project the IDE complains about not finding the frame even though it can be found on the search path.

Also leaving it like that and ignoring the IDE complaint when deriving from this frame, is not really an option. The IDE does not add a corresponding dfm... And while you could edit the uses in the dpr from MyOwnFrame_fr in 'MyOwnFrame_fr.pas' to MyOwnFrame_fr in 'MyOwnFrame_fr.pas' {LibraryFrame1} and add a dfm stubb manually, the IDE still gets confused with not being able to find LibraryFrame_fr.pas ...

All in all Cosmin, it seems the IDE is just a bit too set in it what it wants and expects. I think what you want is achievable but only if you are willing and able to have your library folders always in the same relative location to your projects.

Marjan Venema
Wow, this should be a sticky. <g> > *"..library folders always in the same relative location.."* - Dunno, maybe one might map library folders to a drive letter (subst) to instead have them always in the same absolute location...
Sertac Akyuz
@Sertac: <g> thanks. "Mapping to a drive letter" Yes, that could work. Point all your packages and dpr references to L:\Lib and change the mapping of L: depending on the branch you work in. In essence you would be using the drive mapping as a substitute for my $(myLib) environment variable. Not sure how the IDE will react to this though, coz its been known be finicky with paths not on the same drive as the project, especially network drives. But I'd say it is certainly worth investigating for Cosmin.
Marjan Venema
`Inheriting from a frame in a package` is pretty much what I'm interested in, but all you're saying is, it doesn't actually work. `Using frames from a package at design time` if all I needed was "use the frame" I'd be done already, but I don't care about that. `Enabling different project branches to use different versions of your own packages`: You're over-engineering your answer! I'll figure out how to use different versions of the same package once I can get 1 (one) package working. But so far my question is still unanswered. Thanks for the helping hand those.
Cosmin Prund
@Cosmin: Being able to using the frame at design time is part of being able to inherit from it. If the IDE cannot find it correctly, you won't be able to visually design your descending frames. I am sorry that my efforts did not result in something that'll work straight of, but that is the way it is. I think that the information in my answer on inheriting from a frame in a package combined with Sertac's suggestion in the comments, could be the solution you are looking for, but you'll have to investigate that yourself. I am out of time on this one.
Marjan Venema
A: 

When we created the build process out the last company I worked for, we wanted to do exactly the same thing. We use subversion and had a project for our shared components which contained a (Finalbuilder) project to build all our shared packages.

I was then going to use a similar technique to Marjan's (MyLib) variable. And launch Delphi from a batch file with the latest set of components.

In the end we found it wasn't necessary. The published properties of our main frames changed so infrequently that we just used one installed set of packages say in

c:\BDS\Components\D12\Bpl (This could be different from developer to developer)

But the code that was sitting in

c:\BDS\MyProject\Shared was always linked in when compiling because that was on the relative search path for when building the project

c:\BDS\MyProjectExperimental would use the correctly branched code when building as well.

We actually got around the repository problem by using CodeSmith (a code generator) to generate the frames, with the added bonus that they would be put in the right folder with correct naming conventions (and any branch specific changes as well). We did this as a time saver for us but we (by chance) avoided this problem all together.

The CodeSmith templates took a little bit of time to set up for the first frame, but then we easily adapted it to create other subclasses without too many brain cycles (for things such as dataModules).

cfEditFrame.dfm.cst

  
<%@ CodeTemplate Language="C#" TargetLanguage="Delphi" Src="" Inherits="" Debug="False" Description="cfEditFrames.dfm Template" ResponseEncoding="ASCII" %>  
<%@ Property Name="TypeName" Type="System.String" Default="TypeName" Optional="False" Category="Strings" Description="Name of Type. eg Account; AgeMonths" %>  
inherited cf<%=TypeName%>EditFrame: Tcf<%=TypeName%>EditFrame  
  Width = 425  
  Height = 63  
end  

cfEditFrame.cst - The one script we had to call to generate a new subclassed frame

  
<%@ CodeTemplate Language="C#" TargetLanguage="Delphi" Src="" Inherits="" Debug="False" Description="cfSDMEditComp Template." ResponseEncoding="ASCII" %>  
<%@ Property Name="OutputFolder" Type="System.String" Default="..\\SharedNonInstalled" Optional="False" Category="Strings" Description="" %>  
<%@ Property Name="TypeName" Type="System.String" Default="TypeName" Optional="False" Category="Strings" Description="Name of Type. eg Account; AgeMonths." %>    
<%@ Register Name="EditFramesPasTemplate" Template="cfEditFrames.pas.cst" %>  
<%@ Register Name="EditFramesDfmTemplate" Template="cfEditFrames.dfm.cst" %>  
<%@ Import NameSpace="System.IO" %>  
<script runat="template">  

public override void Render(TextWriter writer)  
{  
  EditFramesPasTemplate cfEditFramesPasTemplate = new EditFramesPasTemplate();  
  this.CopyPropertiesTo(cfEditFramesPasTemplate);  
  cfEditFramesPasTemplate.RenderToFile(String.Format("{0}\\Edit\\cf{1}EditFrames.pas", OutputFolder, TypeName), true);  

  EditFramesDfmTemplate cfEditFramesDfmTemplate = new EditFramesDfmTemplate();  
  this.CopyPropertiesTo(cfEditFramesDfmTemplate);  
  cfEditFramesDfmTemplate.RenderToFile(String.Format("{0}\\Edit\\cf{1}EditFrames.dfm", OutputFolder, TypeName), true);  
}

</script>

There may be other source code generators out there that do this just as well, if not better. We had CodeSmith and therefore used it. Hopefully the above files are formatted OK. I'm a SO newbie and the hopefully rendering of html like code is correct.

As far as having both properties and components on these frames you want to inherit from you need to do one strange thing. You need to do it in two steps. First you need to add the frame properties in the first class and THEN add the components in a subclass of that.

For example we add the custom properties in cfBaseEditFrames.TcfBaseEditFrame.

We then subclass that in cfEditFrames.TcfEditFrame = class(TcfBaseEditFrame). This is where we add our components (in our example a TActionList and TImageList)

When registering these in a package we added

RegisterCustomModule(TcfBaseEditFrame, TWinControlCustomModule);

We then ensure this package is in our project group and there is no problem opening up newly subclassed frames.

One last note, from memory it was important that the descendant frame (TcfEditFrame) was the one that added components. You couldn't add components in TcfBaseEditFrame and properties in TcfEditFrame.

BaseEditFrames.pas

unit BaseEditFrames;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms, 
  Dialogs;

type
  TBaseEditFrame = class(TFrame)
  private
    { Private declarations }
    FNewFormProperty: string;
  published
    { Published declarations }
    property NewFormProperty: string read FNewFormProperty write FNewFormProperty;
  end;

implementation

{$R *.dfm}

end.

BaseEditFrames.dfm

object BaseEditFrame: TBaseEditFrame
  Left = 0
  Top = 0
  Width = 320
  Height = 240
  TabOrder = 0
end

EditFrames.pas

unit EditFrames;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, ActnList, BaseEditFrames;

type
  TEditFrame = class(TBaseEditFrame)
    ActionList: TActionList;
  private
    { Private declarations }
  public
    { Public declarations }
  end;

implementation

{$R *.dfm}

end.

EditFrames.dfm

object EditFrame: TEditFrame
  Left = 0
  Top = 0
  Width = 320
  Height = 240
  TabOrder = 0
  object ActionList: TActionList
    Left = 72
    Top = 16
  end
end

FramePackage.dpk

package FramePackage;

{$R *.res}
{$ALIGN 8}
{$ASSERTIONS ON}
{$BOOLEVAL OFF}
{$DEBUGINFO ON}
{$EXTENDEDSYNTAX ON}
{$IMPORTEDDATA ON}
{$IOCHECKS ON}
{$LOCALSYMBOLS ON}
{$LONGSTRINGS ON}
{$OPENSTRINGS ON}
{$OPTIMIZATION ON}
{$OVERFLOWCHECKS OFF}
{$RANGECHECKS OFF}
{$REFERENCEINFO OFF}
{$SAFEDIVIDE OFF}
{$STACKFRAMES OFF}
{$TYPEDADDRESS OFF}
{$VARSTRINGCHECKS ON}
{$WRITEABLECONST OFF}
{$MINENUMSIZE 1}
{$IMAGEBASE $400000}
{$DESCRIPTION 'Inheritable Frames'}
{$IMPLICITBUILD ON}

requires
  rtl,
  vcl,
  designide;

contains
  RegisterFramePackage in 'RegisterFramePackage.pas',
  BaseEditFrames in 'BaseEditFrames.pas' {BaseEditFrame: TFrame},
  EditFrames in 'EditFrames.pas' {EditFrame: TFrame};

end.
unit RegisterFramePackage;

interface

procedure Register;

implementation

uses Classes, DesignIntf, WCtlForm, BaseEditFrames;

procedure Register;
begin
  RegisterCustomModule(TBaseEditFrame, TWinControlCustomModule);
end;

end.

You need to install this design time package. You could then have a frame in another project like the following

EditFrameDescendants.pas

unit EditFrameDescendants;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms, 
  Dialogs, EditFrames, ActnList;

type
  TEditFrameDescendant = class(TEditFrame)
    Action1: TAction;
  private
    { Private declarations }
  public
    { Public declarations }
  end;

implementation

{$R *.dfm}

end.

EditFrameDescendants.dfm

    
inherited EditFrameDescendant: TEditFrameDescendant
  ParentFont = False
  inherited ActionList: TActionList
    object Action1: TAction
      Caption = 'Action1'
    end
  end
end

You should be able to open up EditFrameDescendant, edit its amazing "NewFormProperty" and add actions to to its action list. Works for me....Good luck

Clint Good
I don't understand the role CodeSimth plays in your solution. Would you mind explaining that a bit? I also don't understand how this solution helps with inheritance: I absolutely need to inherit from the frames, not just use them. Thank you.
Cosmin Prund
Clint Good
Just to make it a bit more explicit for any others reading this, we never go to File|New to create a subclass, we use a CodeSmith template to generate the files and then add then to the appropriate package/project.
Clint Good
In this example CodeSimth only takes care of replacing the "New - Inherited" functionality, but the big problem still remains: The IDE needs the DFM for the base frame available in order to allow editing of the inherited frame, and having it on the library path is not enough: It needs to be opened or added to the current project. Delphi' IDE doesn't seem to like opening DFM's from Packages so having the Frame in a package doesn't help. How did you solve this? How did you get the IDE to load the proper DFM? Did you add the parent DataMdoule class to your project?
Cosmin Prund
Disregard the last comment. To add properties to the Frame AND components you need to do it in two stages. For example we use RegisterCustomModule(TcfBaseEditFrame, TWinControlCustomModule) for the properties of our edit frame and RegisterCustomModule(TcfEditFrame, TWinControlCustomModule); to add the components. We then add this package to our project group and have no problems opening up newly created frames. From memory it was even important that the descendant frame (TcfEditFrame) was the one that added components (You couldn't do it the other way around).
Clint Good
Aha. So you were essentially using "runtime packages", the same solution as Trinidad's.
Cosmin Prund
We certainly use packages the same as Cosmin, but for design-time only. We compile a monolithic executable (We do not build with packages). The design time packages are only there to support the IDE. That's why it doesn't matter that we might be using a slightly old (or even from a different branch) build of the packages for design time.
Clint Good
@Clint, I think I'm missing something from your solution, because it doesn't work for me. My comment on CodeSmith only replacing the "New - Inherited" functionality is my attempt to peal away irrelevant layers in order to get to the real solution. In all my tests, Delphi never allowed me to create a frame when the DFM was in an package. The only case where I know it works, is if the package is linked to the project as an runtime package - but that's something I don't want to do for countless reasons. In fact I don't even test with "New-Inherited", I simply open an existing frame for editing.
Cosmin Prund
Clint Good
Did you have problems with this ?
Clint Good