Delphi 在载入窗体时的所触发的事件的顺序是怎么样的?(200分)

  • 主题发起人 主题发起人 rope
  • 开始时间 开始时间
R

rope

Unregistered / Unconfirmed
GUEST, unregistred user!
我把代码贴出来吧。


unit Unit1;

interface

uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
Menus,
dbctrls, comctrls, FRDBGrid, db, dbtables;

type
TFRPopupMenu = class(TPopupMenu)

private
FPMnuDBEditUndoAll: TMenuItem;

protected

public
property PMnuDBEditUndoAll: TMenuItem
Read FPMnuDBEditUndoAll Write FPMnuDBEditUndoAll;

constructor Create(AOwner: TComponent); override;

// Create Default MenuItem
Procedure FRCreatePopupMenuItem;
// Set PopupMenuItem

end;


procedure Register;

implementation

procedure Register;
begin
RegisterComponents('Test', [TFRPopupMenu]);
end;

constructor TFRPopupMenu.Create(AOwner: TComponent);
begin
inherited Create(AOwner);

// 问题就在这里
FRCreatePopupMenuItem;
End;

Procedure TFRPopupMenu.FRCreatePopupMenuItem;
Var
I: Integer;
SrcComponet:TComponent;
Begin

PMnuDBEditUndoAll:= TMenuItem.Create(Owner);
PMnuDBEditUndoAll.Caption := '取消';
PMnuDBEditUndoAll.Name:='PMnuDBEditUndoAll';
Items.Add(PMnuDBEditUndoAll);
UpdateItems;
End;


end.


这个问题实际上是:由TFRPopupMenu在设计期生成的MenuItem(子控件)如何能够正确的
被储存(File|Save),或者是被正确的储存后在Create过程中如何被FindComponent
找到(假设已经被正确存储了),在集成环境中TFRPopupMenu内部的事件发生顺序如下:
(1)窗体没有该TFRPopupMenu时,向窗体添加:
TFRPopupMenu.Create
(2)添加后 File|Save
(3)关闭窗体,
(4)重新打开
(5)已经存在TFRPopupMenu的窗体在重新打开时:
TFRPopupMenu.Create;
TFRPopupMenu.Loaded;

现在,为了合乎编程习惯,必须在第一步结束后,就允许程序员可以修改TFRPopupMenu
生成的MenuItem,也就是,必须在控件的Create中加入生成MenuItem的代码,同时,
生成前必须检测:是否上一次已经Create过MenuItem了。
对此,我专门在TFRPopupMenu中加入调试代码,跟踪后发现在窗体载入时(此时
MenuItem已在窗体上了)的事件顺序大概是下面这种情况:
A)
窗体.Create
窗体内的控件.Create
变化了的属性.Loaded
在这种情况下,
1)如果TFRPopupMenu自己创建的MenuItem被Delphi认为是一个属性,一个
具有控件特点的属性,则在 "窗体内的控件.Create" 过程中永远也检测不到,
2)如果TFRPopupMenu自己创建的MenuItem被Delphi认为是一个控件,一个
成为了TFRPopupMenu的属性的控件,这种情况稍微好一点,但是存在另一个问题:
如果MenuItem的Create过程在最后(对于MenuItem来说,这也合乎逻辑),则该
MenuItem还是不能被TFRPopupMenu检测到。

如果事情真是这样(我倒是很希望我搞错了),应该怎么办?
为了应付差事,我可以将MenuItem作为TFRPopupMenu的属性Publish出来,实际上
我现在就是这么做的,可是,我希望事情能够解决的完美一些。

大虾们,救我!

俗话说的好:不管是龙虾还是对虾,能解决问题就是大虾!
 
try this:

Procedure TFRPopupMenu.FRCreatePopupMenuItem;
Var
I: Integer;
SrcComponet:TComponent;
Begin

//****** add these lines ******//

if not (csReading in ComponentState) then
begin
//************//

//****** your old codes here ********//
PMnuDBEditUndoAll:= TMenuItem.Create(Owner);
PMnuDBEditUndoAll.Caption := '取消';
PMnuDBEditUndoAll.Name:='PMnuDBEditUndoAll';
Items.Add(PMnuDBEditUndoAll);
UpdateItems;

end;
End;;
 
首先,多谢 Another_eYes。

大虾的方法我试过了,可是好象只能在 Create 事件中检测到 csDesign,这可该如何是好?
 
