Register custom form so I can inherit from it from

2019-01-22 22:39发布

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.

3条回答
太酷不给撩
2楼-- · 2019-01-22 23:25

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.

查看更多
祖国的老花朵
3楼-- · 2019-01-22 23:33

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.
查看更多
闹够了就滚
4楼-- · 2019-01-22 23:38

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

查看更多
登录 后发表回答