【问题标题】:Why does this code fail when declaring TMemoryStream locally but works when globally declared?为什么此代码在本地声明 TMemoryStream 时失败,但在全局声明时有效?
【发布时间】:2015-05-19 20:56:37
【问题描述】:

以下函数在 Richedit 控件中获取所选文本,在回调函数中写入 TMemoryStream,然后以纯文本字符串的形式返回原始 rtf 代码。

var
  MS: TMemoryStream; // declared globally and works.

implementation

function GetSelectedRTFCode(RichEdit: TRichedit): string;

  function RichEditCallBack(dwCookie: Longint; pbBuff: PByte;
    CB: Longint; var pCB: Pointer): Longint; stdcall;
  begin
    MS.WriteBuffer(pbBuff^, CB);
    Result := CB;
  end;

var
  EditStream: TEditStream;
  SL: TStringList;
begin
  MS := TMemoryStream.Create;
  try
    EditStream.dwCookie     := SF_RTF or SFF_SELECTION;
    EditStream.dwError      := 0;
    EditStream.pfnCallback  := @RichEditCallBack;
    Richedit.Perform(EM_StreamOut, SF_RTF or SFF_SELECTION, DWord(@EditStream));
    MS.Seek(0, soBeginning);

    SL := TStringList.Create;
    try
      SL.LoadFromStream(MS);
      Result := SL.Text;
    finally
      SL.Free;
    end;
  finally
    MS.Free;
  end;
end;

以上内容按预期工作,没有任何错误。

但是,我尽量避免全局声明的变量,并将它们保留在需要它的过程或函数的本地,但由于某种原因,在 GetSelectedRTFCode 函数内声明 MS: TMemoryStream; 失败并出现特权指令和访问冲突错误。

因此,考虑到这一点,下面唯一的更改是 MS: TMemoryStream; 在本地声明失败:

function GetSelectedRTFCode(RichEdit: TRichedit): string;
var
  MS: TMemoryStream; // declare here instead of globally but fails.

  function RichEditCallBack(dwCookie: Longint; pbBuff: PByte;
    CB: Longint; var pCB: Pointer): Longint; stdcall;
  begin
    MS.WriteBuffer(pbBuff^, CB);
    Result := CB;
  end;

var
  EditStream: TEditStream;
  SL: TStringList;
begin
  MS := TMemoryStream.Create;
  try
    EditStream.dwCookie     := SF_RTF or SFF_SELECTION;
    EditStream.dwError      := 0;
    EditStream.pfnCallback  := @RichEditCallBack;
    Richedit.Perform(EM_StreamOut, SF_RTF or SFF_SELECTION, DWord(@EditStream));
    MS.Seek(0, soBeginning);

    SL := TStringList.Create;
    try
      SL.LoadFromStream(MS);
      Result := SL.Text;
    finally
      SL.Free;
    end;
  finally
    MS.Free;
  end;
end;

为什么全局声明内存流变量有效,但在本地声明时失败?

【问题讨论】:

    标签: delphi delphi-xe7


    【解决方案1】:

    问题是您使用嵌套函数作为回调是错误的。以这种方式使用嵌套函数实现的机会适用于 32 位编译器,只要嵌套函数不引用周围函数的任何局部变量。

    但是,一旦嵌套函数引用任何此类局部变量,则必须传递一个额外的隐藏参数,以便嵌套函数可以访问周围的函数堆栈帧。而对于 64 位编译器,总是会传递一个隐藏的额外参数。

    您会在网络上找到很多示例,其中人们演示了将嵌套函数作为回调传递。但是所有这些例子都违反了documented 语言的规则:

    嵌套过程和函数(在其他例程中声明的例程)不能用作过程值,预定义过程和函数也不能。

    您必须做的是停止使用嵌套函数进行回调。您需要将回调函数声明为具有全局范围。通过EDITSTREAM 结构的dwCookie 成员传递内存流。

    // This compiles now, but the callback implementation is wrong, see below
    
    function RichEditCallBack(dwCookie: DWORD_PTR; pbBuff: PByte;
      CB: Longint; var pCB: Longint): Longint; stdcall;
    var
      MS: TMemoryStream;
    begin
      MS := TMemoryStream(dwCookie);
      MS.WriteBuffer(pbBuff^, CB);
      Result := CB;
    end;
    
    function GetSelectedRTFCode(RichEdit: TRichedit): string;
    var
      MS: TMemoryStream;
      EditStream: TEditStream;
      SL: TStringList;
    begin
      MS := TMemoryStream.Create;
      try
        EditStream.dwCookie     := DWORD_PTR(MS);
        EditStream.dwError      := 0;
        EditStream.pfnCallback  := RichEditCallBack;
        Richedit.Perform(EM_StreamOut, SF_RTF or SFF_SELECTION, LPARAM(@EditStream));
        MS.Seek(0, soBeginning);
    
        SL := TStringList.Create;
        try
          SL.LoadFromStream(MS);
          Result := SL.Text;
        finally
          SL.Free;
        end;
      finally
        MS.Free;
      end;
    end;
    

    请特别注意,我没有使用@ 运算符来获取回调函数的地址。在函数上使用@ 运算符会导致抑制类型检查。如果您没有使用 @ 运算符,那么编译器将能够告诉您您的错误。

    编译器会说:

    [dcc32 错误] E2094 本地过程/函数“RichEditCallBack”分配给 过程变量

    还要注意,您的代码错误地声明了最终参数的类型。它是Longint 类型的参考参数。同样,编译器可以报告这一点,并且确实会报告这一点,除非您使用 @ 获取函数地址。

    第二个错误导致回调的实现。这是不正确的。返回值表示成功。零值表示成功,任何其他值表示失败。写入的字节数必须通过最终参数返回。您的回调应如下所示:

    function RichEditCallBack(dwCookie: DWORD_PTR; pbBuff: PByte;
      CB: Longint; var CBWritten: Longint): Longint; stdcall;
    var
      MS: TMemoryStream;
    begin
      MS := TMemoryStream(dwCookie);
      CBWritten := MS.Write(pbBuff^, CB);
      Result := IfThen(CB = CBWritten, 0, 1);
    end;
    

    【讨论】:

    • IfThen 只需要 System.Math。我没有提到它,因为我怀疑你会知道在哪里可以找到它。也可以使用 if 语句来完成。
    • 您也可以add some sugar。顺便提一句。 EMBT 似乎越来越接近该回调的正确原型(尚不正确,但更接近)。自 Delphi XE3 以来它发生了变化。也许在 XE8 中最终会是正确的。
    • @TLama 啊,错误翻译的 Win32 原型的乐趣。在这种情况下,它并不重要,因为它是 0,或者不是 0。
    • 这是一个很好的答案,它详细、信息丰富且易于理解。我从你的回答中学到了很多,非常感谢。
    猜你喜欢
    • 2019-01-19
    • 1970-01-01
    • 1970-01-01
    • 2019-07-17
    • 1970-01-01
    • 1970-01-01
    • 2012-05-15
    • 2022-11-12
    • 1970-01-01
    相关资源
    最近更新 更多