Delphi 窗体间通信之事件回调模式

 

test

 

Delphi 窗体间数据传递的「事件回调模式」套路总结

一、核心思想

「单向依赖 + 事件驱动」 - 下级窗体通过事件通知上级,避免双向引用导致的循环依赖。

在下级窗体中完成参数收集工作,并传向上级窗体.

在上级窗体关联并具现事件方法.

以下以FrmRK单元(上级)和FMTop20Record (下级)为例,演示实现过程

二、标准套路(5步法)

第1步:定义事件类型(在下级单元)

// 在 FMTop20Record 单元的 interface 部分添加
type
  // 定义物料选择事件类型.自定义事件要定义在窗体之前,因为窗体要引用这个自定义事件
  TMaterialSelectEvent = procedure(Sender: TObject; 
    MaterialID: Integer; 
    MaterialCode, MaterialName, SpecModel, CustomerPartNo: string;
    WarehouseID: Integer) of object;

 

第2步:暴露事件属性(在下级类)

  TTop20Record = class(TForm)    //注意窗体和事件的定义顺序
  // ... 原有代码
  private
    FOnMaterialSelected: TMaterialSelectEvent; // 物料选择事件
  published
    // 物料选择事件属性
    property OnMaterialSelected: TMaterialSelectEvent read FOnMaterialSelected write FOnMaterialSelected;
  end;

 

第3步:触发事件(在下级适当位置)

这是cxGrid的一个OnCellDblClick事件,即单元格的双击事件,它把用户双击的单元格的数据收集起来,并传给上级

procedure TTop20Record.GridViewTop20CellDblClick(Sender: TcxCustomGridTableView;
  ACellViewInfo: TcxGridTableDataCellViewInfo; AButton: TMouseButton;
  AShift: TShiftState; var AHandled: Boolean);
begin
  AHandled := True; // 标记事件已处理
  
  if FDQTop20.IsEmpty then 
    Exit; // 数据集为空则退出
    
  // 检查是否有事件处理程序
  if Assigned(FOnMaterialSelected) then
  begin
    // 触发物料选择事件,传递当前行的数据
    FOnMaterialSelected(Self,
      FDQTop20.FieldByName('物料ID').AsInteger,
      FDQTop20.FieldByName('物料代码').AsString,
      FDQTop20.FieldByName('物料名称').AsString,
      FDQTop20.FieldByName('规格型号').AsString,
      FDQTop20.FieldByName('客户料号').AsString,
      FDQTop20.FieldByName('仓库ID').AsInteger);
  end;
end;

 

第4步:实现事件处理(在上级单元)

procedure TRK.HandleMaterialSelected(Sender: TObject; 
  MaterialID: Integer; 
  MaterialCode, MaterialName, SpecModel, CustomerPartNo: string;
  WarehouseID: Integer);
var
  Found: Boolean;
begin
  // 1. 检查是否已存在相同的物料ID
  Found := False;
  FDMemTable1.DisableControls; // 禁用控件更新以提高性能
  try
    FDMemTable1.First;
    while not FDMemTable1.EOF do
    begin
      if FDMemTable1.FieldByName('物料ID').AsInteger = MaterialID then
      begin
        Found := True;
        Break;
      end;
      FDMemTable1.Next;
    end;
  finally
    FDMemTable1.EnableControls; // 恢复控件更新
  end;
  
  // 2. 如果已存在,提示用户
  if Found then
  begin
    if MessageDlg('该物料已存在于入库单中,是否继续添加?',
                  mtConfirmation, [mbYes, mbNo], 0) = mrNo then
      Exit;
  end;
  
  // 3. 添加新记录到FDMemTable1
  FDMemTable1.Append;
  try
    FDMemTable1.FieldByName('物料ID').AsInteger := MaterialID;
    FDMemTable1.FieldByName('物料代码').AsString := MaterialCode;
    FDMemTable1.FieldByName('物料名称').AsString := MaterialName;
    FDMemTable1.FieldByName('规格型号').AsString := SpecModel;
    FDMemTable1.FieldByName('客户料号').AsString := CustomerPartNo;
    FDMemTable1.FieldByName('数量').AsFloat := 1.0; // 默认数量
    FDMemTable1.FieldByName('备注').AsString := ''; // 空备注
    FDMemTable1.FieldByName('收货仓库').AsInteger := WarehouseID;
    FDMemTable1.Post; // 提交记录
  except
    FDMemTable1.Cancel; // 出错时取消添加
    raise; // 重新抛出异常
  end;
