4

i have an example descendant of TBitmap:

TMyBitmap = class(TBitmap)
public
    constructor Create; override;
end;

constructor TMyBitmap.Create;
begin
   inherited;
   Beep;
end;

At run-time i construct one of these TMyBitmap objects, load an image into it, and place it into a TImage on the form:

procedure TForm1.Button1Click(Sender: TObject);
var
   g1: TGraphic;
begin
   g1 := TMyBitmap.Create;
   g1.LoadFromFile('C:\...\example.bmp');

   Image1.Picture.Graphic := g1;
end;

Inside of TPicture.SetGraphic you can see that it makes a copy of the graphic, by constructing a new one, and calling .Assign on the newly constructed clone:

procedure TPicture.SetGraphic(Value: TGraphic);
var
   NewGraphic: TGraphic;
begin
   ...
   NewGraphic := TGraphicClass(Value.ClassType).Create;
   NewGraphic.Assign(Value);
   ...
end;

The line where the new graphic class is constructed:

NewGraphic := TGraphicClass(Value.ClassType).Create;

correctly calls my constructor, and all is well.


i want to do something similar, i want to clone a TGraphic:

procedure TForm1.Button1Click(Sender: TObject);
var
   g1: TGraphic;
   g2: TGraphic;
begin
   g1 := TMyBitmap.Create;
   g1.LoadFromFile('C:\...\example.bmp');

   //Image1.Picture.Graphic := g1;
   g2 := TGraphicClass(g1.ClassType).Create;
end;

Except this never calls my constructor, nor is it calling TBitmap constructor. It's only calling TObject constructor. After construction:

g2.ClassName: 'TMyBitmap'
g2.ClassType: TMyBitmap

The type is right, but it doesn't call my constructor, but identical code elsewhere does.

Why?


Even in this hypothethetical contrived example it's still a problem, because the constructor of TBitmap isn't being called; internal state variables are not being initialized to valid values:

constructor TBitmap.Create;
begin
  inherited Create;
  FTransparentColor := clDefault;
  FImage := TBitmapImage.Create;
  FImage.Reference;
  if DDBsOnly then HandleType := bmDDB;
end;

The version in TPicture:

NewGraphic := TGraphicClass(Value.ClassType).Create;

decompiles to:

mov eax,[ebp-$08]
call TObject.ClassType
mov dl,$01
call dword ptr [eax+$0c]
mov [ebp-$0c],eax

My version:

g2 := TGraphicClass(g1.ClassType).Create;

decompiles to:

mov eax,ebx
call TObject.ClassType
mov dl,$01
call TObject.Create
mov ebx,eax

Update One

Pushing the "cloning" to a separate function:

function CloneGraphic(Value: TGraphic): TGraphic;
var
    NewGraphic: TGraphic;
begin
   NewGraphic := TGraphicClass(Value.ClassType).Create;
   Result := NewGraphic;
end;

doesn't help.

Update Two

Clearly, i'm clearly providing a clear screenshot clearly of my clearly code that clearly shows that my clearly code is clearly all there clearly is. Clearly:

enter image description here

Update Three

Here's an unambigious version with OutputDebugStrings:

{ TMyGraphic }

constructor TMyBitmap.Create;
begin
  inherited Create;
    OutputDebugStringA('Inside TMyBitmap.Create');
end;

function CloneGraphic(Value: TGraphic): TGraphic;
var
    NewGraphic: TGraphic;
begin
    NewGraphic := TGraphicClass(Value.ClassType).Create;
    Result := NewGraphic;
end;

procedure TForm1.Button1Click(Sender: TObject);
var
    g1: TGraphic;
    g2: TGraphic;
begin
    OutputDebugString('Creating g1');
    g1 := TMyBitmap.Create;
    g1.LoadFromFile('C:\Archive\-=Images=-\ChessvDanCheckmateIn38.bmp');
    OutputDebugString(PChar('g1.ClassName: '+g1.ClassName));

    OutputDebugStringA('Assigning g1 to Image.Picture.Graphic');
    Image1.Picture.Graphic := g1;

    OutputDebugString('Creating g2');
    g2 := Graphics.TGraphicClass(g1.ClassType).Create;
    OutputDebugString(PChar('g2.ClassName: '+g2.ClassName));

    OutputDebugString(PChar('Cloning g1 into g2'));
    g2 := CloneGraphic(g1);
    OutputDebugString(PChar('g2.ClassName: '+g2.ClassName));
