5
votes


I want to let the user to create multiple instances of the same form (let's call it Form1 which is a MDI child form). So I have two procedures like this where I create the forms.

procedure MyProcedure1;           // procedure 2 is similar. it also has a var called MyFrm
var MyFrm: TFrm1;
begin
  ... 
  MyFrm:= TFrm1.create(MainForm);
  MyFrm.BringToFront;
  MyFrm.LoadFromFile(someFile);
end;

As you can see MyFrm is local var. This is ok for me as I don't need to programatically access the form after I create it. There is no other global variable named Frm1. In the OnClose event of MyFrm I have Action:= caFree;

What could cause the error above? A user sent that error. It happened only once and I cannot reproduce it.


Edit:

  1. The error appears in the "MyFrm:= TFrm1.create" line.

  2. Some people suggested that I need to programatically give unique names to my dynamically created forms. I also wondered myself what name a form takes when it is created so I stepped into the code while calling the MyProcedure1 procedure.
    Delphi automatically gives unique names like
    MyFrm.name= MyFrm, then
    MyFrm.name= MyFrm_1,
    MyFrm.name= MyFrm_2,
    MyFrm.name= MyFrm_3, and so on.

  3. The MyFrm.Name is not altered in LoadFromFile. I have checked (breakpoint) the value of 'MyFrm.Name' at the end of procedure MyProcedure1; after LoadFromFile. The name is unique.

  4. As some people suggested, I override the SetName procedure and checked the name of TMyFrm. Indeed each form gets a unique name.

    procedure TMyFrm.SetName(const Value: TComponentName);
    begin
    ShowMessage(Value);
    inherited;
    end;

  5. I have many forms in this app but only the MainForm is auto-created.

  6. I don't use threads. Anyway this will not be relevant since the forms are created by user (so multi-threading is irrelevant unless the user can create 2 forms at the same time).

4
Er, what's this LoadFromFile method? It must be one of yours, unless it's something added in XE.David Heffernan
@David, I've mentioned the LoadFromFile as a cause in comments to two answers. As OP has not mentioned it, I assume it's not the LoadFromFile.Lieven Keersmaekers
@Altar, are you by any chance using multiple threads?Lieven Keersmaekers
@Altar: OK. Then we can't say that the user are lying either :-P The strange thing, then, is that the error refers to 'TFrm1' as the component name. How can that be, when the class is named 'TFrm1'? Is the code snippet exactly the same as your production code? I.e. the function is a function and not a method? Are the names the same? Is any TFrm1-form autocreated in the .dpr? What is the 'Name'-property in TFrm1's object inspector?Jørn E. Angeltveit
@Altar: Check this link gnostice.com/… and find if there is a mistake like that in your codeBharat

4 Answers

8
votes

Giving MainForm as the Owner in TFrm1.Create will include the newly created form in the components list of MainForm. A component ensures that this list doesn't contain any two components with the same non-empty name (otherwise FindComponent won't work). This mechanism also works when a component changes its name.

As long as you don't specify the name in TFrm1.Create it is most likely that it is set by the LoadFromFile method, which means that you don't have much influence on the name unless you change the file's content.

A valid workaround is to create the form with nil as Owner, load the form from the file, change the name to a unique value or to an empty string and finally call MainForm.InsertComponent.

procedure MyProcedure1;           
var MyFrm: TFrm1;
begin
  ... 
  MyFrm:= TFrm1.create(nil);
  MyFrm.BringToFront;
  MyFrm.LoadFromFile(someFile);
  MyFrm.Name := ''; // or some unique name
  MainForm.InsertComponent(MyFrm);
end;
7
votes

The message is caused because each form must be uniquely named.

When you create a form twice, you need to ensure each instance has a unique name, or set the Name to an empty string. The latter also is the trick when using multiple instances of a data module, so that the automatic linking of data-aware controls does not end up always using the first instance.

Add

MyFrm.Name := MyFrm.Name + <something unique>;

MyFrm.Name := '';

after the Create call and you should be fine

3
votes

MyFrm.Name is the same for both instances...

Make sure than MyFrm.Name is unique...

0
votes

As far as my exploration along this line, yes the problem of "already exists" stems from having intances of the editor with the same value for the Name property. As another work around do not visually create the editor(s). Create a new component based on TForm/TFrame/TPanel for the editor(s) you want the user to be able to create multiple instances of. You will have to hand code the creation & deletion of any sub-controls, Setting their properties within your code and assigning values - anything from V_Btn = new TBitBtn(this), V_Btn->Color = clTeal, to V_Btn->OnClick = Close_The_Window. BUT NEVER assign a value to the Name property of any component in the new class and do not set the Name property of the editor once you have created an instance of the editor. Treat the Name property for editor as if it did not exist. After you have created the class and added it to your project the following is valid :

