Delphi XE8 / 从 TToolbutton Click windows api 函数 GetClipboardFormatName 调用时对内存位置的访问无效

Delphi XE8 / Invalid access to memory location when calling windows api function GetClipboardFormatName from TToolbutton Click

我在 Delphi XE8 中有一个 strage effekt,想知道是否有人可以重现它并给出解释!

我正在调用 windows api 函数 GetClipboardFormatName 并使用局部变量作为接收剪贴板格式名称的缓冲区。

当这是从 TButton Click Handler 完成时,它按预期工作,当它从 TToolButton Click Handler 完成时,它不起作用并且 getlasterror returns 998 / ERROR_NOACCESS / 无效访问内存位置。

这在 Delphi 7!

下没有发生

我不是在寻找解决方法,我只是想知道这里发生了什么。难道我做错了什么?我们的 IDE 安装(2 个开发人员)有问题吗?是XE8的BUG吗?

这是一个演示单元:

DFM 文件

object Form3: TForm3
  Left = 0
  Top = 0
  Caption = 'Form3'
  ClientHeight = 311
  ClientWidth = 643
  Color = clBtnFace
  Font.Charset = DEFAULT_CHARSET
  Font.Color = clWindowText
  Font.Height = -11
  Font.Name = 'Tahoma'
  Font.Style = []
  OldCreateOrder = False
  PixelsPerInch = 96
  TextHeight = 13
  object Panel1: TPanel
    Left = 0
    Top = 0
    Width = 643
    Height = 41
    Align = alTop
    Caption = 'Panel1'
    TabOrder = 0
    object Button1: TButton
      Left = 16
      Top = 10
      Width = 148
      Height = 25
      Caption = 'Standard TButton ==> OK'
      TabOrder = 0
      OnClick = Button1Click
    end
  end
  object Memo1: TMemo
    Left = 0
    Top = 70
    Width = 643
    Height = 241
    Align = alClient
    Lines.Strings = (
      'Memo1')
    TabOrder = 1
  end
  object ToolBar1: TToolBar
    Left = 0
    Top = 41
    Width = 643
    Height = 29
    ButtonHeight = 21
    ButtonWidth = 289
    Caption = 'ToolBar1'
    ShowCaptions = True
    TabOrder = 2
    object ToolButton1: TToolButton
      Left = 0
      Top = 0
      Caption = 'Standard TToolBar / TToolButton ==> ERROR_NOACCESS'
      ImageIndex = 0
      OnClick = ToolButton1Click
    end
  end
end

PAS 文件

unit Unit3;

interface

uses
    Winapi.Windows,
    Winapi.Messages,
    System.SysUtils,
    System.Variants,
    System.Classes,
    Vcl.Graphics,
    Vcl.Controls,
    Vcl.Forms,
    Vcl.Dialogs,
    Vcl.StdCtrls,
    Vcl.ExtCtrls,
    Vcl.ComCtrls,
    Vcl.ToolWin;

type
    TForm3 = class(TForm)
        Panel1: TPanel;
        Memo1: TMemo;
        Button1: TButton;
        ToolBar1: TToolBar;
        ToolButton1: TToolButton;
        procedure Button1Click(Sender: TObject);
        procedure ToolButton1Click(Sender: TObject);
    private
        procedure say(s: string);
        procedure ListFormats;
        function GetRegisteredClipBoardFormatName(Format: word): string;
        function IsPredefinedFormat(format: word): boolean;
    { Private-Deklarationen }
    public
    { Public-Deklarationen }
    end;

var
    Form3: TForm3;

implementation

uses
    clipbrd;

const arPredefinedFormats: array[0..27] of word = (
        CF_TEXT,
        CF_BITMAP,
        CF_METAFILEPICT,
        CF_SYLK,
        CF_DIF,
        CF_TIFF,
        CF_OEMTEXT,
        CF_DIB,
        CF_PALETTE,
        CF_PENDATA,
        CF_RIFF,
        CF_WAVE,
        CF_UNICODETEXT,
        CF_ENHMETAFILE,
        CF_HDROP,
        CF_LOCALE,
        CF_MAX,
        CF_DIBV5,
        CF_MAX_XP,
        CF_OWNERDISPLAY,
        CF_DSPTEXT,
        CF_DSPBITMAP,
        CF_DSPMETAFILEPICT,
        CF_DSPENHMETAFILE,
        CF_PRIVATEFIRST,
        CF_PRIVATELAST,
        CF_GDIOBJFIRST,
        CF_GDIOBJLAST);

