Search code examples
winapilocal-variablesdelphi-xe8

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


I have a strage effekt in Delphi XE8 and would like to know if anyone can reproduce this and has an explanation for it!

I'm calling the windows api function GetClipboardFormatName with a local variable as a buffer to receive the clipboard format names.

When this is done from a TButton Click Handler it works as expected, when it's done from a TToolButton Click Handler then it does not work and getlasterror returns 998 / ERROR_NOACCESS / Invalid access to memory location.

This did not happen under Delphi 7!

I'm not looking for a workaround, i just would like to know what is going on here. Am I doing something wrong? Is there a problem with our IDE installation (2 Developers)? Is it a BUG in XE8?

Here is a demo unit:

DFM File

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 File

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.

Solution

  • Your code is broken. The error is here:

    GetClipboardFormatName(Format, fmtname, SizeOf(fmtname))
    

    The documentation of GetClipboardFormatName describes the cchMaxCount parameter like this:

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

    You are passing the length in bytes rather than the length in characters. In Delphi 7 Char is an alias for AnsiChar, an 8 bit type, but in Unicode Delphi, 2009 and later, Char is an alias for WideChar, a 16 bit type.

    As a consequence, under XE8 which is a Unicode Delphi, you are claiming that the buffer is twice as long as it actually is.

    You must replace SizeOf(fmtname) with Length(fmtname).

    I should also mention that the change from 8 bit ANSI to 16 bit UTF-16 Unicode in Delphi 2009 should always be your first suspect when you find behaviour difference between ANSI Delphi and Unicode Delphi. In your question you wondered whether this was a Delphi bug, or an installation issue, but the first thought into your head should have been an issue with text encoding. With the reported symptoms that is going to be the culprit almost every time.


    As an aside, it makes no real sense for GetRegisteredClipBoardFormatName to be an instance method of a GUI form. It doesn't refer to Self and it has nothing at all to do with your form class. This should be a low-level helper method that is not part of a GUI form type.