首先,要想使自定义的组件拥有保存和载入功能,必须继承自TPersistent类,
其次,缺省情况下只有published的才能自动保存,
这样你该知道了吧,编写几个items类,聚合到你定制的类中,
并且申明为属性
private
FItems:T....;
published
property Items: T.... read FItems;
 
To:netyjj
  首先,我的 FRPopupMenu 肯定是从 TPersistent 继承过来的。
>> TFRPopupMenu = class(TPopupMenu <<

  其次,通过 Publish 来解决问题,我也试过了。
>> 为了应付差事,我可以将MenuItem作为TFRPopupMenu的属性Publish出来,实际上 <<
>> 我现在就是这么做的,可是,我希望事情能够解决的完美一些。 <<

  最后,不论 netyjj 大虾是否继续继续关注这个问题,我都表示非常的感谢。
 
我的经验是如果某个控件正从dfm中, 或者资源中加载时, 就能检测到csReading标记
被置位.
我原来在Create中就这么做过, 并且跟踪过, 的确检测到csReading并跳过了我希望只
在完全是新建控件时(而不是load时)才执行的代码).
这么做的目的是为了防止多Create出一个clone子控件.
一般如果某个属性是个控件并且它不是private的(protected, public, published),
如果它的owner为form, 在加载时form会自动create一个实例并将设计时的内容填入.
如果你记录这个属性的变量又由你的程序建立了一个实例的话就会造成一个clone.
运行时肯定会出现问题, 因为它们的owner都是这个form, 你读取或者写入的就可能
并不是 你所希望的哪个控件.
 
多谢 Another_eYes 大虾指点,俺再去试一下。
 
你的程序太长了,以下摘抄一段:

Form生成时的事件次序
如果是一般的 SDI Form, 各事件的发生次序如下:OnCreate/OnShow
在萤幕上看到这个视窗OnActivate/OnPaint
如果是 MDI 视窗, 而 MdiChild 的第一个子视窗是在程式启动时, 就出现在MdiForm中的话,
那麽,各事件的次序是
主视窗的 OnCreate子视窗的 OnCreate子视窗的 OnShow子视窗的 OnActivate主视窗的 OnShow
在萤幕上看到主视窗及第一个子视窗
主视窗的 OnPaint
ok, 您发现了吗? OnShow 是在视窗被看到之前的事, 而 OnActivate 并没有发生,因此,您的问题中所采用的 '我试过的 OnActivate ,OnShow ....',
当然不能在视窗於'被看到後'才出现设定资料的对话盒
因此, 以您的状况, 检查与设定的程式可以写在主视窗的 OnPaint 中, 不过,OnPain是一个常发生的事件,所以应该要在主视窗的类别定义中额外加上一个 private 的逻辑成员资料变数,以控制不要每次视窗重画时都检查设定.
另外, 有一个方法您可以参考看看:
1. 利 Options | Project , 将子视窗从 Auto-Create forms 移到 Available forms,不要让 Delphi在程式启动时就产生 MdiChild 子视窗
2. 此时, 虽然是 MDI 架构, 但是 MDI 主视窗产生时的各事件次序与一般视窗相同,您的检查与呼叫设定视窗的程式可以写在 OnActivate 中
3. 各子视窗的产生, 以Application.CreateForm 方法,写在您的应用程式主选单的File | New Click 事件中,以手动的方式让子视窗出现在主视窗中
最後, 视窗内部的资料起始与设定, 我一般是写在 Form 的OnCreate 事件中.
Form有AutoCreate与Available两种设定
Delphi 一个Form 中有一 Edit and Button 按此 Button 会 show 出新 From 而此新form 中的 Edit 内容要与原Form 相同, 我试不出来说两个 Form 都 uses 对方
UNIT1
uses unit2;
procedure button1_click()
begin
form2.show;
end;
UNIT2
uses unit1;
procedure form2_onCreate()
begin
edit1.text:=Form1.edit1.text;
end;
执行结果没效 ??
Form 产生後、使用者使用前先设定 form上元件的值是写在 Form 的 OnCreate Event吧?
又如果原 Form是一个 MDIChild 的话, 是否会有差别?两个 unit 均在 interface 互相 uses 对方是不可以的,一定要互相参考的话, 您可以在unitA 的 interfaceuses unitB, 然後在 unitB 的 implementation 中 uses unitA
你写的程式还是曾经执行过, 只是早在程式启动的时候, Delphi就已经自动为您Create 这个Form 了,关於这点你可以点一下主选单 View | Project Source, 看一下project1.dpr 的内容, 在 Application.Run 前,是不是已经 Create 过 Form2 了呢?建议您可以先从主选单 Option | Project, 将 Form2 从Auto-create forms 移到Available程forms, 然後在 Button1的OnClick 事件中加入以下程式段:
Application.CreateForm(TForm2, Form2);
Form2.Show;
相信你在 Form2 Create 事件中写的程式码就会如你的预期了。或者, 您可以用下列的方法试试看:
unit Unit1;
interface
uses
...., Unit2;
.
.
implementation
{$R *.DFM}
procedure TForm1.Button1Click(Sender: TObject);
begin
Form2.Edit1.Text := Form1.Edit1.Text;
Form2.Show;
end;
end.
 