TMyeditor* Editor_01 = new TMyeditor(Main_Form);
TMyeditor* Editor_02 = new TMyeditor(Main_Form);
  Editor_01->Parent = Tab_Sheet_Addresses;
  Editor_02->Parent = Tab_Sheet_Billing;

The more complex the design concept for your editor the more effort you will undergoe to code the class. However this approach will resolve the "already exists" error.

            End of answer.

The following is tangental to the original question as it is an extension of what you may want to further do with your code & I write it to help you along should it be the case. The following allows you to efficiently store/retrieve the editor(s) and its published properties such as position on the user's screen, themes, etc. If you've gone the above route, add the following :

void RegisterClassesWithStreamingSystem(void)
{   
  // Make sure that as part of the startup   
  // code TMyEditor is registered   
  // with the streaming system.   
  #pragma startup RegisterClassesWithStreamingSystem  
  Classes::RegisterClass(__classid(TMyEditor)); 
}

You can now ComponentToString <---> StringToComponent[*1] the editor(s). You can now create a simple database of each editor saving it [*2] and re-creating the editor(s) at runtime. Saving & Recreating is almost entirely done by the TReader/TWriter objects. {It is worth while to read about TReader/TWriter which is included in the Delphi help file}

[ Presupposing you have an instances of TMyEditor you want to save called Editor_01 & Editor_02 and you've created the dataset and assigned it to a TClientDataSet named "CDS" ]

//How to write the Editors
String_Version_Of_Editor = ComponentToString(Editor_01);
CDS->Insert();
CDS->FieldByName("Data")->AsString = String_Version_Of_Editor;
CDS->Post();
String_Version_Of_Editor = ComponentToString(Editor_02);
CDS->Insert();
CDS->FieldByName("Data")->AsString = String_Version_Of_Editor;
CDS->Post();

//How to read, create an instance of, set the Owner of 
//(allowing for automatic destruction/deletion
// if desired, Vis-à-vis Let the compiler/runtime package handle that), 
//& setting the form's Parent
AnsiString   String_Version_Of_Editor; 
TWinControl* New_Editor;
String_Version_Of_Editor = CDS->FieldByName("Data")->AsString;
//The next line creates/constructs the new editor
New_Editor = StringToComponent(String_Version_Of_Editor);
//The next line sets the new editor's Owner to Main_Form 
//It also assigns Main_Form the responsibility of object cleanup
Main_Form->Insert(New_Editor);
//The next line sets the Editor's Parent causing it to be part of the 
//displayed user interface (it has been invisble since creation)
New_Editor->Parent = Tab_Sheet_Addresses;
//Move on to the next editor;
CDS->Next();
String_Version_Of_Editor = CDS->FieldByName("Data")->AsString;
New_Editor = StringToComponent(String_Version_Of_Editor);
Main_Form->Insert(New_Editor);
New_Editor->Parent = Tab_Sheet_Billing;

People who read the above who are astute will have noted that in the above code the New_Editor is of type TWincontrol not TMyEditor - though it likely should have been. However I did this to draw attention to the fact that problematically the TReader object in Delphi which is really doing the work of converting a string to a component object instance creates/constructs any object which has been registered with the streaming class via RegisterClass. In this manner explicit creation of the editor via explicitedly naming it's type is avoided. If thought is given to the design of TMyEditor and its descendents the only change required to the code is to change TWinControl* to TMyEditor* - even that is not required if published properties beyond TWinControl* are not accessed outside the scope of TMyEditor - Example TMyEditor has access to the variables whose values it is editing and does not require this information to be supplied to the editor.(If working from a DataModule, #include the datamodule's header into TMyEditor).

Side Note: You may have a utility to know what class was read from the database so that you may place the instance where it belongs. To do this #include <typeinfo> into your code. Example : If you have instances of TMyEditor, TMyEditor_Generation_01, TMyEditor_Generation_02, etc writen to the database the following will allow you to examine the instances read at runtime for placement into the user interface :

 if (typeid(New).name() == "TMyEditor *")
   New_Editor->Parent = Tab_Sheet_Addresses;
  else
   if (typeid(New).name() == "TMyEditor_Generation_01 *")
     New_Editor->Parent = Tab_Sheet_Billing;
   else
    if (typeid(New).name() == "TMyEditor_Generation_02 *")
      New_Editor->Parent = Tab_Sheet_Other_Editor;

typeid(__).name() will return a string which is the name of the class, in this case will also inculde " *".

The above allows ANY object(s) to be stored in the database and recreated. The entries within the database are not required to be related. The TReader object buried in Delphi's code will decide at runtime what they are and use the correct constructor.

[*1] Note : The ComponentToString and StringToComponent are examples in the delpi/c++ help file.

[*2] Note : What is being saved are the published properties, therefore in your editor class any values you want stored and retrieved which are not already inherited and published should be declared in the __published section of your new class. Those items may also be custom objects, for those you will likely code custom specific methods/functions for the read/write access specifiers in defining the _property. I would suggest translating any complex object into a string value for ease of examining your code while under development.