end;

And the raw results:

ODS: Creating g1 Process Project2.exe ($1138)
ODS: Inside TMyBitmap.Create Process Project2.exe ($1138)
ODS: g1.ClassName: TMyBitmap Process Project2.exe ($1138)
ODS: Assigning g1 to Image.Picture.Graphic Process Project2.exe ($1138)
ODS: Inside TMyBitmap.Create Process Project2.exe ($1138)
ODS: Creating g2 Process Project2.exe ($1138)
ODS: g2.ClassName: TMyBitmap Process Project2.exe ($1138)
ODS: Cloning g1 into g2 Process Project2.exe ($1138)
ODS: g2.ClassName: TMyBitmap Process Project2.exe ($1138)
ODS: g1.ClassName: TMyBitmap Process Project2.exe ($1138)

And the formatted results:

Creating g1
   Inside TMyBitmap.Create
g1.ClassName: TMyBitmap

Assigning g1 to Image.Picture.Graphic
   Inside TMyBitmap.Create

Creating g2
g2.ClassName: TMyBitmap

Cloning g1 into g2
g2.ClassName: TMyBitmap

g1.ClassName: TMyBitmap

Update Four

i tried turning off all compilers options i could:

enter image description here

Note: Don't turn off Extended syntax. Without it you cannot assign the Result of a function (Undeclared identifier Result).

Update Five

Following @David's suggestion, i tried compiling the code on some other machines (all Delphi 5):

  • Ian Boyd (me): Fails (Windows 7 64-bit)
  • Dale: Fails (Windows 7 64-bit)
  • Dave: Fails (Windows 7 64-bit)
  • Chris: Fails (Windows 7 64-bit)
  • Jamie: Fails (Windows 7 64-bit)
  • Jay: Fails (Windows XP 32-bit)
  • Customer Build Server: Fails (Windows 7 32-bit)

Here's the source.

Ian Boyd
  • 246,734
  • 253
  • 869
  • 1,219
  • Maybe you should tamper with compiler directives, some optimizations might affect virtual method table calls for your code and do not affect VCL library as it's precompiled. – too Mar 16 '11 at 23:08
  • @too, for example, which compiler directive affect virtual method table calls to occur correctly? – jachguate Mar 17 '11 at 00:14
  • @Ian are you sure the g1.classtype is still TMyBitmap just before the last line of your Button1Click routine? I bet the class is somewhere changing in the not shown lines. As @David, I get two beeps also in Delphi XE, I have no D5 at hand to test, but it seems unlikely a bug like this is present at that time to me (not even in D1). :) – jachguate Mar 17 '11 at 00:17
  • @jachguate I tested with Delphi 2010 and Delphi 6. Clearly @Ian has got some extra code that we can't see. – David Heffernan Mar 17 '11 at 00:40
  • @David Heffernan. i've updated the question with a screenshot of my code – Ian Boyd Mar 17 '11 at 11:43
  • @Ian Sorry, but I'm not going to type in your code from a screenshot!! – David Heffernan Mar 17 '11 at 11:54
  • @jachguate i've updated the question to a version of the code that does ODS of class names. i'm sure `g1.ClassType` is still `TMyBitmap` just before the last line of my event handler. – Ian Boyd Mar 17 '11 at 11:54
  • This is a very long question, indeed! – Andreas Rejbrand Mar 17 '11 at 14:41
  • @David: Naturally you were supposed to OCR the bitmap! – Andreas Rejbrand Mar 17 '11 at 15:20
  • @Andreas Rejbrand i just wanted to keep any readers up to date on things i'd tried, or evidence i'd collected. (i.e. not like it'd created the question and given up). – Ian Boyd Mar 17 '11 at 17:18
  • @Ian Great that it's finally been solved. I knew it had to be something that the rest of us could not see, but I don't think I'd ever have worked it out without access to D5 source. I never doubted that you had a problem, by the way, I just couldn't work out what it could be. I've now deleted my answer because it is no longer useful to anyone. – David Heffernan Mar 17 '11 at 17:54

2 Answers2

7

This seems to be a scoping problem (the following is from D5 Graphics.pas):

TGraphic = class(TPersistent)
...
protected
  constructor Create; virtual;
...
end;

TGraphicClass = class of TGraphic;

You don't have any problems overriding Create, and you don't have any problems when TGraphicClass(Value.ClassType).Create; is called from within the Graphics.pas unit.

However, in another unit TGraphicClass(Value.ClassType).Create; does not have access to protected members of TGraphic. So therefore you end up calling TObject.Create; (which is non-virtual).

Possible Solutions

  • Edit and recompile Graphics.pas
  • Ensure your clone method subclasses lower down the hierarchy. (e.g. TBitmap.Create is public)

EDIT: Additional Solution

This is a variation on the technique to gain access to the protected members of a class.
No guarantees on the robustness of the solution, but it does seem to work. :)
You'll have to do your own extensive testing I'm afraid.

