Delphi MDI application with components shared using pointers

我的梦境 提交于 2019-12-12 05:49:59

问题


I am developing an MDI application (Delphi 7) that will load .bpl packages in the form of plugins as the MDI children. Only one instance of a plugin can be opened, but obviously multiple plugins can be opened simultaneously.

I have a class which is a common class that is used to 'share' certain components available on the MDI parent. I achieve this by having the common class store a pointer to each relevant component when it is constructed.

For example:

...
TCommonClass = class(TObject)
  public
    MainMenu:   ^TMainMenu;
    MyClass:    ^TMyClass;
...
constructor TCommonClass.Create;
var
  CtrlItm: array[0..999] of TComponent;
...
  for i := 0 to (Application.MainForm.ComponentCount - 1) do
  begin
    CtrlItm[i] := Application.MainForm.Components[i];
    if CtrlItm[i].ClassName = ‘TMainMenu’ then MainMenu := @CtrlItm[i];
    if CtrlItm[i].ClassName = ‘TMyClass’  then MyClass  := @CtrlItm[i];
  end;

Whenever I refer to an object I simply do as follows:

  ...
  var
    tmp: String;
  begin
    MainMenu^.items[0].Caption := 'Something'; //just to demonstrate
    MyClass.DoSomething;
  end;

Every plugin will have its own instance of this common class with the idea that any update to one of its components will truly update the component on the MDI parent. This approach has been working well for me until the last plugin I wrote (which is rather large and contains many TMS Components) has started giving me errors which I cannot seem to trace.

What I would like to know is if this approach is sound in terms of memory (pointers) usage? By loading and unloading packages is there a possibility that changes in the memory mapping is corrupting the pointers? Should I be doing this differently?


回答1:


You don't need the extra level of pointer indirection that you are currently using. You can slim that down, eg:

TCommonClass = class(TObject)
public
  MainMenu:   TMainMenu;
  MyClass:    TMyClass;
  ...
end;

constructor TCommonClass.Create;
var
  Ctrl: TComponent;
  ...
begin
  ...
  for i := 0 to (Application.MainForm.ComponentCount - 1) do
  begin
    CtrlItm := Application.MainForm.Components[i];
    if CtrlItm.ClassName = 'TMainMenu' then MainMenu := TMainMenu(CtrlItm);
    else if CtrlItm.ClassName = 'TMyClass' then MyClass := TMyClass(CtrlItm);
    ...
  end;
  ...
end;

.

var
  tmp: String;
begin
  MainMenu.Items[0].Caption := 'Something'; //just to demonstrate
  MyClass.DoSomething;
end;

Now, with that said, I would suggest an alternative approach. Have the MainForm itself provide the pointers to each plugin, rather than having the plugins poll the MainForm for them. The MainForm knows all of the pointers, so have it collect the pointers in its own local record and pass a pointer to that record to every plugin that it loads. If any of the pointers change, all active plugins will automatically have the up-to-date pointer without having to do anything extra for that. Inside each plugin, simply check each pointer for nil before accessing it. For example:

MainForm:

type
  PSharedPointers = ^TSharedPointers;
  TSharedPointers = record
    MainMenu: TMainMenu;
    MyClass:  TMyClass;
    ...
  end;

var
  SharedPointers: TSharedPointers;

procedure TMainForm.FormCreate(Sender: TObject);
begin
  SharedPointers.MainMenu := MainMenu1;
  ...
end;

procedure TMainForm.LoadAPlugin;
type
  InitProc = procedure(Pointers: PSharedPointers);
var
  PluginInst: HInstance;
  Init: InitProc;
begin
  PluginInst := LoadPackage('plugin.bpl');
  @Init := GetProcAddress(PluginInst, 'InitPlugin');
  Init(@SharedPointers);
end;

Plugin:

type
  PSharedPointers = ^TSharedPointers;
  TSharedPointers = record
    MainMenu: TMainMenu;
    MyClass:  TMyClass;
    ...
  end;

var
  SharedPointers: PSharedPointers = nil;

procedure InitPlugin(Pointers: PSharedPointers);
begin
  SharedPointers := Pointers;
end;

...

var
  tmp: String;
begin
  if SharedPointers.MainMenu <> nil then
    SharedPointers.MainMenu.Items[0].Caption := 'Something'; //just to demonstrate

  if SharedPointers.MyClass <> nil then
    SharedPointers.MyClass.DoSomething;
end;

exports
  InitPlugin;



回答2:


The appropriate response to any question involving the gratuitous use of explicit pointer syntax in delphi is to shudder from head to foot, and then wait a minute while the feeling of nausea passes.

Any object inheriting from TObject is already by-reference and the pointer logic you're considering is (a) unnecessary second level of pointers and (b) likely to cause errors.

Look at this code:

var
  a : TMyObject;
  b : TMyObject;
begin
   a := TMyObject.Create;
   a.Name := 'Test';
   b := a;
end;

If that code compiled, what would the value of b.Name be after we assign b := a? It would be 'Test', because a and b are just variable REFERENCES to the same object. So for TMyClass, you can just assign values of one to the other, and you will not be copying and creating TWO objects, you'll have two variables each of which reference the SAME object.

What is a reference? A reference is a pointer with simpler and safer semantics. You can't dereference it (it's done automatically) and you can't forget to do it (it's always done for you).

In short, feel free to treat references to CLASSES as pointers.

However, in the case of TMainMenu, you don't really need to share a single TMainMenu instance. In fact, if you try, I think you'll find you have problems, perhaps crashes, or visual painting problems. What did you plan to do with the "shared" TMainMenu? You haven't made any explanation of what you thought you might do with it, and I suspect if you articulate that a bit better you'll find that you're barking up the wrong tree with sharing a TMainMenu reference.

You see, TMainMenu knows about it's parent object, and you can't have the SAME object parented into two different forms without causing problems. As you're using it in the context of MDI client forms, you should find another solution... You could implement a plugin system using Actionmanager for example, or TActionList, or simply by creating your own IPluginCommand interface, which the main form enumerates and creates menu items from abstracting your plugins from knowing about whether they're being shown as menu items, or something else. If all you wanted to do was have the main Menu visible to your plugin so your plugin could add more items at runtime, you could do that (although I think that's gross and a violation of OOP principles), and you COULD just pass a reference to a TMainMenu into your plugin however without the ^ pointer notation.

What you might want to do instead is use the ActionManager component or ActionList component, and have two forms both of which have actions that come from a shared ActionList or ActionManager. Put the actionlist or manager onto a data module called SharedActions, and add the SharedActionsDataMod unit in both form's uses clauses, and you can see the actions at runtime, which then can be used to make menus that share the actions (which are like menu items stored outside a menu) as much as you like.

Update Since you asked about menus but didn't really care about menus, you got information that didn't apply to you. Please don't do that. If you just want a general plugin system, consider using Interfaces and making a stable binary interface (known as an ABI) as that's a requirement to make a really stable plugin system, especially if you wish to be able to load plugins dynamically from a DLL or BPL. Also, you have to TURN ON the use of Runtime Packages (BPLs) in your linker settings, so that you can share references to classes like TForm and other core VCL classes, between different binary modules. If you don't use runtime packages, you'll be statically linking a different TForm and TButton and everything else, into each .DLL and .EXE you build.



来源:https://stackoverflow.com/questions/15148136/delphi-mdi-application-with-components-shared-using-pointers

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!