{$R *.dfm}

procedure TForm3.ToolButton1Click(Sender: TObject);
begin
    ListFormats;

end;


procedure TForm3.Button1Click(Sender: TObject);
begin
    ListFormats;
end;


procedure TForm3.ListFormats;
var
    index: integer;
begin
    for index := 0 to clipboard.formatcount - 1 do
    begin
        if not IsPredefinedFormat(clipboard.formats[index]) then
        begin
            say('Format: ' + inttostr(clipboard.formats[index]));
            say('Name: ' + GetRegisteredClipBoardFormatName(clipboard.formats[index]));
        end;
    end;
end;

procedure TForm3.say(s: string);
begin
    memo1.lines.add(s);
end;


function TForm3.IsPredefinedFormat(format: word): boolean;
var
    index: integer;
begin
    for index := low(arPredefinedFormats) to high(arPredefinedFormats) do
    begin
        if arPredefinedFormats[index] = format then
        begin
            result := true;
            exit;
        end;
    end;
    result := false;
end;

//------------------------------------------------------------------------------------------
(*
  Strange effekt in function GetClipboardFormatName
  when compiled with Delphi XE8 und Win 7.



  If this function is called from tbutton click, then everything ist ok!

  If this function is called from ttoolbutton click (and perhaps other controls...?)
  then the call to GetClipboardFormatName fails with getlasterror = 998
  which means

    ERROR_NOACCESS
    998 (0x3E6)
    Invalid access to memory location.

  which indicates that there is a problem with the local variable fmtname.



  Some Facts...

  * effekt happens under delphi xe8
  * effekt did not happen under delphi 7
  * it doesn't matter if I zero the memory of fmtname before using it.
  * it doesn't matter if I call OpenClipboard / CloseClipboard
  * if I use a local variable, then it does not work with ttoolbutton. The memorylocation of the local variable is
    slightly different from the case when it's called from tbutton.
  * if I use a global variable instead of a local variable, then it works with tbutton and ttoolbutton
    since it's the same memorylocation for both calls


  I'm NOT LOOKING FOR A WORKAROUND, I just would like to know if anybody can
  reproduce the behaviour and has an explanation as to why this is happening.

  Is there something wrong with using local variables for windows api calls in general?

*)
//------------------------------------------------------------------------------------------


function TForm3.GetRegisteredClipBoardFormatName(Format: word): string;
var
    fmtname: array[0..1024] of Char;
begin
    if OpenClipboard(self.handle) then    //<--- does not make a difference if called or not
    begin

        if GetClipboardFormatName(Format, fmtname, SizeOf(fmtname)) <> 0 then
        begin
            result := fmtname;
        end else
        begin
            result := 'Unknown Clipboard Format / GetLastError= ' + inttostr(getlasterror);
        end;

        CloseClipboard;
    end else say('OpenClipboard failed');
end;

//------------------------------------------------------------------------------------------




end.

您的代码已损坏。错误在这里:

GetClipboardFormatName(Format, fmtname, SizeOf(fmtname))

GetClipboardFormatName 的文档这样描述 cchMaxCount 参数:

The maximum length, in characters, of the string to be copied to the buffer. If the name exceeds this limit, it is truncated.

您传递的是字节长度而不是字符长度。在 Delphi 7 CharAnsiChar 的别名,一个 8 位类型,但在 Unicode Delphi, 2009 及之后, CharWideChar,一个16位的类型。

因此,在作为 Unicode Delphi 的 XE8 下,您声称缓冲区的长度是实际长度的两倍。

您必须将 SizeOf(fmtname) 替换为 Length(fmtname)

我还应该提到,当您发现 ANSI Delphi 和 Unicode 之间的行为差​​异时,Delphi 2009 中从 8 位 ANSI 到 16 位 UTF-16 Unicode 的变化应该始终是您的第一个怀疑对象Delphi。在您的问题中,您想知道这是 Delphi 错误还是安装问题,但您首先想到的应该是文本编码问题。几乎每一次报告的症状都是罪魁祸首。


顺便说一句,GetRegisteredClipBoardFormatName 成为 GUI 窗体的实例方法没有任何实际意义。它不涉及 Self,并且与您的表单 class 完全无关。这应该是一个 low-level 辅助方法,它不是 GUI 表单类型的一部分。