我好像觉得你的方向错了,正好 Another_eYes 大侠也在,大家讨论一下吧。

我认为应该 override TComponent 的 DefineProperties 方法,看看 TDBGrid 吧:
procedure DefineProperties(Filer: TFiler); override;
procedure TCustomDBGrid.DefineProperties(Filer: TFiler);
begin
...
// 定义属性
Filer.DefineProperty('Columns', ReadColumns, WriteColumns, StoreIt);
end;
然后定义读写类对象的方法,ReadColumns,、WriteColumns。

还有 TCustomGrid 保存列宽行高的例子:
procedure TCustomGrid.DefineProperties(Filer: TFiler);
// 判断有无必要保存列宽(没有改变则不保存)
function DoColWidths: Boolean;
begin
if Filer.Ancestor <> nil then
Result := not CompareExtents(TCustomGrid(Filer.Ancestor).FColWidths, FColWidths)
else
Result := FColWidths <> nil;
end;
// 判断有无必要保存行高(没有改变则不保存)
function DoRowHeights: Boolean;
begin
if Filer.Ancestor <> nil then
Result := not CompareExtents(TCustomGrid(Filer.Ancestor).FRowHeights, FRowHeights)
else
Result := FRowHeights <> nil;
end;


begin
inherited DefineProperties(Filer);
if FSaveCellExtents then
with Filer do
begin
DefineProperty('ColWidths', ReadColWidths, WriteColWidths, DoColWidths);
DefineProperty('RowHeights', ReadRowHeights, WriteRowHeights, DoRowHeights);
end;
end;

procedure TCustomGrid.ReadColWidths(Reader: TReader);
var
I: Integer;
begin
with Reader do
begin
ReadListBegin;
for I := 0 to ColCount - 1 do ColWidths := ReadInteger;
ReadListEnd;
end;
end;

procedure TCustomGrid.ReadRowHeights(Reader: TReader);
var
I: Integer;
begin
with Reader do
begin
ReadListBegin;
for I := 0 to RowCount - 1 do RowHeights := ReadInteger;
ReadListEnd;
end;
end;
最关键的一点是:自定义的对象,需要自己动手去读写。

所以我认为这个问题的解决方案应该是这样:
首先定义对象的读写函数:
procedure TFRPopupMenu.LoadMenu(Reader: TReader);
begin
if Reader.ReadBoolean then
FPMnuDBEditUndoAll := Reader.ReadComponent(nil);
end;
procedure TFRPopupMenu.StoreMenu(Writer: TWriter);
begin
Writer.WriteBoolean(FPMnuDBEditUndoAll <> nil);
if FPMnuDBEditUndoAll <> nil then
Writer.WriteComponent(FPMnuDBEditUndoAll);
end;
然后定义属性:
procedure TFRPopupMenu.DefineProperties(Filer: TFiler);// override;
function DoWrite: Boolean;
begin
if Filer.Ancestor <> nil then // 查找父类有没有继承值
begin
if TFRPopupMenu(Filer.Ancestor).FPMnuDBEditUndoAll = nil then
Result := FPMnuDBEditUndoAll <> nil
else if FPMnuDBEditUndoAll = nil or
TFRPopupMenu(Filer.Ancestor).FPMnuDBEditUndoAll.Name <> FPMnuDBEditUndoAll.Name then
Result := True

else Result := False;
end
else // 没有继承值
Result := FPMnuDBEditUndoAll<> nil;
end;
begin
inherited; // 继承父类
Filer.DefineProperty('FPMnuDBEditUndoAll', LoadMenu, StoreMenu, DoWrite);
end;
像这样做了以后,只需在构造函数中把 FPMnuDBEditUndoAll 置为 nil,就不要再在
程序里创建了,把属性 Publish 出来,让它在设计期可编辑,像标准的 TPopupMenu
一样。
From: BaKuBaKu
 