type
  TGraphicCracker = class(TGraphic)
  end;

  TGraphicCrackerClass = class of TGraphicCracker;

procedure TForm1.Button1Click(Sender: TObject);
var
  a: TGraphic;
  b: TGraphic;
begin
  a := TMyBitmap.Create;
  b := TGraphicCrackerClass(a.ClassType).Create;
  b.Free;
  a.Free;
end;
Disillusioned
  • 14,635
  • 3
  • 43
  • 77
  • Well holy hell. You sir win +100 internets. That is exactly the problem, and exactly the solution. **+1 Extraordinarily helpful** answer that answers the exact question. i used the well known "**Cracker**" solution. i presume that in later versions of Delphi, the constructor of `TGraphic` was promoted to `public`, which is why nobody was able to reproduce it? – Ian Boyd Mar 17 '11 at 17:13
  • @IanBoyd The `TGraphic` constructor was made `public` in Delphi 6. – Remy Lebeau Jan 02 '23 at 04:12
3

For what it's worth: I downloaded your source (the ZIP file), and ran CannotCloneGraphics.exe and got a "Not valid." error message. Then I opened the project (the DPR file) in Delphi 2009, compiled it, and ran it. Then I didn't get any error message, and the custom constructur ran four times, as it should.

It would thus seem like this is an issue with your Delphi 5 installations. Indeed, all your machines had Delphi 5 (time to upgrade?!). Either there is some issue with Delphi 5, or all your machines have been "tampered" with in the same way.

I am pretty sure I have an old Delphi 4 Personal somewhere. I might install it and see what happens there...

Update

I just installed Delphi 4 Standard in a virtual Windows 95 system. I tried this code:

  TMyBitmap = class(TBitmap)
  public
    constructor Create; override;
  end;

  ...

  constructor TMyBitmap.Create;
  begin
    inherited;
    ShowMessage('Constructor constructing!');
  end;

  ...

  procedure TForm1.Button1Click(Sender: TObject);
  var
    g, g2: TGraphic;
  begin
    g := TMyBitmap.Create;
    g2 := TGraphicClass(g.ClassType).Create;
    g.Free;
    g2.Free;
  end;

and I only got one message box! Therefore, this is a problem with Delphi 4 (and 5), after all. (Sorry, David!)

Andreas Rejbrand
  • 105,602
  • 8
  • 282
  • 384
  • Finally, someone else with the issue. i agree that it's time to upgrade, but i'm not the person in charge of that polotical decision. This makes the answer "Issue with Delphi 5 (and more)". So i'll rate this answer **+1** - **very** helpful. i'll have to see if Craig's answer actually works around the bug. – Ian Boyd Mar 17 '11 at 17:21
  • You don't need to apologise to me. I never doubted what Ian was saying, I just couldn't reproduce it or imagine what it was that meant he saw different behaviour. In the comments you'll see that I called out the fact that nobody else had tried with D5 and that was indeed the issue. – David Heffernan Mar 17 '11 at 17:55