end;

 

第5步:关联事件(创建下级窗体时,也就是showModal前)

procedure TRK.入库单号RightButtonClick(Sender: TObject);begin
  // ... 原有的检查代码 ...
  
  var FM := TTop20Record.Create(nil);
  try
    var FM.sobj := 入库对象.Text;// 关联事件处理程序.关键步骤
    FM.OnMaterialSelected := HandleMaterialSelected;
    
    FM.GetData;
    FM.ShowModal;
  finally
    FM.Free;
  end;
end;

 

三、设计模式要点

1. 依赖方向

 
父窗体 (FrmRK) → 使用 → 子窗体 (FMTop20Record)
    ↑                          ↓
    |--(事件回调)------------|
  • 编译时依赖:父→子(uses 子单元)

  • 运行时通信:子→父(通过事件)

2. 松耦合设计

  • 子窗体不知道谁处理事件

  • 父窗体不知道子窗体内部实现

  • 仅通过事件接口交互

3. 扩展性优势

  • 同一子窗体可被多个父窗体复用

  • 只需实现不同的事件处理程序

  • 添加新参数只需修改事件类型定义

四、使用场景判断

适合使用此模式:

  1. 父窗体打开子窗体选择/查询数据

  2. 需要将子窗体的结果传回父窗体

  3. 避免窗体间的循环引用

  4. 希望子窗体可被多处复用

不适合此模式:

  1. 简单的消息显示(用 ShowMessage)

  2. 单向数据展示(用属性传递)

  3. 复杂的主从窗体(用数据模块)

五、常见变体

变体1:多事件支持

// 子窗体可定义多个事件
property OnSelect: TSelectEvent;
property OnCancel: TCancelEvent;
property OnClose: TCloseEvent;

 

变体2:模态结果增强

// 结合 ModalResult 和事件
if Assigned(FOnDataSelected) then
begin
  FOnDataSelected(Self, Data);
  ModalResult := mrOK;  // 自动关闭
end;

 

变体3:带返回值的事件

// 事件处理程序可返回布尔值
TValidationEvent = function(Sender: TObject; Data: TData): Boolean of object;

 

六、对比其他方案

 
方案优点缺点适用场景
事件回调 松耦合、可复用、类型安全 稍复杂 窗体间数据传递
接口方式 更灵活、支持多接口 更复杂、需定义GUID 复杂交互系统
全局变量 简单直接 紧耦合、难维护 简单原型
消息机制 完全解耦 类型不安全、调试困难 跨进程/复杂框架

七、最佳实践

  1. 命名规范

    • 事件类型:T[动作]Event

    • 事件属性:On[动作]

    • 处理方法:Handle[动作]

  2. 参数设计

    • 包含 Sender: TObject 参数

    • 按需传递最少必要数据

    • 避免传递大型对象(传ID而非对象)

  3. 错误处理

    • 始终检查 Assigned(FOnXxx)

    • 在事件处理器中做好异常处理

    • 考虑添加取消机制

八、记忆口诀

「一下二暴三触发,四实五联搞定它」

  • 一下:下级定义事件类型

  • 二暴:暴露事件属性

  • 三触发:适当时机触发事件

  • 四实:上级实现处理方法

  • 五联:创建时关联事件

这个套路是 Delphi 窗体间通信的经典解决方案,掌握后可以应对80%的窗体数据传递需求,既避免了循环引用,又保持了代码的清晰和可维护性。

posted @ 2025-12-26 15:18  一曲轻扬  阅读(58)  评论(0)    收藏  举报