BaKuBaKu大虾:
  我最近实在是忙,大家的心意我十分感激,有回应不到的地方请各位多多包含。

》》最关键的一点是:自定义的对象,需要自己动手去读写《〈
是不是自编的控件都算是“自定义的对象”?理论上应该是这样,可我以前
写的控件工作的都非常正常,它们有些也有自己的属性存取操作,只是这个
需要自己在 Create 时建立 MenuItem 的不能成功。
我想留意一下其他的控件,看看有没有什么踪迹可寻。一有消息就立即与各
位大虾切磋切磋。
 
如果你自编的控件中包含某个VCL 可视控件作为子控件并且将它作为property
时需要特别留心对它的读写.
窍门: 不要在你的程序中Free它. 如果一定要Free, 则必须override Notification方法
并在那里面判断是否该控件已经被Delphi Free了.
确定是否有必要保存这个property到dfm中. 可以在property定义中加上关键字
stored false 防止保存到dfm中. 如果要保存的话:
一般不直接用Txxx.Create(Self)或者Txxx.Create(Owner)建立此控件. 如果没有进
行特殊处理, 将会在设计时产生大量clone(每保存一次产生一个). 如果用Create(Self)
建立的, 需要判断ComponentState中的csReading, csLoading标志, 如果置位的话就
不要程序Create.
如果打算用Create(Owner)建立的话, 首先要保证只能在运行时才调用. 设计时程序
里千万不能出现fxxx := Txxx.Create(Owner). 如果 要在设计期间建立此控件的要用
IFormDesigner.CreateComponent来建立(一般通过写个ComponentEditor来完成建立
工作).
只要注意好这几方面, 根本不必自己写它的Load和Store, 完全可以让Delphi做

 
各位大虾,真是不好意思,俺试来试去都还未彻底解决,
不过,现在已经有了很大进展,俺的倒霉控件现在可以只克隆一次了!!(大虾们不要哭笑不得嘛)
否则,绳子只能找根裤腰带把自己勒死算了。

等到问题彻底解决后,俺就把代码贴出来,到时再请各位大虾指正。
 
rope,

看了半天才看出来一点门道, 你的问题是你自己的TMenuItem是在创建时候自动生成的, 这样
用于动态创建是没有问题的; 但是在设计阶段, 当你保存Form的时候也把你的这个自动生成
的Item保存到了Form中; 这样问题就来了, 下次Form在读入资源的时候, 读到TFRPopupMenu
的时候它就创建一个TFRPopupMenu, 注意这个时候TFRPopupMenu.Create会创建一个你的Item,
然后再从资源中读入TFRPopupMenu的Items的时候就会出现问题,因为已经有了一个名字叫做
'PMnuDBEditUndoAll'的Item了.

解决办法很简单:
override Loaded, 把自动创建Item的部分移动到Loaded过程中去; 因为你不需要保存这个Item

Procedure TFRPopupMenu.Loaded;
begin
FRCreatePopupMenuItem;
inherited;
End;

 
上面的处理还是有问题,在第一次使用的时候可以,但是在保存后,还是会把你的Item保存到Stream里面
最简单的解决办法是:
Procedure TFRPopupMenu.Loaded;
begin
if Items.Find('PMnuDBEditUndoAll') = nil then
FRCreatePopupMenuItem;
inherited;
End;
 
老屯大哥:
  可是,在第一次向窗体加入控件的时候,并不会触发 Loaded 事件。
  总不能让程序员在加入控件以后关闭窗体,再重新打开一次吧,这样好象不太专业。
 
rope:
将菜单的内容保存到item中可以解决,
...
private
fitem:tmenuitem;
published
item:Tmenuite; read getitem write setitem;
当窗体被建立时,程序会读出属性信息,调用write方法,在write方法中建立menuitem
 
to Rope:
当然你第一次加入控件的时候不会触发Loaded事件,但是在程序执行的时候会触发它的
 
老屯大哥:
  可是,在第一次向窗体加入控件的时候,并不会触发 Loaded 事件。
  在程序员编程的时候,总不能让他加入控件以后关闭窗体,再重新打开一次吧。
 
overload notification
 
后退
顶部